feat: expanded search capabilities of spotlight (#3255)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
@@ -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<Doc> {
|
||||
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<string, Doc> = 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<Doc>
|
||||
): 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<string>) => {
|
||||
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<string>) => {
|
||||
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<string>>
|
||||
): [Ref<SpotlightSearcherSessionState>, () => void] {
|
||||
const loading = ref(false)
|
||||
const results = ref<SpotlightSearcherResult[]>([])
|
||||
|
||||
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<SpotlightSearcherSessionState>(() => ({
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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<Doc> {
|
||||
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<string, Doc> = 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<Doc>
|
||||
): 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Doc> {
|
||||
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<string, Doc> = 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<Doc>
|
||||
): 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`)
|
||||
}
|
||||
}
|
||||
@@ -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<Doc> {
|
||||
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<string, Doc> = 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<Doc>
|
||||
): 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Doc> {
|
||||
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<string, Doc> = 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<Doc>
|
||||
): 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`)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Doc> {
|
||||
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<string, Doc> = 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<Doc>
|
||||
): 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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<Doc> {
|
||||
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<string, Doc> = 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<Doc>
|
||||
): 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<GetMyTeamsQuery["myTeams"]> {
|
||||
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<string>>
|
||||
): [Ref<SpotlightSearcherSessionState>, () => void] {
|
||||
const loading = ref(false)
|
||||
const results = ref<SpotlightSearcherResult[]>([])
|
||||
|
||||
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<SpotlightSearcherSessionState>(() => ({
|
||||
loading: loading.value,
|
||||
results: results.value,
|
||||
}))
|
||||
|
||||
return [resultObj, onSessionEnd]
|
||||
}
|
||||
|
||||
onResultSelect(result: SpotlightSearcherResult): void {
|
||||
invokeAction("workspace.switch", {
|
||||
teamId: result.id.split("-")[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user