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: "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 { 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 = 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 ): 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")}`) } ) )() } } 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 { 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 }) => { id: id, teamID: teamID, environment: { name: name, variables: JSON.parse(variables), }, } ) ) } } return results } createSearchSession( query: Readonly> ): [Ref, () => void] { const loading = ref(false) const results = ref([]) 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(() => ({ 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")) } } }