diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index 03099e667..0d0b57a31 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -175,6 +175,7 @@ "different_parent": "Cannot reorder collection with different parent", "edit": "Edit Collection", "import_or_create": "Import or create a collection", + "import_collection":"Import Collection", "invalid_name": "Please provide a name for the collection", "invalid_root_move": "Collection already in the root", "moved": "Moved Successfully", @@ -849,6 +850,13 @@ "new": "Create new workspace", "switch_to_personal": "Switch to your personal workspace", "title": "Workspaces" + }, + "phrases":{ + "try": "Try", + "import_collections": "Import collections", + "create_environment": "Create environment", + "create_workspace": "Create workspace", + "share_request": "Share request" } }, "sse": { diff --git a/packages/hoppscotch-common/src/components/app/Header.vue b/packages/hoppscotch-common/src/components/app/Header.vue index 55557f960..aa1a448da 100644 --- a/packages/hoppscotch-common/src/components/app/Header.vue +++ b/packages/hoppscotch-common/src/components/app/Header.vue @@ -21,19 +21,7 @@
- +
@@ -251,7 +239,6 @@ import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core" import { computed, reactive, ref, watch } from "vue" import { useToast } from "~/composables/toast" import { GetMyTeamsQuery, TeamMemberRole } from "~/helpers/backend/graphql" -import { getPlatformSpecialKey } from "~/helpers/platformutils" import { platform } from "~/platform" import IconDownload from "~icons/lucide/download" import IconLifeBuoy from "~icons/lucide/life-buoy" diff --git a/packages/hoppscotch-common/src/components/app/SpotlightSearch.vue b/packages/hoppscotch-common/src/components/app/SpotlightSearch.vue new file mode 100644 index 000000000..6704390bb --- /dev/null +++ b/packages/hoppscotch-common/src/components/app/SpotlightSearch.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/packages/hoppscotch-common/src/components/collections/graphql/index.vue b/packages/hoppscotch-common/src/components/collections/graphql/index.vue index bf95552b4..67fe145fc 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/index.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/index.vue @@ -192,6 +192,7 @@ import { PersistenceService } from "~/services/persistence" import { PersistedOAuthConfig } from "~/services/oauth/oauth.service" import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue" import { EditingProperties } from "../Properties.vue" +import { defineActionHandler } from "~/helpers/actions" const t = useI18n() const toast = useToast() @@ -676,4 +677,11 @@ const resetSelectedData = () => { editingRequest.value = null editingRequestIndex.value = null } + +defineActionHandler("collection.new", () => { + displayModalAdd(true) +}) +defineActionHandler("modals.collection.import", () => { + displayModalImportExport(true) +}) diff --git a/packages/hoppscotch-common/src/components/collections/index.vue b/packages/hoppscotch-common/src/components/collections/index.vue index db0b4f192..9e1fc4d66 100644 --- a/packages/hoppscotch-common/src/components/collections/index.vue +++ b/packages/hoppscotch-common/src/components/collections/index.vue @@ -2351,4 +2351,7 @@ const getErrorMessage = (err: GQLError) => { defineActionHandler("collection.new", () => { displayModalAdd(true) }) +defineActionHandler("modals.collection.import", () => { + displayModalImportExport(true) +}) diff --git a/packages/hoppscotch-common/src/helpers/actions.ts b/packages/hoppscotch-common/src/helpers/actions.ts index cef62d130..b8fbd1ca8 100644 --- a/packages/hoppscotch-common/src/helpers/actions.ts +++ b/packages/hoppscotch-common/src/helpers/actions.ts @@ -36,6 +36,7 @@ export type HoppAction = | "collection.new" // Create root collection | "flyouts.chat.open" // Shows the keybinds flyout | "flyouts.keybinds.toggle" // Shows the keybinds flyout + | "modals.collection.import" // Shows the collection import modal | "modals.search.toggle" // Shows the search modal | "modals.support.toggle" // Shows the support modal | "modals.share.toggle" // Shows the share modal diff --git a/packages/hoppscotch-common/src/layouts/default.vue b/packages/hoppscotch-common/src/layouts/default.vue index 424d9d947..e7727661d 100644 --- a/packages/hoppscotch-common/src/layouts/default.vue +++ b/packages/hoppscotch-common/src/layouts/default.vue @@ -73,7 +73,7 @@ import { useI18n } from "~/composables/i18n" import { useToast } from "~/composables/toast" import { InvocationTriggers, defineActionHandler } from "~/helpers/actions" import { hookKeybindingsListener } from "~/helpers/keybindings" -import { applySetting } from "~/newstore/settings" +import { applySetting, toggleSetting } from "~/newstore/settings" import { platform } from "~/platform" import { HoppSpotlightSessionEventData } from "~/platform/analytics" import { PersistenceService } from "~/services/persistence" @@ -97,6 +97,8 @@ const t = useI18n() const persistenceService = useService(PersistenceService) const spotlightService = useService(SpotlightService) +const HAS_OPENED_SPOTLIGHT = useSetting("HAS_OPENED_SPOTLIGHT") + onBeforeMount(() => { if (!mdAndLarger.value) { rightSidebar.value = false @@ -160,6 +162,7 @@ defineActionHandler("modals.search.toggle", (_, trigger) => { }) showSearch.value = !showSearch.value + !HAS_OPENED_SPOTLIGHT.value && toggleSetting("HAS_OPENED_SPOTLIGHT") }) defineActionHandler("modals.support.toggle", () => { diff --git a/packages/hoppscotch-common/src/newstore/settings.ts b/packages/hoppscotch-common/src/newstore/settings.ts index 61ed992f8..fe86430f7 100644 --- a/packages/hoppscotch-common/src/newstore/settings.ts +++ b/packages/hoppscotch-common/src/newstore/settings.ts @@ -65,6 +65,8 @@ export type SettingsDef = { SIDEBAR: boolean SIDEBAR_ON_LEFT: boolean COLUMN_LAYOUT: boolean + + HAS_OPENED_SPOTLIGHT: boolean } export const getDefaultSettings = (): SettingsDef => ({ @@ -109,6 +111,8 @@ export const getDefaultSettings = (): SettingsDef => ({ SIDEBAR: true, SIDEBAR_ON_LEFT: false, COLUMN_LAYOUT: true, + + HAS_OPENED_SPOTLIGHT: false, }) type ApplySettingPayload = { diff --git a/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts b/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts index 640604286..5efdf7655 100644 --- a/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts +++ b/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts @@ -66,6 +66,8 @@ const SettingsDefSchema = z.object({ cookie: z.boolean().catch(true), }) ), + + HAS_OPENED_SPOTLIGHT: z.optional(z.boolean()), }) // Common properties shared across REST & GQL collections diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/collections.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/collections.searcher.ts index 5326661bc..376b7e8c0 100644 --- a/packages/hoppscotch-common/src/services/spotlight/searchers/collections.searcher.ts +++ b/packages/hoppscotch-common/src/services/spotlight/searchers/collections.searcher.ts @@ -15,6 +15,7 @@ import { restCollectionStore, } from "~/newstore/collections" import IconFolder from "~icons/lucide/folder" +import IconImport from "~icons/lucide/folder-down" import RESTRequestSpotlightEntry from "~/components/app/spotlight/entry/RESTRequest.vue" import GQLRequestSpotlightEntry from "~/components/app/spotlight/entry/GQLRequest.vue" import { @@ -151,6 +152,10 @@ export class CollectionsSpotlightSearcherService id: `create-collection`, name: this.t("collection.new"), }) + minisearch.add({ + id: "import-collection", + name: this.t("collection.import"), + }) } if (pageCategory === "rest") { @@ -168,6 +173,11 @@ export class CollectionsSpotlightSearcherService text: this.t("collection.new"), } + const importCollectionText: SpotlightResultTextType = { + type: "text", + text: this.t("collection.import_collection"), + } + scopeHandle.run(() => { const isPersonalWorkspace = computed( () => this.workspaceService.currentWorkspace.value.type === "personal" @@ -183,44 +193,37 @@ export class CollectionsSpotlightSearcherService results.value = [] return } - - if (pageCategory === "rest") { - const searchResults = minisearch.search(query).slice(0, 10) - - results.value = searchResults.map((result) => ({ - id: result.id, - text: - result.id === "create-collection" - ? newCollectionText - : { - type: "custom", - component: markRaw(RESTRequestSpotlightEntry), - componentProps: { - folderPath: result.id.split("rest-")[1], - }, - }, - icon: markRaw(IconFolder), - score: result.score, - })) - } else if (pageCategory === "graphql") { - const searchResults = minisearch.search(query).slice(0, 10) - - results.value = searchResults.map((result) => ({ - id: result.id, - text: - result.id === "create-collection" - ? newCollectionText - : { - type: "custom", - component: markRaw(GQLRequestSpotlightEntry), - componentProps: { - folderPath: result.id.split("gql-")[1], - }, - }, - icon: markRaw(IconFolder), - score: result.score, - })) + const getResultText = (id: string): SpotlightResultTextType => { + if (id === "create-collection") return newCollectionText + else if (id === "import-collection") return importCollectionText + return { + type: "custom", + component: markRaw( + pageCategory === "rest" + ? RESTRequestSpotlightEntry + : GQLRequestSpotlightEntry + ), + componentProps: { + folderPath: id.split( + pageCategory === "rest" ? "rest-" : "gql-" + )[1], + }, + } } + + const getResultIcon = (id: string) => { + if (id === "import-collection") return markRaw(IconImport) + return markRaw(IconFolder) + } + + const searchResults = minisearch.search(query).slice(0, 10) + + results.value = searchResults.map((result) => ({ + id: result.id, + text: getResultText(result.id), + icon: getResultIcon(result.id), + score: result.score, + })) }) }) @@ -288,6 +291,9 @@ export class CollectionsSpotlightSearcherService public onResultSelect(result: SpotlightSearcherResult): void { if (result.id === "create-collection") return invokeAction("collection.new") + if (result.id === "import-collection") + return invokeAction(`modals.collection.import`) + const [type, path] = result.id.split("-") if (type === "rest") { diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/teamRequest.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/teamRequest.searcher.ts index 2fc82b096..ac3426506 100644 --- a/packages/hoppscotch-common/src/services/spotlight/searchers/teamRequest.searcher.ts +++ b/packages/hoppscotch-common/src/services/spotlight/searchers/teamRequest.searcher.ts @@ -1,20 +1,24 @@ import { Service } from "dioc" import { + SpotlightResultTextType, SpotlightSearcher, SpotlightSearcherResult, SpotlightSearcherSessionState, SpotlightService, } from ".." import { getI18n } from "~/modules/i18n" -import { Ref, computed, effectScope, markRaw, watch } from "vue" +import { Ref, computed, effectScope, markRaw, ref, watch } from "vue" import { TeamSearchService } from "~/helpers/teams/TeamsSearch.service" import { cloneDeep, debounce } from "lodash-es" import IconFolder from "~icons/lucide/folder" +import IconImport from "~icons/lucide/folder-down" import { WorkspaceService } from "~/services/workspace.service" import RESTTeamRequestEntry from "~/components/app/spotlight/entry/RESTTeamRequestEntry.vue" import { RESTTabService } from "~/services/tab/rest" import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties" import { HoppRESTRequest } from "@hoppscotch/data" +import MiniSearch from "minisearch" +import { invokeAction } from "~/helpers/actions" export class TeamsSpotlightSearcherService extends Service @@ -41,9 +45,79 @@ export class TeamsSpotlightSearcherService this.spotlight.registerSearcher(this) } + 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" + } + return "other" + } catch (e) { + return "other" + } + } + createSearchSession( query: Readonly> ): [Ref, () => void] { + const pageCategory = this.getCurrentPageCategory() + + // Only show the searcher on the REST page + if (pageCategory !== "rest") { + return [computed(() => ({ loading: false, results: [] })), () => {}] + } + + const results = ref([]) + + const minisearch = new MiniSearch({ + fields: ["name"], + storeFields: ["name"], + searchOptions: { + prefix: true, + fuzzy: true, + boost: { + name: 2, + }, + weights: { + fuzzy: 0.2, + prefix: 0.8, + }, + }, + }) + + minisearch.add({ + id: `create-collection`, + name: this.t("collection.new"), + }) + minisearch.add({ + id: "import-collection", + name: this.t("collection.import"), + }) + + const newCollectionText: SpotlightResultTextType = { + type: "text", + text: this.t("collection.new"), + } + + const importCollectionText: SpotlightResultTextType = { + type: "text", + text: this.t("collection.import_collection"), + } + + const getResultText = (id: string): SpotlightResultTextType => { + if (id === "create-collection") return newCollectionText + return importCollectionText + } + + const getResultIcon = (id: string) => { + if (id === "import-collection") return markRaw(IconImport) + return markRaw(IconFolder) + } + const isTeamWorkspace = computed( () => this.workspaceService.currentWorkspace.value.type === "team" ) @@ -59,6 +133,13 @@ export class TeamsSpotlightSearcherService if (this.workspaceService.currentWorkspace.value.type === "team") { const teamID = this.workspaceService.currentWorkspace.value.teamID debouncedSearch(query, teamID)?.catch(() => {}) + const searchResults = minisearch.search(query).slice(0, 10) + results.value = searchResults.map((result) => ({ + id: result.id, + text: getResultText(result.id), + icon: getResultIcon(result.id), + score: result.score, + })) } }, { @@ -91,36 +172,47 @@ export class TeamsSpotlightSearcherService } const resultObj = computed(() => { - return isTeamWorkspace.value - ? { - loading: this.teamsSearch.teamsSearchResultsLoading.value, - results: - this.teamsSearch.teamsSearchResultsFormattedForSpotlight.value.map( - (result) => ({ - id: result.request.id, - icon: markRaw(IconFolder), - score: 1, // make a better scoring system for this - text: { - type: "custom", - component: markRaw(RESTTeamRequestEntry), - componentProps: { - collectionTitles: result.collectionTitles, - request: result.request, - }, - }, - }) - ), - } - : { - loading: false, - results: [], - } - }) + if (isTeamWorkspace.value) { + const teamsSearchResults = + this.teamsSearch.teamsSearchResultsFormattedForSpotlight.value + const minisearchResults = results.value + const mergedResults = [ + ...teamsSearchResults.map((result) => ({ + id: result.request.id, + icon: markRaw(IconFolder), + score: 1, // make a better scoring system for this + text: { + type: "custom", + component: markRaw(RESTTeamRequestEntry), + componentProps: { + collectionTitles: result.collectionTitles, + request: result.request, + }, + }, + })), + ...minisearchResults, + ] as SpotlightSearcherResult[] + + return { + loading: this.teamsSearch.teamsSearchResultsLoading.value, + results: mergedResults, + } + } + return { + loading: false, + results: [], + } + }) return [resultObj, onSessionEnd] } onResultSelect(result: SpotlightSearcherResult): void { + if (result.id === "create-collection") return invokeAction("collection.new") + + if (result.id === "import-collection") + return invokeAction(`modals.collection.import`) + let inheritedProperties: HoppInheritedProperty | undefined = undefined const selectedRequest = this.teamsSearch.searchResultsRequests[result.id]