diff --git a/apps/desktop/src/lib/sidebar/sidebarSearchTree.ts b/apps/desktop/src/lib/sidebar/sidebarSearchTree.ts index ed4a20c0a4..7be3588124 100644 --- a/apps/desktop/src/lib/sidebar/sidebarSearchTree.ts +++ b/apps/desktop/src/lib/sidebar/sidebarSearchTree.ts @@ -4,7 +4,14 @@ import { buildTableTreeNodes } from "@/lib/table/tableTree"; import { stripTableVGroupsFromChildren } from "@/lib/table/tableVGroup"; const preserveMatchedSubtreeTypes = new Set(["connection", "database", "schema", "table", "view", "mongo-db", "mongo-collection"]); -const hiddenSearchNodeTypes = new Set(["user-admin", "dameng-job-admin"]); +// Synthetic connection utility entries (the isConnectionUtilityNode types in +// connectionStore) are admin/navigation shortcuts, not schema objects. Their +// labels are i18n keys that can never match a text query, so they never +// self-match — but a few of them (oracle-db-links, group-tablespaces) are +// groups whose children ARE real objects, so their children stay searchable +// and the group only survives through a child hit. saved-sql-root is exempt: +// its saved queries are real searchable files rather than a shortcut. +const hiddenSearchNodeTypes = new Set(["user-admin", "dameng-users", "dameng-roles", "dameng-job-admin", "group-tablespaces", "oracle-db-links"]); function bestMatch(matchLabel: SidebarLabelMatcher, label: string, comment?: string | null, aliases?: readonly string[]) { let best = matchLabel(label); @@ -360,7 +367,11 @@ function filterSidebarTreeWithMatcher(nodes: TreeNode[], matchLabel: SidebarLabe const filteredNodes: { node: TreeNode; score: number }[] = []; for (const node of nodes) { - if (matchLabel && hiddenSearchNodeTypes.has(node.type)) continue; + // Utility groups never match by their own label; groups without children + // (or not yet loaded) drop out entirely, while groups that hold real + // objects (db links, tablespaces) survive through a matching child. + const hiddenUtility = !!matchLabel && hiddenSearchNodeTypes.has(node.type); + if (hiddenUtility && !node.children?.length) continue; if (node.type === "object-browser" && node.hiddenChildren) { const matches = node.hiddenChildren.flatMap((child) => { if (searchableNodeTypes && !searchableNodeTypes.has(child.type)) return []; @@ -373,7 +384,7 @@ function filterSidebarTreeWithMatcher(nodes: TreeNode[], matchLabel: SidebarLabe } const label = normalizedLabel(node, resolveLabel); - const canSelfMatch = !searchableNodeTypes || searchableNodeTypes.has(node.type); + const canSelfMatch = (!searchableNodeTypes || searchableNodeTypes.has(node.type)) && !hiddenUtility; const selfMatch = canSelfMatch ? (matchLabel ? bestMatch(matchLabel, label, node.comment, node.searchAliases) : { score: 0 }) : null; // Type-only filtering keeps matching rows and their ancestor path, but not // unrelated descendants that would make the selected type appear ignored. diff --git a/packages/app-tests/sidebarSearchTree.test.ts b/packages/app-tests/sidebarSearchTree.test.ts index ac1a5a6ed6..cfccdd141e 100644 --- a/packages/app-tests/sidebarSearchTree.test.ts +++ b/packages/app-tests/sidebarSearchTree.test.ts @@ -318,6 +318,20 @@ test("omits synthetic connection management entries when the connection itself m connectionId: "conn:1", database: "", }, + { + id: "conn:1:__dameng_users", + label: "tree.damengUsers", + type: "dameng-users", + connectionId: "conn:1", + database: "", + }, + { + id: "conn:1:__dameng_roles", + label: "tree.damengRoles", + type: "dameng-roles", + connectionId: "conn:1", + database: "", + }, { id: "conn:1:__dameng_jobs", label: "tree.damengJobAdmin", @@ -325,6 +339,20 @@ test("omits synthetic connection management entries when the connection itself m connectionId: "conn:1", database: "", }, + { + id: "conn:1:__xugu_tablespaces", + label: "tree.xuguTablespaces", + type: "group-tablespaces", + connectionId: "conn:1", + database: "", + }, + { + id: "conn:1:__oracle_db_links", + label: "tree.databaseLinks", + type: "oracle-db-links", + connectionId: "conn:1", + database: "", + }, ], }, ]; @@ -354,6 +382,14 @@ test("keeps a disconnected connection search result collapsed when it only has s connectionId: "conn:1", database: "", }, + { + id: "conn:1:__oracle_db_links", + label: "tree.databaseLinks", + type: "oracle-db-links", + connectionId: "conn:1", + database: "", + children: [], + }, ], }, ]; @@ -364,6 +400,79 @@ test("keeps a disconnected connection search result collapsed when it only has s assert.equal(filtered[0]?.isExpanded, false); }); +test("keeps utility groups searchable through their real object children", () => { + const nodes: TreeNode[] = [ + { + id: "conn:1", + label: "prod", + type: "connection", + connectionId: "conn:1", + isExpanded: true, + children: [ + { + id: "conn:1:__oracle_db_links", + label: "tree.databaseLinks", + type: "oracle-db-links", + connectionId: "conn:1", + database: "", + children: [ + { + id: "conn:1:__oracle_db_links:reporting", + label: "REPORTING.LOCALDOMAIN", + type: "oracle-db-link", + connectionId: "conn:1", + database: "", + }, + ], + }, + ], + }, + ]; + + const filtered = filterSidebarTree(nodes, "reporting", new Set()); + + assert.equal(filtered[0]?.type, "connection"); + assert.deepEqual( + filtered[0]?.children?.map((child) => child.type), + ["oracle-db-links"], + ); + assert.equal(filtered[0]?.children?.[0]?.children?.[0]?.label, "REPORTING.LOCALDOMAIN"); +}); + +test("drops utility groups whose children do not match the query", () => { + const nodes: TreeNode[] = [ + { + id: "conn:1", + label: "prod", + type: "connection", + connectionId: "conn:1", + isExpanded: true, + children: [ + { + id: "conn:1:__xugu_tablespaces", + label: "tree.xuguTablespaces", + type: "group-tablespaces", + connectionId: "conn:1", + database: "", + children: [ + { + id: "conn:1:__xugu_tablespaces:system", + label: "SYSTEM", + type: "tablespace", + connectionId: "conn:1", + database: "", + }, + ], + }, + ], + }, + ]; + + const filtered = filterSidebarTree(nodes, "reporting", new Set()); + + assert.deepEqual(filtered, []); +}); + test("does not return synthetic connection management entries as direct text matches", () => { const nodes: TreeNode[] = [ {