Files
hoppscotch/packages/hoppscotch-common/src/services/spotlight/searchers/environment.searcher.ts
Andrew Bastin be57255bf7 refactor: update to dioc v3 (#4009)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-04-29 19:06:18 +05:30

483 lines
13 KiB
TypeScript

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 IconCopy from "~icons/lucide/copy"
import IconEdit from "~icons/lucide/edit"
import IconLayers from "~icons/lucide/layers"
import IconTrash2 from "~icons/lucide/trash-2"
import { Container, Service } from "dioc"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { cloneDeep } from "lodash-es"
import MiniSearch from "minisearch"
import { map } from "rxjs"
import { useStreamStatic } from "~/composables/stream"
import { GQLError, runGQLQuery } from "~/helpers/backend/GQLClient"
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import {
SelectedEnvironmentIndex,
createEnvironment,
currentEnvironment$,
duplicateEnvironment,
environmentsStore,
getGlobalVariables,
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import * as E from "fp-ts/Either"
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
import { useToast } from "~/composables/toast"
import { GetTeamEnvironmentsDocument } from "~/helpers/backend/graphql"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { WorkspaceService } from "~/services/workspace.service"
import IconCircle from "~icons/lucide/circle"
type Doc = {
text: string | string[]
alternates: string[]
icon: object | Component
excludeFromSearch?: boolean
}
type SelectedEnv = {
selected?: boolean
} & (
| Omit<SelectedEnvironmentIndex & { type: "TEAM_ENV" }, "environment">
| (SelectedEnvironmentIndex & { type: "MY_ENV" })
)
/**
*
* 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$,
{
type: "NO_ENV_SELECTED",
},
() => {
/* 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.title"),
this.t("spotlight.environments.new"),
],
alternates: ["new", "environment"],
icon: markRaw(IconLayers),
},
new_environment_variable: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.new_variable"),
],
alternates: ["new", "environment", "variable"],
icon: markRaw(IconLayers),
},
edit_selected_env: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.edit"),
],
alternates: ["edit", "environment"],
icon: markRaw(IconEdit),
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
},
delete_selected_env: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.delete"),
],
alternates: ["delete", "environment"],
icon: markRaw(IconTrash2),
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
},
duplicate_selected_env: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.duplicate"),
],
alternates: ["duplicate", "environment"],
icon: markRaw(IconCopy),
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
},
edit_global_env: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.edit_global"),
],
alternates: ["edit", "global", "environment"],
icon: markRaw(IconEdit),
},
duplicate_global_env: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.duplicate_global"),
],
alternates: ["duplicate", "global", "environment"],
icon: markRaw(IconCopy),
},
})
// TODO: This pattern is no longer recommended in dioc > 3, move to something else
constructor(c: Container) {
super(c, {
searchFields: ["text", "alternates"],
fieldWeights: {
text: 2,
alternates: 1,
},
})
}
override onServiceInit() {
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")}`)
}
)
)()
}
}
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":
invokeAction(`modals.environment.delete-selected`)
break
case "duplicate_selected_env":
this.duplicateSelectedEnv()
break
case "edit_global_env":
invokeAction(`modals.global.environment.update`, {})
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()
private toast = useToast()
public searcherID = "switch_env"
public searcherSectionTitle = this.t("tab.environments")
private readonly spotlight = this.bind(SpotlightService)
private readonly workspaceService = this.bind(WorkspaceService)
private teamEnvironmentList: TeamEnvironment[] = []
override onServiceInit() {
this.spotlight.registerSearcher(this)
}
private selectedEnvIndex = useStreamStatic(
selectedEnvironmentIndex$,
{
type: "NO_ENV_SELECTED",
},
() => {
/* noop */
}
)[0]
private environmentSearchable = useStreamStatic(
activeActions$.pipe(
map((actions) => actions.includes("modals.environment.add"))
),
activeActions$.value.includes("modals.environment.add"),
() => {
/* noop */
}
)[0]
async fetchTeamEnvironmentList(teamID: string): Promise<TeamEnvironment[]> {
const results: TeamEnvironment[] = []
const result = await runGQLQuery({
query: GetTeamEnvironmentsDocument,
variables: {
teamID: teamID,
},
})
if (E.isRight(result)) {
if (result.right.team) {
results.push(
...result.right.team.teamEnvironments.map(
({ id, teamID, name, variables }) =>
<TeamEnvironment>{
id: id,
teamID: teamID,
environment: {
name: name,
variables: JSON.parse(variables),
},
}
)
)
}
}
return 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"],
})
if (this.environmentSearchable.value) {
minisearch.addAll(
environmentsStore.value.environments.map((entry, index) => {
const id: SelectedEnv = {
type: "MY_ENV",
index,
}
if (
this.selectedEnvIndex.value?.type === "MY_ENV" &&
this.selectedEnvIndex.value.index === index
) {
id.selected = true
}
return {
id: JSON.stringify(id),
name: entry.name,
alternates: ["environment", "change", entry.name],
}
})
)
const workspace = this.workspaceService.currentWorkspace
if (workspace.value?.type === "team") {
this.fetchTeamEnvironmentList(workspace.value.teamID).then(
(results) => {
this.teamEnvironmentList = results
minisearch.addAll(
results.map(({ teamID, id: teamEnvID, environment }) => {
const id: SelectedEnv = {
type: "TEAM_ENV",
teamID,
teamEnvID,
}
if (
this.selectedEnvIndex.value?.type === "TEAM_ENV" &&
this.selectedEnvIndex.value.teamEnvID === teamEnvID
) {
id.selected = true
}
return {
id: JSON.stringify(id),
name: environment.name,
alternates: ["environment", "change", environment.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(({ id, score, name }) => {
return {
id: id,
icon: markRaw(
JSON.parse(id).selected ? IconCheckCircle : IconCircle
),
score: score,
text: {
type: "text",
text: [this.t("environment.set"), 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 {
try {
const selectedEnv = JSON.parse(result.id) as SelectedEnv
if (selectedEnv.type === "MY_ENV") {
setSelectedEnvironmentIndex({
type: "MY_ENV",
index: selectedEnv.index,
})
}
if (selectedEnv.type === "TEAM_ENV") {
const teamEnv = this.teamEnvironmentList.find(
({ id }) => id === selectedEnv.teamEnvID
)
if (!teamEnv) return
const { teamID, teamEnvID } = selectedEnv
setSelectedEnvironmentIndex({
type: "TEAM_ENV",
teamEnvID,
teamID,
environment: teamEnv.environment,
})
}
} catch (e) {
console.error((e as Error).message)
this.toast.error(this.t("error.something_went_wrong"))
}
}
}