feat: spotlight collection searcher (#3262)

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
Andrew Bastin
2023-08-18 20:56:45 +05:30
committed by GitHub
parent f21ed30e10
commit c626fb9241
4 changed files with 453 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
<template>
<span class="flex flex-1 space-x-2 items-center">
<template v-for="(folder, index) in pathFolders" :key="index">
<span class="block" :class="{ truncate: index !== 0 }">
{{ folder.name }}
</span>
<icon-lucide-chevron-right class="flex flex-shrink-0" />
</template>
<span v-if="request" class="block">
{{ request.name }}
</span>
</span>
</template>
<script setup lang="ts">
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
import { computed } from "vue"
import { graphqlCollectionStore } from "~/newstore/collections"
const props = defineProps<{
folderPath: string
}>()
const pathFolders = computed(() => {
try {
const folderIndicies = props.folderPath
.split("/")
.slice(0, -1)
.map((x) => parseInt(x))
const pathItems: HoppCollection<HoppGQLRequest>[] = []
let currentFolder =
graphqlCollectionStore.value.state[folderIndicies.shift()!]
pathItems.push(currentFolder)
while (folderIndicies.length > 0) {
const folderIndex = folderIndicies.shift()!
const folder = currentFolder.folders[folderIndex]
pathItems.push(folder)
currentFolder = folder
}
return pathItems
} catch (e) {
console.error(e)
return []
}
})
const request = computed(() => {
try {
const requestIndex = parseInt(props.folderPath.split("/").at(-1)!)
return pathFolders.value[pathFolders.value.length - 1].requests[
requestIndex
]
} catch (e) {
return null
}
})
</script>

View File

@@ -0,0 +1,71 @@
<template>
<span class="flex flex-1 items-center space-x-2">
<template v-for="(folder, index) in pathFolders" :key="index">
<span class="block" :class="{ truncate: index !== 0 }">
{{ folder.name }}
</span>
<icon-lucide-chevron-right class="flex flex-shrink-0" />
</template>
<span
v-if="request"
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
:class="getMethodLabelColorClassOf(request)"
>
{{ request.method.toUpperCase() }}
</span>
<span v-if="request" class="block">
{{ request.name }}
</span>
</span>
</template>
<script setup lang="ts">
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed } from "vue"
import { restCollectionStore } from "~/newstore/collections"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
const props = defineProps<{
folderPath: string
}>()
const pathFolders = computed(() => {
try {
const folderIndicies = props.folderPath
.split("/")
.slice(0, -1)
.map((x) => parseInt(x))
const pathItems: HoppCollection<HoppRESTRequest>[] = []
let currentFolder = restCollectionStore.value.state[folderIndicies.shift()!]
pathItems.push(currentFolder)
while (folderIndicies.length > 0) {
const folderIndex = folderIndicies.shift()!
const folder = currentFolder.folders[folderIndex]
pathItems.push(folder)
currentFolder = folder
}
return pathItems
} catch (e) {
console.error(e)
return []
}
})
const request = computed(() => {
try {
const requestIndex = parseInt(props.folderPath.split("/").at(-1)!)
return pathFolders.value[pathFolders.value.length - 1].requests[
requestIndex
]
} catch (e) {
return null
}
})
</script>

View File

@@ -97,6 +97,7 @@ import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
const t = useI18n()
@@ -114,6 +115,7 @@ useService(HistorySpotlightSearcherService)
useService(UserSpotlightSearcherService)
useService(NavigationSpotlightSearcherService)
useService(SettingsSpotlightSearcherService)
useService(CollectionsSpotlightSearcherService)
const search = ref("")

View File

@@ -0,0 +1,315 @@
import { Service } from "dioc"
import {
SpotlightSearcher,
SpotlightSearcherResult,
SpotlightSearcherSessionState,
SpotlightService,
} from "../"
import { Ref, computed, effectScope, markRaw, ref, watch } from "vue"
import { getI18n } from "~/modules/i18n"
import MiniSearch from "minisearch"
import {
graphqlCollectionStore,
restCollectionStore,
} from "~/newstore/collections"
import IconFolder from "~icons/lucide/folder"
import RESTRequestSpotlightEntry from "~/components/app/spotlight/entry/RESTRequest.vue"
import GQLRequestSpotlightEntry from "~/components/app/spotlight/entry/GQLRequest.vue"
import { createNewTab } from "~/helpers/rest/tab"
import { getTabRefWithSaveContext } from "~/helpers/rest/tab"
import { currentTabID } from "~/helpers/rest/tab"
import {
HoppCollection,
HoppGQLRequest,
HoppRESTRequest,
} from "@hoppscotch/data"
import { setGQLSession } from "~/newstore/GQLSession"
import { cloneDeep } from "lodash-es"
import { hoppWorkspaceStore } from "~/newstore/workspace"
import { changeWorkspace } from "~/newstore/workspace"
/**
* A spotlight searcher that searches through the user's collections
*
* NOTE: Initializing this service registers it as a searcher with the Spotlight Service.
*/
export class CollectionsSpotlightSearcherService
extends Service
implements SpotlightSearcher
{
public static readonly ID = "COLLECTIONS_SPOTLIGHT_SEARCHER_SERVICE"
private t = getI18n()
public searcherID = "collections"
public searcherSectionTitle = this.t("collection.my_collections")
private readonly spotlight = this.bind(SpotlightService)
constructor() {
super()
this.spotlight.registerSearcher(this)
}
private loadGQLDocsIntoMinisearch(minisearch: MiniSearch) {
const gqlCollsQueue = [
...graphqlCollectionStore.value.state.map((coll, index) => ({
coll: coll,
index: `${index}`,
})),
]
while (gqlCollsQueue.length > 0) {
const { coll, index } = gqlCollsQueue.shift()!
gqlCollsQueue.push(
...coll.folders.map((folder, folderIndex) => ({
coll: folder,
index: `${index}/${folderIndex}`,
}))
)
minisearch.addAll(
coll.requests.map((req, reqIndex) => ({
id: `gql-${index}/${reqIndex}`,
name: req.name,
}))
)
}
}
private loadRESTDocsIntoMinisearch(minisearch: MiniSearch) {
const restDocsQueue = [
...restCollectionStore.value.state.map((coll, index) => ({
coll: coll,
index: `${index}`,
})),
]
while (restDocsQueue.length > 0) {
const { coll, index } = restDocsQueue.shift()!
restDocsQueue.push(
...coll.folders.map((folder, folderIndex) => ({
coll: folder,
index: `${index}/${folderIndex}`,
}))
)
minisearch.addAll(
coll.requests.map((req, reqIndex) => ({
id: `rest-${index}/${reqIndex}`,
name: req.name,
}))
)
}
}
private getCurrentPageCategory() {
// TODO: Better logic for this ?
try {
const url = new URL(window.location.href)
if (url.pathname.startsWith("/graphql")) {
return "graphql"
} else if (url.pathname === "/") {
return "rest"
} else {
return "other"
}
} catch (e) {
return "other"
}
}
public createSearchSession(
query: Readonly<Ref<string>>
): [Ref<SpotlightSearcherSessionState>, () => void] {
const pageCategory = this.getCurrentPageCategory()
const minisearch = new MiniSearch({
fields: ["name"],
storeFields: ["name"],
searchOptions: {
prefix: true,
fuzzy: true,
boost: {
name: 2,
},
weights: {
fuzzy: 0.2,
prefix: 0.8,
},
},
})
if (pageCategory === "rest") {
this.loadRESTDocsIntoMinisearch(minisearch)
} else if (pageCategory === "graphql") {
this.loadGQLDocsIntoMinisearch(minisearch)
}
const results = ref<SpotlightSearcherResult[]>([])
const scopeHandle = effectScope()
scopeHandle.run(() => {
watch(query, (query) => {
if (pageCategory === "other") {
results.value = []
return
}
if (pageCategory === "rest") {
const searchResults = minisearch.search(query).slice(0, 10)
results.value = searchResults.map((result) => ({
id: result.id,
text: {
type: "custom",
component: markRaw(RESTRequestSpotlightEntry),
componentProps: {
folderPath: result.id.split("rest-")[1],
},
},
icon: markRaw(IconFolder),
score: result.score,
}))
} else {
const searchResults = minisearch.search(query).slice(0, 10)
results.value = searchResults.map((result) => ({
id: result.id,
text: {
type: "custom",
component: markRaw(GQLRequestSpotlightEntry),
componentProps: {
folderPath: result.id.split("gql-")[1],
},
},
icon: markRaw(IconFolder),
score: result.score,
}))
}
})
})
const resultObj = computed<SpotlightSearcherSessionState>(() => ({
loading: false,
results: results.value,
}))
return [
resultObj,
() => {
scopeHandle.stop()
},
]
}
private getRESTFolderFromFolderPath(
folderPath: string
): HoppCollection<HoppRESTRequest> | undefined {
try {
const folderIndicies = folderPath.split("/").map((x) => parseInt(x))
let currentFolder =
restCollectionStore.value.state[folderIndicies.shift()!]
while (folderIndicies.length > 0) {
const folderIndex = folderIndicies.shift()!
const folder = currentFolder.folders[folderIndex]
currentFolder = folder
}
return currentFolder
} catch (e) {
console.error(e)
return undefined
}
}
private getGQLFolderFromFolderPath(
folderPath: string
): HoppCollection<HoppGQLRequest> | undefined {
try {
const folderIndicies = folderPath.split("/").map((x) => parseInt(x))
let currentFolder =
graphqlCollectionStore.value.state[folderIndicies.shift()!]
while (folderIndicies.length > 0) {
const folderIndex = folderIndicies.shift()!
const folder = currentFolder.folders[folderIndex]
currentFolder = folder
}
return currentFolder
} catch (e) {
console.error(e)
return undefined
}
}
public onResultSelect(result: SpotlightSearcherResult): void {
const [type, path] = result.id.split("-")
if (type === "rest") {
const folderPath = path.split("/").map((x) => parseInt(x))
const reqIndex = folderPath.pop()!
if (hoppWorkspaceStore.value.workspace.type !== "personal") {
changeWorkspace({
type: "personal",
})
}
const possibleTab = getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: folderPath.join("/"),
requestIndex: reqIndex,
})
if (possibleTab) {
currentTabID.value = possibleTab.value.id
} else {
const req = this.getRESTFolderFromFolderPath(folderPath.join("/"))
?.requests[reqIndex]
if (!req) return
createNewTab(
{
request: req,
isDirty: false,
saveContext: {
originLocation: "user-collection",
folderPath: folderPath.join("/"),
requestIndex: reqIndex,
},
},
true
)
}
} else if (type === "gql") {
const folderPath = path.split("/").map((x) => parseInt(x))
const reqIndex = folderPath.pop()!
const req = this.getGQLFolderFromFolderPath(folderPath.join("/"))
?.requests[reqIndex]
if (!req) return
setGQLSession({
request: cloneDeep(req),
schema: "",
response: "",
})
}
}
}