diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 70d0e2f8fc..92df951ba4 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -25,7 +25,7 @@ import { localTableSearchParentTypes, } from "@/lib/sidebar/sidebarSearchTree"; import { createSidebarLabelMatcher } from "@/lib/sidebar/sidebarSearch"; -import { collectSidebarRegexIndexScopes, resolveSidebarRemoteSearchQuery, resolveSidebarSearchDispatchMode } from "@/lib/sidebar/sidebarRegexSearchIndex"; +import { collectSidebarRegexIndexScopes, resolveSidebarRemoteSearchQuery, resolveSidebarSearchDispatchMode, shouldRestoreTrackedSidebarSearchTargetsInRegexMode } from "@/lib/sidebar/sidebarRegexSearchIndex"; import { needsSidebarObjectGroupDiscovery } from "@/lib/sidebar/sidebarSearchDiscovery"; import { createSidebarSearchExpansionState } from "@/lib/sidebar/sidebarSearchExpansionState"; import { createSidebarSearchLoadingTracker } from "@/lib/sidebar/sidebarSearchLoadingTracker"; @@ -301,7 +301,10 @@ watch([deferredSearchQuery, regexMode], ([newQuery, isRegexMode], [oldQuery, was if (dispatchMode === "regex") { // Regex search is a read-only projection over live nodes and the local // table index. It must never trigger ensureConnected/listTables. - const restoreTasks = restoreTrackedSearchTargets(); + // Keep results already loaded by an ordinary query until the regex is + // cleared. Restoring them here can empty the tree when this connection has + // no complete local table index yet (for example: "user" -> Regex mode). + const restoreTasks = shouldRestoreTrackedSidebarSearchTargetsInRegexMode(newQuery) ? restoreTrackedSearchTargets() : []; const searchGeneration = sidebarSearchLoadingTracker.begin(); isSidebarSearchLoading.value = true; void Promise.allSettled([loadRegexTableSearchIndexes(), runSidebarSearchTasks(restoreTasks)]) diff --git a/apps/desktop/src/lib/sidebar/sidebarRegexSearchIndex.ts b/apps/desktop/src/lib/sidebar/sidebarRegexSearchIndex.ts index 3a3714911f..72cf5d7af1 100644 --- a/apps/desktop/src/lib/sidebar/sidebarRegexSearchIndex.ts +++ b/apps/desktop/src/lib/sidebar/sidebarRegexSearchIndex.ts @@ -106,3 +106,13 @@ export function resolveSidebarSearchDispatchMode(transition: SidebarSearchTransi if (transition.wasRegexMode && !transition.query) return "none"; return "ordinary"; } + +/** + * Keep an ordinary-search projection available while a non-empty regex is + * filtering it. Restoring those tracked nodes on the mode switch can remove + * the only currently loaded matches before a local table index exists. The + * projection is restored once the regex query is cleared. + */ +export function shouldRestoreTrackedSidebarSearchTargetsInRegexMode(query: string): boolean { + return query.length === 0; +} diff --git a/packages/app-tests/sidebarRegexSearchIndex.test.ts b/packages/app-tests/sidebarRegexSearchIndex.test.ts index 2530345dd7..08f229abe8 100644 --- a/packages/app-tests/sidebarRegexSearchIndex.test.ts +++ b/packages/app-tests/sidebarRegexSearchIndex.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import { test, vi } from "vitest"; -import { collectSidebarRegexIndexScopes, regexTableSearchParents, resolveSidebarRemoteSearchQuery, resolveSidebarSearchDispatchMode } from "../../apps/desktop/src/lib/sidebar/sidebarRegexSearchIndex.ts"; +import { collectSidebarRegexIndexScopes, regexTableSearchParents, resolveSidebarRemoteSearchQuery, resolveSidebarSearchDispatchMode, shouldRestoreTrackedSidebarSearchTargetsInRegexMode } from "../../apps/desktop/src/lib/sidebar/sidebarRegexSearchIndex.ts"; import type { SidebarRegexIndexScope } from "../../apps/desktop/src/lib/sidebar/sidebarSearchTree.ts"; import type { TableInfo, TreeNode } from "../../apps/desktop/src/types/database.ts"; @@ -40,6 +40,12 @@ test("local search on keeps ordinary queries local and local search off enables assert.equal(resolveSidebarSearchDispatchMode({ query: "", regexMode: false, wasRegexMode: false }, { localSearchEnabled: false }), "ordinary"); }); +test("keeps ordinary search results available until the regex query is cleared", () => { + assert.equal(shouldRestoreTrackedSidebarSearchTargetsInRegexMode("user"), false); + assert.equal(shouldRestoreTrackedSidebarSearchTargetsInRegexMode("user|role"), false); + assert.equal(shouldRestoreTrackedSidebarSearchTargetsInRegexMode(""), true); +}); + test("collects manifest scopes and live-tree backfill scopes through the read-only reader", async () => { const manifestScope = scope("conn-1:app:public:__tables", { path: [{ id: "conn-1", label: "c", type: "connection", connectionId: "conn-1" }] }); const live: TreeNode[] = [