feat: expanded search capabilities of spotlight (#3255)

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
Anwarul Islam
2023-08-21 20:33:51 +06:00
committed by GitHub
parent b0b6edc58e
commit 5a91fb53b2
23 changed files with 1633 additions and 25 deletions

View File

@@ -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,
})
}
}