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 IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue" 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 | 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("team.title"), this.t("spotlight.workspace.new")], alternates: ["new", "team", "workspace"], icon: markRaw(IconUsers), }, edit_team: { text: [this.t("team.title"), this.t("spotlight.workspace.edit")], alternates: ["edit", "team", "workspace"], icon: markRaw(IconEdit), excludeFromSearch: computed(() => !this.isTeamSelected.value), }, invite_members: { text: [this.t("team.title"), this.t("spotlight.workspace.invite")], alternates: ["invite", "members", "workspace"], icon: markRaw(IconUserPlus), excludeFromSearch: computed(() => !this.isTeamSelected.value), }, delete_team: { text: [this.t("team.title"), this.t("spotlight.workspace.delete")], alternates: ["delete", "team", "workspace"], icon: markRaw(IconTrash2), excludeFromSearch: computed(() => !this.isTeamSelected.value), }, switch_to_personal: { text: [ this.t("team.title"), 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") { if (platform.auth.getCurrentUser()) { invokeAction(`modals.team.new`) } else { invokeAction(`modals.login.toggle`) } } 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) }) } private workspace = useStreamStatic( workspaceStatus$, { type: "personal" }, () => { /* noop */ } )[0] 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) => { let id = `workspace-${entry.id}` // if id matches add -selected to it if ( this.workspace.value.type === "team" && this.workspace.value.teamID === entry.id ) { id += "-selected" } return { 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( x.id.endsWith("-selected") ? IconCheckCircle : 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], }) } }