feat: expanded search capabilities of spotlight (#3255)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
@@ -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