312 lines
9.6 KiB
TypeScript
312 lines
9.6 KiB
TypeScript
/* An `action` is a unique verb that is associated with certain thing that can be done on Hoppscotch.
|
|
* For example, sending a request.
|
|
*/
|
|
|
|
import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue"
|
|
import { BehaviorSubject } from "rxjs"
|
|
import { HoppRESTDocument } from "./rest/document"
|
|
import { Environment, HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
|
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
|
|
import { HoppGQLSaveContext } from "./graphql/document"
|
|
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
|
|
import { computed } from "vue"
|
|
|
|
export type HoppAction =
|
|
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
|
| "request.send-cancel" // Send/Cancel a Hoppscotch Request
|
|
| "request.reset" // Clear request data
|
|
| "request.share-request" // Share Request
|
|
| "request.save" // Save to Collections
|
|
| "request.save-as" // Save As
|
|
| "request.rename" // Rename request on REST or GraphQL
|
|
| "request.method.next" // Select Next Method
|
|
| "request.method.prev" // Select Previous Method
|
|
| "request.method.get" // Select GET Method
|
|
| "request.method.head" // Select HEAD Method
|
|
| "request.method.post" // Select POST Method
|
|
| "request.method.put" // Select PUT Method
|
|
| "request.method.delete" // Select DELETE Method
|
|
| "request.import-curl" // Import cURL
|
|
| "request.show-code" // Show generated code
|
|
| "gql.connect" // Connect to GraphQL endpoint given
|
|
| "gql.disconnect" // Disconnect from GraphQL endpoint given
|
|
| "tab.close-current" // Close current tab
|
|
| "tab.close-other" // Close other tabs
|
|
| "tab.open-new" // Open new tab
|
|
| "collection.new" // Create root collection
|
|
| "flyouts.chat.open" // Shows the keybinds flyout
|
|
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
|
|
| "modals.search.toggle" // Shows the search modal
|
|
| "modals.support.toggle" // Shows the support modal
|
|
| "modals.share.toggle" // Shows the share modal
|
|
| "modals.environment.add" // Show add environment modal via context menu
|
|
| "modals.environment.new" // Add new environment
|
|
| "modals.environment.delete-selected" // Delete Selected Environment
|
|
| "modals.my.environment.edit" // Edit current personal environment
|
|
| "modals.global.environment.update" // Update global environment
|
|
| "modals.team.environment.edit" // Edit current team environment
|
|
| "modals.team.new" // Add new team
|
|
| "modals.team.edit" // Edit selected team
|
|
| "modals.team.invite" // Invite selected team
|
|
| "workspace.switch.personal" // Switch to personal workspace
|
|
| "navigation.jump.rest" // Jump to REST page
|
|
| "navigation.jump.graphql" // Jump to GraphQL page
|
|
| "navigation.jump.realtime" // Jump to realtime page
|
|
| "navigation.jump.documentation" // Jump to documentation page
|
|
| "navigation.jump.settings" // Jump to settings page
|
|
| "navigation.jump.profile" // Jump to profile page
|
|
| "settings.theme.system" // Use system theme
|
|
| "settings.theme.light" // Use light theme
|
|
| "settings.theme.dark" // Use dark theme
|
|
| "settings.theme.black" // Use black theme
|
|
| "response.preview.toggle" // Toggle response preview
|
|
| "response.file.download" // Download response as file
|
|
| "response.copy" // Copy response to clipboard
|
|
| "modals.login.toggle" // Login to Hoppscotch
|
|
| "history.clear" // Clear REST History
|
|
| "user.login" // Login to Hoppscotch
|
|
| "user.logout" // Log out of Hoppscotch
|
|
| "editor.format" // Format editor content
|
|
| "modals.team.delete" // Delete team
|
|
| "workspace.switch" // Switch workspace
|
|
| "rest.request.open" // Open REST request
|
|
| "request.open-tab" // Open REST request
|
|
| "share.request" // Share REST request
|
|
| "tab.duplicate-tab" // Duplicate REST request
|
|
| "gql.request.open" // Open GraphQL request
|
|
|
|
/**
|
|
* Defines the arguments, if present for a given type that is required to be passed on
|
|
* invocation and will be passed to action handlers.
|
|
*
|
|
* This type is supposed to be an object with the key being one of the actions mentioned above.
|
|
* The value to the key can be anything.
|
|
* If an action has no argument, you do not need to add it to this type.
|
|
*
|
|
* NOTE: We can't enforce type checks to make sure the key is Action, you
|
|
* will know if you got something wrong if there is a type error in this file
|
|
*/
|
|
type HoppActionArgsMap = {
|
|
"contextmenu.open": {
|
|
position: {
|
|
top: number
|
|
left: number
|
|
}
|
|
text: string | null
|
|
}
|
|
"modals.global.environment.update": {
|
|
variables?: Environment["variables"]
|
|
isSecret?: boolean
|
|
}
|
|
"modals.my.environment.edit": {
|
|
envName: string
|
|
variableName?: string
|
|
isSecret?: boolean
|
|
}
|
|
"modals.team.environment.edit": {
|
|
envName: string
|
|
variableName?: string
|
|
isSecret?: boolean
|
|
}
|
|
"modals.team.delete": {
|
|
teamId: string
|
|
}
|
|
"workspace.switch": {
|
|
teamId: string
|
|
}
|
|
"rest.request.open": {
|
|
doc: HoppRESTDocument
|
|
}
|
|
"request.save-as":
|
|
| {
|
|
requestType: "rest"
|
|
request: HoppRESTRequest
|
|
}
|
|
| {
|
|
requestType: "gql"
|
|
request: HoppGQLRequest
|
|
}
|
|
| undefined
|
|
"request.open-tab": {
|
|
tab: RESTOptionTabs | GQLOptionTabs
|
|
}
|
|
"share.request": {
|
|
request: HoppRESTRequest
|
|
}
|
|
"tab.duplicate-tab": {
|
|
tabID?: string
|
|
}
|
|
"gql.request.open": {
|
|
request: HoppGQLRequest
|
|
saveContext?: HoppGQLSaveContext
|
|
}
|
|
"modals.environment.add": {
|
|
envName: string
|
|
variableName: string
|
|
}
|
|
}
|
|
|
|
type KeysWithValueUndefined<T> = {
|
|
[K in keyof T]: undefined extends T[K] ? K : never
|
|
}[keyof T]
|
|
|
|
/**
|
|
* HoppActions which require arguments for their invocation
|
|
*/
|
|
export type HoppActionWithArgs = keyof HoppActionArgsMap
|
|
|
|
/**
|
|
* HoppActions which optionally takes in arguments for their invocation
|
|
*/
|
|
|
|
export type HoppActionWithOptionalArgs =
|
|
| HoppActionWithNoArgs
|
|
| KeysWithValueUndefined<HoppActionArgsMap>
|
|
|
|
/**
|
|
* HoppActions which do not require arguments for their invocation
|
|
*/
|
|
export type HoppActionWithNoArgs = Exclude<HoppAction, HoppActionWithArgs>
|
|
|
|
/**
|
|
* Resolves the argument type for a given HoppAction
|
|
*/
|
|
type ArgOfHoppAction<A extends HoppAction> = A extends HoppActionWithArgs
|
|
? HoppActionArgsMap[A]
|
|
: undefined
|
|
|
|
/**
|
|
* Resolves the action function for a given HoppAction, used by action handler function defs
|
|
*/
|
|
type ActionFunc<A extends HoppAction> = A extends HoppActionWithArgs
|
|
? (arg: ArgOfHoppAction<A>, trigger?: InvocationTriggers) => void
|
|
: (_?: undefined, trigger?: InvocationTriggers) => void
|
|
|
|
type BoundActionList = {
|
|
[A in HoppAction]?: Array<ActionFunc<A>>
|
|
}
|
|
|
|
const boundActions: BoundActionList = reactive({})
|
|
|
|
export const activeActions$ = new BehaviorSubject<HoppAction[]>([])
|
|
|
|
export function bindAction<A extends HoppAction>(
|
|
action: A,
|
|
handler: ActionFunc<A>
|
|
) {
|
|
if (boundActions[action]) {
|
|
boundActions[action]?.push(handler)
|
|
} else {
|
|
// 'any' assertion because TypeScript doesn't seem to be able to figure out the links.
|
|
boundActions[action] = [handler] as any
|
|
}
|
|
|
|
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
|
}
|
|
|
|
export type InvocationTriggers = "keypress" | "mouseclick"
|
|
|
|
type InvokeActionFunc = {
|
|
(
|
|
action: HoppActionWithOptionalArgs,
|
|
args?: undefined,
|
|
trigger?: InvocationTriggers
|
|
): void
|
|
<A extends HoppActionWithArgs>(action: A, args: HoppActionArgsMap[A]): void
|
|
}
|
|
|
|
/**
|
|
* Invokes an action, triggering action handlers if any registered.
|
|
* The second and third arguments are optional
|
|
* @param action The action to fire
|
|
* @param args The argument passed to the action handler. Optional if action has no args required
|
|
* @param trigger Optionally supply the trigger that invoked the action (keypress/mouseclick)
|
|
*/
|
|
export const invokeAction: InvokeActionFunc = <A extends HoppAction>(
|
|
action: A,
|
|
args?: ArgOfHoppAction<A>,
|
|
trigger?: InvocationTriggers
|
|
) => {
|
|
boundActions[action]?.forEach((handler) => handler(args! as any, trigger))
|
|
}
|
|
|
|
export function unbindAction<A extends HoppAction>(
|
|
action: A,
|
|
handler: ActionFunc<A>
|
|
) {
|
|
// 'any' assertion because TypeScript doesn't seem to be able to figure out the links.
|
|
boundActions[action] = boundActions[action]?.filter(
|
|
(x) => x !== handler
|
|
) as any
|
|
|
|
if (boundActions[action]?.length === 0) {
|
|
delete boundActions[action]
|
|
}
|
|
|
|
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
|
}
|
|
|
|
/**
|
|
* Returns a ref that indicates whether a given action is bound at a given time
|
|
*
|
|
* @param action The action to check
|
|
*/
|
|
export function isActionBound(action: HoppAction): Ref<boolean> {
|
|
return computed(() => !!boundActions[action])
|
|
}
|
|
|
|
/**
|
|
* A composable function that defines a component can handle a given
|
|
* HoppAction. The handler will be bound when the component is mounted
|
|
* and unbound when the component is unmounted.
|
|
* @param action The action to be bound
|
|
* @param handler The function to be called when the action is invoked
|
|
* @param isActive A ref that indicates whether the action is active
|
|
*/
|
|
export function defineActionHandler<A extends HoppAction>(
|
|
action: A,
|
|
handler: ActionFunc<A>,
|
|
isActive: Ref<boolean> | undefined = undefined
|
|
) {
|
|
let mounted = false
|
|
let bound = false
|
|
|
|
onMounted(() => {
|
|
mounted = true
|
|
|
|
// Only bind if isActive is undefined or true
|
|
if (isActive === undefined || isActive.value === true) {
|
|
bound = true
|
|
bindAction(action, handler)
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
mounted = false
|
|
bound = false
|
|
|
|
unbindAction(action, handler)
|
|
})
|
|
|
|
if (isActive) {
|
|
watch(
|
|
isActive,
|
|
(active) => {
|
|
if (mounted) {
|
|
if (active) {
|
|
if (!bound) {
|
|
bound = true
|
|
bindAction(action, handler)
|
|
}
|
|
} else if (bound) {
|
|
bound = false
|
|
|
|
unbindAction(action, handler)
|
|
}
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
}
|
|
}
|