diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json
index 6315e5d2b..1058eab0b 100644
--- a/packages/hoppscotch-common/locales/en.json
+++ b/packages/hoppscotch-common/locales/en.json
@@ -69,6 +69,8 @@
"invite": "Invite",
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
"invite_your_friends": "Invite your friends",
+ "social_links": "Social links",
+ "social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
"join_discord_community": "Join our Discord community",
"keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch",
@@ -203,6 +205,7 @@
"create_new": "Create new environment",
"created": "Environment created",
"deleted": "Environment deletion",
+ "duplicated": "Environment duplicated",
"edit": "Edit Environment",
"global": "Global",
"empty_variables": "No variables",
@@ -220,6 +223,7 @@
"replace_with_variable": "Replace with variable",
"scope": "Scope",
"select": "Select environment",
+ "set": "Set environment",
"set_as_environment": "Set as environment",
"team_environments": "Team Environments",
"title": "Environments",
@@ -594,6 +598,9 @@
"delete_method": "Select DELETE method",
"get_method": "Select GET method",
"head_method": "Select HEAD method",
+ "rename": "Rename Current Request",
+ "import_curl": "Import cURL",
+ "show_code": "Show generated code",
"method": "Method",
"next_method": "Select Next method",
"post_method": "Select POST method",
@@ -602,6 +609,7 @@
"reset_request": "Reset Request",
"save_to_collections": "Save to Collections",
"send_request": "Send Request",
+ "save_request": "Save Request",
"title": "Request"
},
"response": {
@@ -632,6 +640,55 @@
"url": "URL"
},
"spotlight": {
+ "general": {
+ "help_menu": "Open help and support menu",
+ "chat": "Chat with support",
+ "open_docs": "Open documentation",
+ "open_keybindings": "Open keyboard shortcuts",
+ "social": "Social links and GitHub",
+ "title": "General"
+ },
+ "miscellaneous": {
+ "invite": "Invite people to Hoppscotch",
+ "title": "Miscellaneous"
+ },
+ "request": {
+ "tab_parameters": "Open parameters tab",
+ "tab_body": "Open body tab",
+ "tab_headers": "Open headers tab",
+ "tab_authorization": "Open authorization tab",
+ "tab_pre_request_script": "Open pre-request script tab",
+ "tab_tests": "Open tests tab"
+ },
+ "response": {
+ "copy": "Copy response as JSON",
+ "download": "Download response as file",
+ "title": "Response"
+ },
+ "environments": {
+ "new": "Create new environment",
+ "new_variable": "Create a new environment variable",
+ "edit": "Edit selected environment",
+ "delete": "Delete selected environment",
+ "duplicate": "Duplicate selected environment",
+ "edit_global": "Edit global environment",
+ "duplicate_global": "Duplicate global environment",
+ "title": "Environments"
+ },
+ "workspace": {
+ "new": "Create new team",
+ "edit": "Edit selected team",
+ "delete": "Delete selected team",
+ "invite": "Invite people to team",
+ "switch_to_personal": "Switch to personal workspace",
+ "title": "Teams"
+ },
+ "tab": {
+ "close_current": "Close current tab",
+ "close_others": "Close others tab",
+ "new_tab": "Open a new tab",
+ "title": "Tabs"
+ },
"section": {
"user": "User",
"theme": "Theme",
@@ -649,9 +706,9 @@
"system": "System Mode"
},
"font": {
- "size_sm": "Change Font Size to Small",
- "size_md": "Change Font Size to Medium",
- "size_lg": "Change Font Size to Large"
+ "size_sm": "Change to Small",
+ "size_md": "Change to Medium",
+ "size_lg": "Change to Large"
},
"change_interceptor": "Change Interceptor",
"change_language": "Change Language",
diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts
index 4b3dd1425..1bfcbcd9b 100644
--- a/packages/hoppscotch-common/src/components.d.ts
+++ b/packages/hoppscotch-common/src/components.d.ts
@@ -1,11 +1,11 @@
-/* eslint-disable */
-/* prettier-ignore */
-// @ts-nocheck
-// Generated by unplugin-vue-components
+// generated by unplugin-vue-components
+// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
+import '@vue/runtime-core'
+
export {}
-declare module 'vue' {
+declare module '@vue/runtime-core' {
export interface GlobalComponents {
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
@@ -24,10 +24,13 @@ declare module 'vue' {
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
+ AppSocial: typeof import('./components/app/Social.vue')['default']
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
+ AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
+ AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
AppSupport: typeof import('./components/app/Support.vue')['default']
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
@@ -81,6 +84,7 @@ declare module 'vue' {
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
+ HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
@@ -210,4 +214,5 @@ declare module 'vue' {
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
}
+
}
diff --git a/packages/hoppscotch-common/src/components/app/ActionHandler.vue b/packages/hoppscotch-common/src/components/app/ActionHandler.vue
index 6f1cfa547..1ca90fa93 100644
--- a/packages/hoppscotch-common/src/components/app/ActionHandler.vue
+++ b/packages/hoppscotch-common/src/components/app/ActionHandler.vue
@@ -1,17 +1,57 @@
+
+
+
diff --git a/packages/hoppscotch-common/src/components/app/Header.vue b/packages/hoppscotch-common/src/components/app/Header.vue
index bcb434827..2c6100c94 100644
--- a/packages/hoppscotch-common/src/components/app/Header.vue
+++ b/packages/hoppscotch-common/src/components/app/Header.vue
@@ -382,6 +382,22 @@ const settings = ref(null)
const logout = ref(null)
const accountActions = ref(null)
+defineActionHandler("modals.team.edit", () => {
+ // TODO: Remove this hack
+ setTimeout(() => {
+ handleTeamEdit()
+ }, 100)
+})
+
+defineActionHandler("modals.team.invite", () => {
+ if (
+ selectedTeam.value?.myRole === "OWNER" ||
+ selectedTeam.value?.myRole === "EDITOR"
+ ) {
+ inviteTeam({ name: selectedTeam.value.name }, selectedTeam.value.id)
+ }
+})
+
defineActionHandler(
"user.login",
() => {
diff --git a/packages/hoppscotch-common/src/components/app/Options.vue b/packages/hoppscotch-common/src/components/app/Options.vue
index 590fdcf97..4834c03fb 100644
--- a/packages/hoppscotch-common/src/components/app/Options.vue
+++ b/packages/hoppscotch-common/src/components/app/Options.vue
@@ -130,13 +130,12 @@
@click="nativeShare()"
/>
-
+
+
diff --git a/packages/hoppscotch-common/src/components/app/spotlight/index.vue b/packages/hoppscotch-common/src/components/app/spotlight/index.vue
index 49feb1630..cbe94438a 100644
--- a/packages/hoppscotch-common/src/components/app/spotlight/index.vue
+++ b/packages/hoppscotch-common/src/components/app/spotlight/index.vue
@@ -98,6 +98,19 @@ import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/use
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
+import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
+import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
+import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
+import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
+import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
+import {
+ EnvironmentsSpotlightSearcherService,
+ SwitchEnvSpotlightSearcherService,
+} from "~/services/spotlight/searchers/environment.searcher"
+import {
+ SwitchWorkspaceSpotlightSearcherService,
+ WorkspaceSpotlightSearcherService,
+} from "~/services/spotlight/searchers/workspace.searcher"
const t = useI18n()
@@ -116,6 +129,15 @@ useService(UserSpotlightSearcherService)
useService(NavigationSpotlightSearcherService)
useService(SettingsSpotlightSearcherService)
useService(CollectionsSpotlightSearcherService)
+useService(MiscellaneousSpotlightSearcherService)
+useService(TabSpotlightSearcherService)
+useService(GeneralSpotlightSearcherService)
+useService(ResponseSpotlightSearcherService)
+useService(RequestSpotlightSearcherService)
+useService(EnvironmentsSpotlightSearcherService)
+useService(SwitchEnvSpotlightSearcherService)
+useService(WorkspaceSpotlightSearcherService)
+useService(SwitchWorkspaceSpotlightSearcherService)
const search = ref("")
@@ -242,3 +264,4 @@ function newUseArrowKeysForNavigation() {
return { selectedEntry }
}
+~/services/spotlight/searchers/workspace.searcher
diff --git a/packages/hoppscotch-common/src/components/environments/index.vue b/packages/hoppscotch-common/src/components/environments/index.vue
index 632681b4e..773d7393f 100644
--- a/packages/hoppscotch-common/src/components/environments/index.vue
+++ b/packages/hoppscotch-common/src/components/environments/index.vue
@@ -198,6 +198,11 @@ const resetSelectedData = () => {
editingEnvironmentIndex.value = null
}
+defineActionHandler("modals.environment.new", () => {
+ action.value = "new"
+ showModalDetails.value = true
+})
+
defineActionHandler(
"modals.my.environment.edit",
({ envName, variableName }) => {
diff --git a/packages/hoppscotch-common/src/components/environments/my/Environment.vue b/packages/hoppscotch-common/src/components/environments/my/Environment.vue
index 25c029c22..aa762b0cf 100644
--- a/packages/hoppscotch-common/src/components/environments/my/Environment.vue
+++ b/packages/hoppscotch-common/src/components/environments/my/Environment.vue
@@ -158,5 +158,7 @@ const duplicateEnvironments = () => {
cloneDeep(getGlobalVariables())
)
} else duplicateEnvironment(props.environmentIndex)
+
+ toast.success(`${t("environment.duplicated")}`)
}
diff --git a/packages/hoppscotch-common/src/components/environments/teams/Environment.vue b/packages/hoppscotch-common/src/components/environments/teams/Environment.vue
index 9b20e960a..0f353415d 100644
--- a/packages/hoppscotch-common/src/components/environments/teams/Environment.vue
+++ b/packages/hoppscotch-common/src/components/environments/teams/Environment.vue
@@ -154,7 +154,7 @@ const duplicateEnvironments = () => {
toast.error(`${getErrorMessage(err)}`)
},
() => {
- toast.success(`${t("team_environment.duplicate")}`)
+ toast.success(`${t("environment.duplicated")}`)
}
)
)()
diff --git a/packages/hoppscotch-common/src/components/http/Request.vue b/packages/hoppscotch-common/src/components/http/Request.vue
index 3832cb41e..39f99e373 100644
--- a/packages/hoppscotch-common/src/components/http/Request.vue
+++ b/packages/hoppscotch-common/src/components/http/Request.vue
@@ -630,6 +630,13 @@ defineActionHandler("request.method.put", () => updateMethod("PUT"))
defineActionHandler("request.method.delete", () => updateMethod("DELETE"))
defineActionHandler("request.method.head", () => updateMethod("HEAD"))
+defineActionHandler("request.import-curl", () => {
+ showCurlImportModal.value = true
+})
+defineActionHandler("request.show-code", () => {
+ showCodegenModal.value = true
+})
+
const isCustomMethod = computed(() => {
return (
tab.value.document.request.method === "CUSTOM" ||
diff --git a/packages/hoppscotch-common/src/components/http/RequestOptions.vue b/packages/hoppscotch-common/src/components/http/RequestOptions.vue
index 2281ab42a..0ed5237e8 100644
--- a/packages/hoppscotch-common/src/components/http/RequestOptions.vue
+++ b/packages/hoppscotch-common/src/components/http/RequestOptions.vue
@@ -1,6 +1,6 @@
@@ -56,12 +56,15 @@ import { useI18n } from "@composables/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { computed, ref } from "vue"
+import { defineActionHandler } from "~/helpers/actions"
export type RequestOptionTabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
+ | "preRequestScript"
+ | "tests"
const t = useI18n()
@@ -73,10 +76,10 @@ const emit = defineEmits<{
const request = useVModel(props, "modelValue", emit)
-const selectedRealtimeTab = ref("params")
+const selectedOptionsTab = ref("params")
const changeTab = (e: RequestOptionTabs) => {
- selectedRealtimeTab.value = e
+ selectedOptionsTab.value = e
}
const newActiveParamsCount$ = computed(() => {
@@ -96,4 +99,8 @@ const newActiveHeadersCount$ = computed(() => {
if (e === 0) return null
return `${e}`
})
+
+defineActionHandler("request.open-tab", ({ tab }) => {
+ selectedOptionsTab.value = tab
+})
diff --git a/packages/hoppscotch-common/src/components/workspace/Selector.vue b/packages/hoppscotch-common/src/components/workspace/Selector.vue
index 5984ce866..9696628a5 100644
--- a/packages/hoppscotch-common/src/components/workspace/Selector.vue
+++ b/packages/hoppscotch-common/src/components/workspace/Selector.vue
@@ -82,6 +82,7 @@ import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconDone from "~icons/lucide/check"
import { useLocalState } from "~/newstore/localstate"
+import { defineActionHandler } from "~/helpers/actions"
const t = useI18n()
const colorMode = useColorMode()
@@ -154,4 +155,14 @@ const displayModalAdd = (shouldDisplay: boolean) => {
showModalAdd.value = shouldDisplay
teamListadapter.fetchList()
}
+
+defineActionHandler("modals.team.new", () => {
+ displayModalAdd(true)
+})
+
+defineActionHandler("workspace.switch.personal", switchToPersonalWorkspace)
+defineActionHandler("workspace.switch", ({ teamId }) => {
+ const team = myTeams.value.find((t) => t.id === teamId)
+ if (team) switchToTeamWorkspace(team)
+})
diff --git a/packages/hoppscotch-common/src/helpers/actions.ts b/packages/hoppscotch-common/src/helpers/actions.ts
index 1639852c2..1caacead6 100644
--- a/packages/hoppscotch-common/src/helpers/actions.ts
+++ b/packages/hoppscotch-common/src/helpers/actions.ts
@@ -6,6 +6,7 @@ import { Ref, onBeforeUnmount, onMounted, watch } from "vue"
import { BehaviorSubject } from "rxjs"
import { HoppRESTDocument } from "./rest/document"
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
+import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
export type HoppAction =
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
@@ -14,6 +15,7 @@ export type HoppAction =
| "request.copy-link" // Copy Request Link
| "request.save" // Save to Collections
| "request.save-as" // Save As
+ | "rest.request.rename" // Rename
| "request.method.next" // Select Next Method
| "request.method.prev" // Select Previous Method
| "request.method.get" // Select GET Method
@@ -21,13 +23,22 @@ export type HoppAction =
| "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
+ | "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.social.toggle" // Shows the social links modal
| "modals.environment.add" // Show add environment modal via context menu
+ | "modals.environment.new" // Add new environment
| "modals.my.environment.edit" // Edit current personal 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
@@ -73,6 +84,12 @@ type HoppActionArgsMap = {
envName: string
variableName?: string
}
+ "modals.team.delete": {
+ teamId: string
+ }
+ "workspace.switch": {
+ teamId: string
+ }
"rest.request.open": {
doc: HoppRESTDocument
}
@@ -85,6 +102,10 @@ type HoppActionArgsMap = {
requestType: "gql"
request: HoppGQLRequest
}
+ "request.open-tab": {
+ tab: RequestOptionTabs
+ }
+
"gql.request.open": {
request: HoppGQLRequest
}
diff --git a/packages/hoppscotch-common/src/pages/index.vue b/packages/hoppscotch-common/src/pages/index.vue
index 201f30ff4..1f1798ebc 100644
--- a/packages/hoppscotch-common/src/pages/index.vue
+++ b/packages/hoppscotch-common/src/pages/index.vue
@@ -458,6 +458,13 @@ defineActionHandler("rest.request.open", ({ doc }) => {
createNewTab(doc)
})
+defineActionHandler("rest.request.rename", () => {
+ // TODO: Fix this hack to open the modal
+ setTimeout(() => {
+ openReqRenameModal()
+ }, 100)
+})
+
const inspectionService = useService(InspectionService)
useService(HeaderInspectorService)
useService(EnvironmentInspectorService)
diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/environment.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/environment.searcher.ts
new file mode 100644
index 000000000..970c06615
--- /dev/null
+++ b/packages/hoppscotch-common/src/services/spotlight/searchers/environment.searcher.ts
@@ -0,0 +1,356 @@
+import {
+ Component,
+ Ref,
+ computed,
+ effectScope,
+ markRaw,
+ reactive,
+ ref,
+ watch,
+} from "vue"
+import { activeActions$, invokeAction } from "~/helpers/actions"
+import { getI18n } from "~/modules/i18n"
+import {
+ SpotlightSearcher,
+ SpotlightSearcherResult,
+ SpotlightSearcherSessionState,
+ SpotlightService,
+} from ".."
+import {
+ SearchResult,
+ StaticSpotlightSearcherService,
+} from "./base/static.searcher"
+
+import IconEdit from "~icons/lucide/edit"
+import IconTrash2 from "~icons/lucide/trash-2"
+import IconCopy from "~icons/lucide/copy"
+import IconLayers from "~icons/lucide/layers"
+
+import { useStreamStatic } from "~/composables/stream"
+import {
+ createEnvironment,
+ currentEnvironment$,
+ deleteEnvironment,
+ duplicateEnvironment,
+ environmentsStore,
+ getGlobalVariables,
+ selectedEnvironmentIndex$,
+ setSelectedEnvironmentIndex,
+} from "~/newstore/environments"
+import { pipe } from "fp-ts/function"
+import * as TE from "fp-ts/TaskEither"
+import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
+import { GQLError } from "~/helpers/backend/GQLClient"
+import { cloneDeep } from "lodash-es"
+import { Service } from "dioc"
+import MiniSearch from "minisearch"
+import { map } from "rxjs"
+
+type Doc = {
+ text: string
+ alternates: string[]
+ icon: object | Component
+ excludeFromSearch?: boolean
+}
+
+/**
+ *
+ * This searcher is responsible for providing environments related actions on the spotlight results.
+ *
+ * NOTE: Initializing this service registers it as a searcher with the Spotlight Service.
+ */
+export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearcherService {
+ public static readonly ID = "ENVIRONMENTS_SPOTLIGHT_SEARCHER_SERVICE"
+
+ private t = getI18n()
+
+ public readonly searcherID = "environments"
+ public searcherSectionTitle = this.t("spotlight.environments.title")
+
+ private readonly spotlight = this.bind(SpotlightService)
+
+ private selectedEnvIndex = useStreamStatic(
+ selectedEnvironmentIndex$,
+ null,
+ () => {
+ /* noop */
+ }
+ )[0]
+
+ private selectedEnv = useStreamStatic(currentEnvironment$, null, () => {
+ /* noop */
+ })[0]
+
+ private hasSelectedEnv = computed(
+ () => this.selectedEnvIndex.value?.type !== "NO_ENV_SELECTED"
+ )
+
+ private documents: Record = reactive({
+ new_environment: {
+ text: this.t("spotlight.environments.new"),
+ alternates: ["new", "environment"],
+ icon: markRaw(IconLayers),
+ },
+ new_environment_variable: {
+ text: this.t("spotlight.environments.new_variable"),
+ alternates: ["new", "environment", "variable"],
+ icon: markRaw(IconLayers),
+ },
+ edit_selected_env: {
+ text: this.t("spotlight.environments.edit"),
+ alternates: ["edit", "environment"],
+ icon: markRaw(IconEdit),
+ excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
+ },
+ delete_selected_env: {
+ text: this.t("spotlight.environments.delete"),
+ alternates: ["delete", "environment"],
+ icon: markRaw(IconTrash2),
+ excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
+ },
+ duplicate_selected_env: {
+ text: this.t("spotlight.environments.duplicate"),
+ alternates: ["duplicate", "environment"],
+ icon: markRaw(IconCopy),
+ excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
+ },
+ edit_global_env: {
+ text: this.t("spotlight.environments.edit_global"),
+ alternates: ["edit", "global", "environment"],
+ icon: markRaw(IconEdit),
+ },
+ duplicate_global_env: {
+ text: this.t("spotlight.environments.duplicate_global"),
+ alternates: ["duplicate", "global", "environment"],
+ icon: markRaw(IconCopy),
+ },
+ })
+
+ constructor() {
+ super({
+ searchFields: ["text", "alternates"],
+ fieldWeights: {
+ text: 2,
+ alternates: 1,
+ },
+ })
+
+ this.setDocuments(this.documents)
+ this.spotlight.registerSearcher(this)
+ }
+
+ protected getSearcherResultForSearchResult(
+ result: SearchResult
+ ): SpotlightSearcherResult {
+ return {
+ id: result.id,
+ icon: result.doc.icon,
+ text: { type: "text", text: result.doc.text },
+ score: result.score,
+ }
+ }
+
+ private getSelectedText() {
+ const selection = window.getSelection()
+ return selection?.toString() ?? ""
+ }
+
+ duplicateGlobalEnv() {
+ createEnvironment(
+ `Global - ${this.t("action.duplicate")}`,
+ cloneDeep(getGlobalVariables())
+ )
+ // this.toast.success(`${t("environment.duplicated")}`)
+ }
+
+ duplicateSelectedEnv() {
+ if (this.selectedEnvIndex.value?.type === "NO_ENV_SELECTED") return
+
+ if (this.selectedEnvIndex.value?.type === "MY_ENV") {
+ duplicateEnvironment(this.selectedEnvIndex.value.index)
+ // this.toast.success(`${t("environment.duplicated")}`)
+ }
+
+ if (this.selectedEnvIndex.value?.type === "TEAM_ENV") {
+ pipe(
+ deleteTeamEnvironment(this.selectedEnvIndex.value.teamEnvID),
+ TE.match(
+ (err: GQLError) => {
+ console.error(err)
+ },
+ () => {
+ // this.toast.success(`${this.t("environment.duplicated")}`)
+ }
+ )
+ )()
+ }
+ }
+
+ removeSelectedEnvironment = () => {
+ if (this.selectedEnvIndex.value?.type === "NO_ENV_SELECTED") return
+
+ if (this.selectedEnvIndex.value?.type === "MY_ENV") {
+ deleteEnvironment(this.selectedEnvIndex.value.index)
+ // this.toast.success(`${t("state.deleted")}`)
+ }
+
+ if (this.selectedEnvIndex.value?.type === "TEAM_ENV") {
+ pipe(
+ deleteTeamEnvironment(this.selectedEnvIndex.value.teamEnvID),
+ TE.match(
+ (err: GQLError) => {
+ console.error(err)
+ },
+ () => {
+ // this.toast.success(`${this.t("team_environment.deleted")}`)
+ }
+ )
+ )()
+ }
+ }
+
+ public onDocSelected(id: string): void {
+ switch (id) {
+ case "new_environment":
+ invokeAction(`modals.environment.new`)
+ break
+ case "new_environment_variable":
+ invokeAction(`modals.environment.add`, {
+ envName: "",
+ variableName: this.getSelectedText(),
+ })
+ break
+ case "edit_selected_env":
+ if (this.selectedEnv.value)
+ invokeAction(`modals.my.environment.edit`, {
+ envName: this.selectedEnv.value.name,
+ })
+ break
+ case "delete_selected_env":
+ this.removeSelectedEnvironment()
+ break
+ case "duplicate_selected_env":
+ this.duplicateSelectedEnv()
+ break
+ case "edit_global_env":
+ invokeAction(`modals.my.environment.edit`, {
+ envName: "Global",
+ })
+ break
+ case "duplicate_global_env":
+ this.duplicateGlobalEnv()
+ break
+ }
+ }
+}
+
+/**
+ * This searcher is responsible for searching through the environment.
+ * And switching between them.
+ */
+export class SwitchEnvSpotlightSearcherService
+ extends Service
+ implements SpotlightSearcher
+{
+ public static readonly ID = "SWITCH_ENV_SPOTLIGHT_SEARCHER_SERVICE"
+
+ private t = getI18n()
+
+ public searcherID = "switch_env"
+ public searcherSectionTitle = this.t("tab.environments")
+
+ private readonly spotlight = this.bind(SpotlightService)
+
+ constructor() {
+ super()
+
+ this.spotlight.registerSearcher(this)
+ }
+
+ private environmentSearchable = useStreamStatic(
+ activeActions$.pipe(
+ map((actions) => actions.includes("modals.environment.add"))
+ ),
+ activeActions$.value.includes("modals.environment.add"),
+ () => {
+ /* noop */
+ }
+ )[0]
+
+ createSearchSession(
+ query: Readonly[>
+ ): [Ref, () => void] {
+ const loading = ref(false)
+ const results = ref([])
+
+ const minisearch = new MiniSearch({
+ fields: ["name"],
+ storeFields: ["name"],
+ })
+
+ if (this.environmentSearchable.value) {
+ minisearch.addAll(
+ environmentsStore.value.environments.map((entry, index) => {
+ return {
+ id: `environment-${index}`,
+ name: entry.name,
+ }
+ })
+ )
+ }
+
+ const scopeHandle = effectScope()
+
+ scopeHandle.run(() => {
+ watch(
+ [query],
+ ([query]) => {
+ results.value = minisearch
+ .search(query, {
+ prefix: true,
+ fuzzy: true,
+ boost: {
+ reltime: 2,
+ },
+ weights: {
+ fuzzy: 0.2,
+ prefix: 0.8,
+ },
+ })
+ .map((x) => {
+ return {
+ id: x.id,
+ icon: markRaw(IconLayers),
+ score: x.score,
+ text: {
+ type: "text",
+ text: [this.t("environment.set"), x.name],
+ },
+ }
+ })
+ },
+ { immediate: true }
+ )
+ })
+
+ const onSessionEnd = () => {
+ scopeHandle.stop()
+ minisearch.removeAll()
+ }
+
+ const resultObj = computed(() => ({
+ loading: loading.value,
+ results: results.value,
+ }))
+
+ return [resultObj, onSessionEnd]
+ }
+
+ onResultSelect(result: SpotlightSearcherResult): void {
+ const selectedEnvIndex = Number(result.id.split("-")[1])
+ setSelectedEnvironmentIndex({
+ type: "MY_ENV",
+ index: selectedEnvIndex,
+ })
+ }
+}
diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/general.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/general.searcher.ts
new file mode 100644
index 000000000..b707d1829
--- /dev/null
+++ b/packages/hoppscotch-common/src/services/spotlight/searchers/general.searcher.ts
@@ -0,0 +1,114 @@
+import { Component, markRaw, reactive } from "vue"
+import { invokeAction } from "~/helpers/actions"
+import { getI18n } from "~/modules/i18n"
+import { SpotlightSearcherResult, SpotlightService } from ".."
+import {
+ SearchResult,
+ StaticSpotlightSearcherService,
+} from "./base/static.searcher"
+
+import IconBook from "~icons/lucide/book"
+import IconGithub from "~icons/lucide/github"
+import IconLifeBuoy from "~icons/lucide/life-buoy"
+import IconMessageCircle from "~icons/lucide/message-circle"
+import IconZap from "~icons/lucide/zap"
+
+type Doc = {
+ text: string
+ alternates: string[]
+ icon: object | Component
+}
+
+/**
+ *
+ * This searcher is responsible for providing general related actions on the spotlight results.
+ *
+ * NOTE: Initializing this service registers it as a searcher with the Spotlight Service.
+ */
+export class GeneralSpotlightSearcherService extends StaticSpotlightSearcherService {
+ public static readonly ID = "GENERAL_SPOTLIGHT_SEARCHER_SERVICE"
+
+ private t = getI18n()
+
+ public readonly searcherID = "general"
+ public searcherSectionTitle = this.t("spotlight.general.title")
+
+ private readonly spotlight = this.bind(SpotlightService)
+
+ private documents: Record = reactive({
+ open_help: {
+ text: this.t("spotlight.general.help_menu"),
+ alternates: ["help", "hoppscotch"],
+ icon: markRaw(IconLifeBuoy),
+ },
+ chat_with_support: {
+ text: this.t("spotlight.general.chat"),
+ alternates: ["chat", "support", "hoppscotch"],
+ icon: markRaw(IconMessageCircle),
+ },
+ open_docs: {
+ text: this.t("spotlight.general.open_docs"),
+ alternates: ["docs", "documentation", "hoppscotch"],
+ icon: markRaw(IconBook),
+ },
+ open_keybindings: {
+ text: this.t("spotlight.general.open_keybindings"),
+ alternates: ["key", "shortcuts", "binding"],
+ icon: markRaw(IconZap),
+ },
+ social_links: {
+ text: this.t("spotlight.general.social"),
+ alternates: ["social", "github", "binding"],
+ icon: markRaw(IconGithub),
+ },
+ })
+
+ constructor() {
+ super({
+ searchFields: ["text", "alternates"],
+ fieldWeights: {
+ text: 2,
+ alternates: 1,
+ },
+ })
+
+ this.setDocuments(this.documents)
+ this.spotlight.registerSearcher(this)
+ }
+
+ protected getSearcherResultForSearchResult(
+ result: SearchResult
+ ): SpotlightSearcherResult {
+ return {
+ id: result.id,
+ icon: result.doc.icon,
+ text: { type: "text", text: result.doc.text },
+ score: result.score,
+ }
+ }
+
+ private openDocs() {
+ const url = "https://docs.hoppscotch.io"
+ window.open(url, "_blank")
+ }
+
+ public onDocSelected(id: string): void {
+ switch (id) {
+ case "open_help":
+ invokeAction("modals.support.toggle")
+ break
+ case "chat_with_support":
+ invokeAction("flyouts.chat.open")
+ break
+ case "open_docs":
+ this.openDocs()
+ break
+ case "open_keybindings":
+ invokeAction("flyouts.keybinds.toggle")
+ break
+ case "social_links":
+ invokeAction("modals.social.toggle")
+ break
+ }
+ }
+}
diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/miscellaneous.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/miscellaneous.searcher.ts
new file mode 100644
index 000000000..0d0fdb0da
--- /dev/null
+++ b/packages/hoppscotch-common/src/services/spotlight/searchers/miscellaneous.searcher.ts
@@ -0,0 +1,69 @@
+import { Component, markRaw, reactive } from "vue"
+import { invokeAction } from "~/helpers/actions"
+import { getI18n } from "~/modules/i18n"
+import { SpotlightSearcherResult, SpotlightService } from ".."
+import {
+ SearchResult,
+ StaticSpotlightSearcherService,
+} from "./base/static.searcher"
+
+import IconShare from "~icons/lucide/share"
+
+type Doc = {
+ text: string
+ alternates: string[]
+ icon: object | Component
+}
+
+/**
+ *
+ * This searcher is responsible for providing miscellaneous related actions on the spotlight results.
+ *
+ * NOTE: Initializing this service registers it as a searcher with the Spotlight Service.
+ */
+export class MiscellaneousSpotlightSearcherService extends StaticSpotlightSearcherService {
+ public static readonly ID = "MISCELLANEOUS_SPOTLIGHT_SEARCHER_SERVICE"
+
+ private t = getI18n()
+
+ public readonly searcherID = "miscellaneous"
+ public searcherSectionTitle = this.t("spotlight.miscellaneous.title")
+
+ private readonly spotlight = this.bind(SpotlightService)
+
+ private documents: Record = reactive({
+ invite_hoppscotch: {
+ text: this.t("spotlight.miscellaneous.invite"),
+ alternates: ["invite", "share", "hoppscotch"],
+ icon: markRaw(IconShare),
+ },
+ })
+
+ constructor() {
+ super({
+ searchFields: ["text", "alternates"],
+ fieldWeights: {
+ text: 2,
+ alternates: 1,
+ },
+ })
+
+ this.setDocuments(this.documents)
+ this.spotlight.registerSearcher(this)
+ }
+
+ protected getSearcherResultForSearchResult(
+ result: SearchResult
+ ): SpotlightSearcherResult {
+ return {
+ id: result.id,
+ icon: result.doc.icon,
+ text: { type: "text", text: result.doc.text },
+ score: result.score,
+ }
+ }
+
+ public onDocSelected(id: string): void {
+ if (id === "invite_hoppscotch") invokeAction(`modals.share.toggle`)
+ }
+}
diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/request.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/request.searcher.ts
new file mode 100644
index 000000000..7866a83f2
--- /dev/null
+++ b/packages/hoppscotch-common/src/services/spotlight/searchers/request.searcher.ts
@@ -0,0 +1,259 @@
+import { Component, markRaw, reactive } from "vue"
+import { invokeAction } from "~/helpers/actions"
+import { getI18n } from "~/modules/i18n"
+import { SpotlightSearcherResult, SpotlightService } from ".."
+import {
+ SearchResult,
+ StaticSpotlightSearcherService,
+} from "./base/static.searcher"
+
+import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
+import { currentActiveTab } from "~/helpers/rest/tab"
+import IconWindow from "~icons/lucide/app-window"
+import IconCheck from "~icons/lucide/check"
+import IconChevronLeft from "~icons/lucide/chevron-left"
+import IconChevronRight from "~icons/lucide/chevron-right"
+import IconCode2 from "~icons/lucide/code-2"
+import IconCopy from "~icons/lucide/copy"
+import IconFileCode from "~icons/lucide/file-code"
+import IconRename from "~icons/lucide/file-edit"
+import IconPlay from "~icons/lucide/play"
+import IconRotateCCW from "~icons/lucide/rotate-ccw"
+import IconSave from "~icons/lucide/save"
+
+type Doc = {
+ text: string | string[]
+ alternates: string[]
+ icon: object | Component
+}
+
+/**
+ *
+ * This searcher is responsible for providing request related actions on the spotlight results.
+ *
+ * NOTE: Initializing this service registers it as a searcher with the Spotlight Service.
+ */
+export class RequestSpotlightSearcherService extends StaticSpotlightSearcherService {
+ public static readonly ID = "REQUEST_SPOTLIGHT_SEARCHER_SERVICE"
+
+ private t = getI18n()
+
+ public readonly searcherID = "request"
+ public searcherSectionTitle = this.t("shortcut.request.title")
+
+ private readonly spotlight = this.bind(SpotlightService)
+
+ private documents: Record = reactive({
+ send_request: {
+ text: this.t("shortcut.request.send_request"),
+ alternates: ["request", "send"],
+ icon: markRaw(IconPlay),
+ },
+ save_to_collections: {
+ text: [
+ this.t("request.save_as"),
+ this.t("shortcut.request.save_to_collections"),
+ ],
+ alternates: ["save", "collections"],
+ icon: markRaw(IconSave),
+ },
+ save_request: {
+ text: this.t("shortcut.request.save_request"),
+ alternates: ["save", "request"],
+ icon: markRaw(IconSave),
+ },
+ rename_request: {
+ text: this.t("shortcut.request.rename"),
+ alternates: ["rename", "request"],
+ icon: markRaw(IconRename),
+ },
+ copy_request_link: {
+ text: this.t("shortcut.request.copy_request_link"),
+ alternates: ["copy", "link"],
+ icon: markRaw(IconCopy),
+ },
+ reset_request: {
+ text: this.t("shortcut.request.reset_request"),
+ alternates: ["reset", "request"],
+ icon: markRaw(IconRotateCCW),
+ },
+ import_curl: {
+ text: this.t("shortcut.request.import_curl"),
+ alternates: ["import", "curl"],
+ icon: markRaw(IconFileCode),
+ },
+ show_code: {
+ text: this.t("shortcut.request.show_code"),
+ alternates: ["show", "code"],
+ icon: markRaw(IconCode2),
+ },
+ // Change request method
+ next_method: {
+ text: this.t("shortcut.request.next_method"),
+ alternates: ["next", "method"],
+ icon: markRaw(IconChevronRight),
+ },
+ previous_method: {
+ text: this.t("shortcut.request.previous_method"),
+ alternates: ["previous", "method"],
+ icon: markRaw(IconChevronLeft),
+ },
+ get_method: {
+ text: this.t("shortcut.request.get_method"),
+ alternates: ["get", "method"],
+ icon: markRaw(IconCheck),
+ },
+ head_method: {
+ text: this.t("shortcut.request.head_method"),
+ alternates: ["head", "method"],
+ icon: markRaw(IconCheck),
+ },
+ post_method: {
+ text: this.t("shortcut.request.post_method"),
+ alternates: ["post", "method"],
+ icon: markRaw(IconCheck),
+ },
+ put_method: {
+ text: this.t("shortcut.request.put_method"),
+ alternates: ["put", "method"],
+ icon: markRaw(IconCheck),
+ },
+ delete_method: {
+ text: this.t("shortcut.request.delete_method"),
+ alternates: ["delete", "method"],
+ icon: markRaw(IconCheck),
+ },
+ // Change sub tabs
+ tab_parameters: {
+ text: this.t("spotlight.request.tab_parameters"),
+ alternates: ["parameters", "tab"],
+ icon: markRaw(IconWindow),
+ },
+ tab_body: {
+ text: this.t("spotlight.request.tab_body"),
+ alternates: ["body", "tab"],
+ icon: markRaw(IconWindow),
+ },
+ tab_headers: {
+ text: this.t("spotlight.request.tab_headers"),
+ alternates: ["headers", "tab"],
+ icon: markRaw(IconWindow),
+ },
+ tab_authorization: {
+ text: this.t("spotlight.request.tab_authorization"),
+ alternates: ["authorization", "tab"],
+ icon: markRaw(IconWindow),
+ },
+ tab_pre_request_script: {
+ text: this.t("spotlight.request.tab_pre_request_script"),
+ alternates: ["pre-request", "script", "tab"],
+ icon: markRaw(IconWindow),
+ },
+ tab_tests: {
+ text: this.t("spotlight.request.tab_tests"),
+ alternates: ["tests", "tab"],
+ icon: markRaw(IconWindow),
+ },
+ })
+
+ constructor() {
+ super({
+ searchFields: ["text", "alternates"],
+ fieldWeights: {
+ text: 2,
+ alternates: 1,
+ },
+ })
+
+ this.setDocuments(this.documents)
+ this.spotlight.registerSearcher(this)
+ }
+
+ protected getSearcherResultForSearchResult(
+ result: SearchResult
+ ): SpotlightSearcherResult {
+ return {
+ id: result.id,
+ icon: result.doc.icon,
+ text: { type: "text", text: result.doc.text },
+ score: result.score,
+ }
+ }
+
+ private openRequestTab(tab: RequestOptionTabs): void {
+ invokeAction("request.open-tab", {
+ tab,
+ })
+ }
+
+ public onDocSelected(id: string): void {
+ switch (id) {
+ case "send_request":
+ invokeAction("request.send-cancel")
+ break
+ case "save_to_collections":
+ invokeAction("request.save-as", {
+ requestType: "rest",
+ request: currentActiveTab.value?.document.request,
+ })
+ break
+ case "save_request":
+ invokeAction("request.save")
+ break
+ case "rename_request":
+ invokeAction("rest.request.rename")
+ break
+ case "copy_request_link":
+ invokeAction("request.copy-link")
+ break
+ case "reset_request":
+ invokeAction("request.reset")
+ break
+ case "next_method":
+ invokeAction("request.method.next")
+ break
+ case "previous_method":
+ invokeAction("request.method.prev")
+ break
+ case "get_method":
+ invokeAction("request.method.get")
+ break
+ case "head_method":
+ invokeAction("request.method.head")
+ break
+ case "post_method":
+ invokeAction("request.method.post")
+ break
+ case "put_method":
+ invokeAction("request.method.put")
+ break
+ case "delete_method":
+ invokeAction("request.method.delete")
+ break
+ case "import_curl":
+ invokeAction("request.import-curl")
+ break
+ case "show_code":
+ invokeAction("request.show-code")
+ break
+ case "tab_parameters":
+ this.openRequestTab("params")
+ break
+ case "tab_body":
+ this.openRequestTab("bodyParams")
+ break
+ case "tab_headers":
+ this.openRequestTab("headers")
+ break
+ case "tab_authorization":
+ this.openRequestTab("authorization")
+ break
+ case "tab_pre_request_script":
+ this.openRequestTab("preRequestScript")
+ break
+ case "tab_tests":
+ this.openRequestTab("tests")
+ break
+ }
+ }
+}
diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/response.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/response.searcher.ts
new file mode 100644
index 000000000..de8503374
--- /dev/null
+++ b/packages/hoppscotch-common/src/services/spotlight/searchers/response.searcher.ts
@@ -0,0 +1,101 @@
+import { Component, computed, markRaw, reactive } from "vue"
+import { activeActions$, invokeAction } from "~/helpers/actions"
+import { getI18n } from "~/modules/i18n"
+import { SpotlightSearcherResult, SpotlightService } from ".."
+import {
+ SearchResult,
+ StaticSpotlightSearcherService,
+} from "./base/static.searcher"
+
+import IconDownload from "~icons/lucide/download"
+import IconCopy from "~icons/lucide/copy"
+import { map } from "rxjs"
+import { useStreamStatic } from "~/composables/stream"
+
+type Doc = {
+ text: string
+ alternates: string[]
+ icon: object | Component
+ excludeFromSearch?: boolean
+}
+
+/**
+ *
+ * This searcher is responsible for providing response related actions on the spotlight results.
+ *
+ * NOTE: Initializing this service registers it as a searcher with the Spotlight Service.
+ */
+export class ResponseSpotlightSearcherService extends StaticSpotlightSearcherService {
+ public static readonly ID = "RESPONSE_SPOTLIGHT_SEARCHER_SERVICE"
+
+ private t = getI18n()
+
+ public readonly searcherID = "response"
+ public searcherSectionTitle = this.t("spotlight.response.title")
+
+ private readonly spotlight = this.bind(SpotlightService)
+
+ private copyResponseActionEnabled = useStreamStatic(
+ activeActions$.pipe(map((actions) => actions.includes("response.copy"))),
+ activeActions$.value.includes("response.copy"),
+ () => {
+ /* noop */
+ }
+ )[0]
+
+ private downloadResponseActionEnabled = useStreamStatic(
+ activeActions$.pipe(
+ map((actions) => actions.includes("response.file.download"))
+ ),
+ activeActions$.value.includes("response.file.download"),
+ () => {
+ /* noop */
+ }
+ )[0]
+
+ private documents: Record = reactive({
+ copy_response: {
+ text: this.t("spotlight.response.copy"),
+ alternates: ["copy", "response"],
+ icon: markRaw(IconCopy),
+ excludeFromSearch: computed(() => !this.copyResponseActionEnabled.value),
+ },
+ download_response: {
+ text: this.t("spotlight.response.download"),
+ alternates: ["download", "response"],
+ icon: markRaw(IconDownload),
+ excludeFromSearch: computed(
+ () => !this.downloadResponseActionEnabled.value
+ ),
+ },
+ })
+
+ constructor() {
+ super({
+ searchFields: ["text", "alternates"],
+ fieldWeights: {
+ text: 2,
+ alternates: 1,
+ },
+ })
+
+ this.setDocuments(this.documents)
+ this.spotlight.registerSearcher(this)
+ }
+
+ protected getSearcherResultForSearchResult(
+ result: SearchResult
+ ): SpotlightSearcherResult {
+ return {
+ id: result.id,
+ icon: result.doc.icon,
+ text: { type: "text", text: result.doc.text },
+ score: result.score,
+ }
+ }
+
+ public onDocSelected(id: string): void {
+ if (id === "copy_response") invokeAction(`response.copy`)
+ if (id === "download_response") invokeAction(`response.file.download`)
+ }
+}
diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/settings.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/settings.searcher.ts
index 41fcf35b2..ccf4a92f8 100644
--- a/packages/hoppscotch-common/src/services/spotlight/searchers/settings.searcher.ts
+++ b/packages/hoppscotch-common/src/services/spotlight/searchers/settings.searcher.ts
@@ -76,7 +76,10 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
icon: markRaw(IconMoon),
},
font_size_sm: {
- text: this.t("spotlight.font.size_sm"),
+ text: [
+ this.t("settings.font_size"),
+ this.t("spotlight.settings.font.size_sm"),
+ ],
onClick: () => {
console.log("clicked")
},
@@ -90,7 +93,10 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
icon: markRaw(IconType),
},
font_size_md: {
- text: this.t("spotlight.font.size_md"),
+ text: [
+ this.t("settings.font_size"),
+ this.t("spotlight.settings.font.size_md"),
+ ],
excludeFromSearch: computed(() => this.activeFontSize.value === "medium"),
alternates: [
"font size",
@@ -101,7 +107,10 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
icon: markRaw(IconType),
},
font_size_lg: {
- text: this.t("spotlight.font.size_lg"),
+ text: [
+ this.t("settings.font_size"),
+ this.t("spotlight.settings.font.size_lg"),
+ ],
excludeFromSearch: computed(() => this.activeFontSize.value === "large"),
alternates: [
"font size",
diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/tab.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/tab.searcher.ts
new file mode 100644
index 000000000..e0653d943
--- /dev/null
+++ b/packages/hoppscotch-common/src/services/spotlight/searchers/tab.searcher.ts
@@ -0,0 +1,91 @@
+import { Component, markRaw, reactive } from "vue"
+import { getI18n } from "~/modules/i18n"
+import { SpotlightSearcherResult, SpotlightService } from ".."
+import {
+ SearchResult,
+ StaticSpotlightSearcherService,
+} from "./base/static.searcher"
+
+import {
+ closeOtherTabs,
+ closeTab,
+ createNewTab,
+ currentTabID,
+} from "~/helpers/rest/tab"
+import IconWindow from "~icons/lucide/app-window"
+import { getDefaultRESTRequest } from "~/helpers/rest/default"
+
+type Doc = {
+ text: string
+ alternates: string[]
+ icon: object | Component
+}
+
+/**
+ *
+ * This searcher is responsible for providing REST Tab related actions on the spotlight results.
+ *
+ * NOTE: Initializing this service registers it as a searcher with the Spotlight Service.
+ */
+export class TabSpotlightSearcherService extends StaticSpotlightSearcherService {
+ public static readonly ID = "TAB_SPOTLIGHT_SEARCHER_SERVICE"
+
+ private t = getI18n()
+
+ public readonly searcherID = "tab"
+ public searcherSectionTitle = this.t("spotlight.tab.title")
+
+ private readonly spotlight = this.bind(SpotlightService)
+
+ private documents: Record = reactive({
+ close_current_tab: {
+ text: this.t("spotlight.tab.close_current"),
+ alternates: ["tab", "close", "close tab"],
+ icon: markRaw(IconWindow),
+ },
+ close_others_tab: {
+ text: this.t("spotlight.tab.close_others"),
+ alternates: ["tab", "close", "close all"],
+ icon: markRaw(IconWindow),
+ },
+ open_new_tab: {
+ text: this.t("spotlight.tab.new_tab"),
+ alternates: ["tab", "new", "open tab"],
+ icon: markRaw(IconWindow),
+ },
+ })
+
+ constructor() {
+ super({
+ searchFields: ["text", "alternates"],
+ fieldWeights: {
+ text: 2,
+ alternates: 1,
+ },
+ })
+
+ this.setDocuments(this.documents)
+ this.spotlight.registerSearcher(this)
+ }
+
+ protected getSearcherResultForSearchResult(
+ result: SearchResult
+ ): SpotlightSearcherResult {
+ return {
+ id: result.id,
+ icon: result.doc.icon,
+ text: { type: "text", text: result.doc.text },
+ score: result.score,
+ }
+ }
+
+ public onDocSelected(id: string): void {
+ if (id === "close_current_tab") closeTab(currentTabID.value)
+ if (id === "close_others_tab") closeOtherTabs(currentTabID.value)
+ if (id === "open_new_tab")
+ createNewTab({
+ request: getDefaultRESTRequest(),
+ isDirty: false,
+ })
+ }
+}
diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/workspace.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/workspace.searcher.ts
new file mode 100644
index 000000000..8bba44e82
--- /dev/null
+++ b/packages/hoppscotch-common/src/services/spotlight/searchers/workspace.searcher.ts
@@ -0,0 +1,266 @@
+import {
+ Component,
+ Ref,
+ computed,
+ effectScope,
+ markRaw,
+ reactive,
+ ref,
+ watch,
+} from "vue"
+import { invokeAction } from "~/helpers/actions"
+import { getI18n } from "~/modules/i18n"
+import {
+ SpotlightSearcher,
+ SpotlightSearcherResult,
+ SpotlightSearcherSessionState,
+ SpotlightService,
+} from ".."
+import {
+ SearchResult,
+ StaticSpotlightSearcherService,
+} from "./base/static.searcher"
+
+import { Service } from "dioc"
+import * as E from "fp-ts/Either"
+import MiniSearch from "minisearch"
+import { useStreamStatic } from "~/composables/stream"
+import { runGQLQuery } from "~/helpers/backend/GQLClient"
+import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql"
+import { workspaceStatus$ } from "~/newstore/workspace"
+import { platform } from "~/platform"
+import IconEdit from "~icons/lucide/edit"
+import IconTrash2 from "~icons/lucide/trash-2"
+import IconUser from "~icons/lucide/user"
+import IconUserPlus from "~icons/lucide/user-plus"
+import IconUsers from "~icons/lucide/users"
+
+type Doc = {
+ text: string
+ alternates: string[]
+ icon: object | Component
+ excludeFromSearch?: boolean
+}
+
+/**
+ *
+ * This searcher is responsible for providing team related actions on the spotlight results.
+ *
+ * NOTE: Initializing this service registers it as a searcher with the Spotlight Service.
+ */
+export class WorkspaceSpotlightSearcherService extends StaticSpotlightSearcherService {
+ public static readonly ID = "WORKSPACE_SPOTLIGHT_SEARCHER_SERVICE"
+
+ private t = getI18n()
+
+ public readonly searcherID = "workspace"
+ public searcherSectionTitle = this.t("spotlight.workspace.title")
+
+ private readonly spotlight = this.bind(SpotlightService)
+
+ private workspace = useStreamStatic(
+ workspaceStatus$,
+ { type: "personal" },
+ () => {
+ /* noop */
+ }
+ )[0]
+
+ private isTeamSelected = computed(
+ () =>
+ this.workspace.value.type === "team" &&
+ this.workspace.value.teamID !== undefined
+ )
+
+ private documents: Record = reactive({
+ new_team: {
+ text: this.t("spotlight.workspace.new"),
+ alternates: ["new", "team", "workspace"],
+ icon: markRaw(IconUsers),
+ },
+ edit_team: {
+ text: this.t("spotlight.workspace.edit"),
+ alternates: ["edit", "team", "workspace"],
+ icon: markRaw(IconEdit),
+ excludeFromSearch: computed(() => !this.isTeamSelected.value),
+ },
+ invite_members: {
+ text: this.t("spotlight.workspace.invite"),
+ alternates: ["invite", "members", "workspace"],
+ icon: markRaw(IconUserPlus),
+ excludeFromSearch: computed(() => !this.isTeamSelected.value),
+ },
+ delete_team: {
+ text: this.t("spotlight.workspace.delete"),
+ alternates: ["delete", "team", "workspace"],
+ icon: markRaw(IconTrash2),
+ excludeFromSearch: computed(() => !this.isTeamSelected.value),
+ },
+ switch_to_personal: {
+ text: this.t("spotlight.workspace.switch_to_personal"),
+ alternates: ["switch", "team", "workspace", "personal"],
+ icon: markRaw(IconUser),
+ excludeFromSearch: computed(() => !this.isTeamSelected.value),
+ },
+ })
+
+ constructor() {
+ super({
+ searchFields: ["text", "alternates"],
+ fieldWeights: {
+ text: 2,
+ alternates: 1,
+ },
+ })
+
+ this.setDocuments(this.documents)
+ this.spotlight.registerSearcher(this)
+ }
+
+ protected getSearcherResultForSearchResult(
+ result: SearchResult
+ ): SpotlightSearcherResult {
+ return {
+ id: result.id,
+ icon: result.doc.icon,
+ text: { type: "text", text: result.doc.text },
+ score: result.score,
+ }
+ }
+
+ private deleteTeam(): void {
+ if (this.workspace.value.type === "team")
+ invokeAction(`modals.team.delete`, {
+ teamId: this.workspace.value.teamID,
+ })
+ }
+
+ public onDocSelected(id: string): void {
+ if (id === "new_team") invokeAction(`modals.team.new`)
+ else if (id === "edit_team") invokeAction(`modals.team.edit`)
+ else if (id === "invite_members") invokeAction(`modals.team.invite`)
+ else if (id === "delete_team") this.deleteTeam()
+ else if (id === "switch_to_personal")
+ invokeAction(`workspace.switch.personal`)
+ }
+}
+
+/**
+ * This searcher is responsible for searching through the environment.
+ * And switching between them.
+ */
+export class SwitchWorkspaceSpotlightSearcherService
+ extends Service
+ implements SpotlightSearcher
+{
+ public static readonly ID = "SWITCH_WORKSPACE_SPOTLIGHT_SEARCHER_SERVICE"
+
+ private t = getI18n()
+
+ public searcherID = "switch_workspace"
+ public searcherSectionTitle = this.t("workspace.title")
+
+ private readonly spotlight = this.bind(SpotlightService)
+
+ constructor() {
+ super()
+
+ this.spotlight.registerSearcher(this)
+ }
+
+ private fetchMyTeams(): Promise {
+ return new Promise(async (resolve) => {
+ const currentUser = platform.auth.getCurrentUser()
+ if (!currentUser) return resolve([])
+
+ const results: GetMyTeamsQuery["myTeams"] = []
+
+ const result = await runGQLQuery({
+ query: GetMyTeamsDocument,
+ variables: {
+ cursor:
+ results.length > 0 ? results[results.length - 1].id : undefined,
+ },
+ })
+
+ if (E.isRight(result)) results.push(...result.right.myTeams)
+ resolve(results)
+ })
+ }
+
+ createSearchSession(
+ query: Readonly][>
+ ): [Ref, () => void] {
+ const loading = ref(false)
+ const results = ref([])
+
+ const minisearch = new MiniSearch({
+ fields: ["name", "alternates"],
+ storeFields: ["name"],
+ })
+
+ this.fetchMyTeams().then((teams) => {
+ minisearch.addAll(
+ teams.map((entry) => {
+ return {
+ id: `workspace-${entry.id}`,
+ name: entry.name,
+ alternates: ["team", "workspace", "change", "switch"],
+ }
+ })
+ )
+ })
+
+ const scopeHandle = effectScope()
+
+ scopeHandle.run(() => {
+ watch(
+ [query],
+ ([query]) => {
+ results.value = minisearch
+ .search(query, {
+ prefix: true,
+ fuzzy: true,
+ boost: {
+ reltime: 2,
+ },
+ weights: {
+ fuzzy: 0.2,
+ prefix: 0.8,
+ },
+ })
+ .map((x) => {
+ return {
+ id: x.id,
+ icon: markRaw(IconUsers),
+ score: x.score,
+ text: {
+ type: "text",
+ text: [this.t("workspace.change"), x.name],
+ },
+ }
+ })
+ },
+ { immediate: true }
+ )
+ })
+
+ const onSessionEnd = () => {
+ scopeHandle.stop()
+ minisearch.removeAll()
+ }
+
+ const resultObj = computed(() => ({
+ loading: loading.value,
+ results: results.value,
+ }))
+
+ return [resultObj, onSessionEnd]
+ }
+
+ onResultSelect(result: SpotlightSearcherResult): void {
+ invokeAction("workspace.switch", {
+ teamId: result.id.split("-")[1],
+ })
+ }
+}
]