From 4d50af05fd7deb534948fdff15fb532f3e8501cd Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 3 Jun 2026 20:52:57 +0800 Subject: [PATCH] fix(sidebar): keep connection search results visible --- .../src/components/sidebar/ConnectionTree.vue | 7 ++-- apps/desktop/src/lib/sidebarSearch.ts | 14 ++++++-- apps/desktop/src/lib/sidebarSearchTree.ts | 27 +++++++++++++--- packages/app-tests/sidebarSearchTree.test.ts | 32 ++++++++++++++++++- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index c208877de..d5f7ecc74 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -6,7 +6,7 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; import { useSettingsStore } from "@/stores/settingsStore"; import type { QueryTab, TreeNode, TreeNodeType } from "@/types/database"; -import { filterSidebarTree } from "@/lib/sidebarSearchTree"; +import { filterSidebarSearchRootsByConnectionState, filterSidebarTree } from "@/lib/sidebarSearchTree"; import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts"; import { usesTreeSchemaMode } from "@/lib/databaseFeatureSupport"; import { @@ -139,10 +139,7 @@ const filteredNodes = computed(() => { const q = deferredSearchQuery.value; if (q) { nodes = filterSidebarTree(nodes, q, searchCollapsedIds.value, searchableNodeTypes.value); - nodes = nodes.filter((node) => { - if (node.type === "connection-group") return true; - return node.connectionId ? store.connectedIds.has(node.connectionId) : true; - }); + nodes = filterSidebarSearchRootsByConnectionState(nodes, store.connectedIds); } return nodes; diff --git a/apps/desktop/src/lib/sidebarSearch.ts b/apps/desktop/src/lib/sidebarSearch.ts index 6edaeb6b0..ff4de14bd 100644 --- a/apps/desktop/src/lib/sidebarSearch.ts +++ b/apps/desktop/src/lib/sidebarSearch.ts @@ -14,6 +14,8 @@ export interface SidebarSearchMatch { score: number; } +export type SidebarLabelMatcher = (label: string) => SidebarSearchMatch | null; + function isWordBoundary(text: string, index: number): boolean { if (index === 0) return true; const prev = text[index - 1]; @@ -48,10 +50,9 @@ function matchesSubsequence(text: string, query: string): boolean { return j === query.length; } -export function matchSidebarLabel(label: string, query: string): SidebarSearchMatch | null { +function matchSidebarLabelWithRegex(label: string, query: string, regex: RegExp | null): SidebarSearchMatch | null { if (!query) return null; - const regex = parseSlashDelimitedRegexQuery(query); if (regex) return regex.test(label) ? { kind: "regex", score: 95 } : null; if (label === query) return { kind: "exact", score: 100 }; @@ -63,3 +64,12 @@ export function matchSidebarLabel(label: string, query: string): SidebarSearchMa return null; } + +export function createSidebarLabelMatcher(query: string): SidebarLabelMatcher { + const regex = parseSlashDelimitedRegexQuery(query); + return (label) => matchSidebarLabelWithRegex(label, query, regex); +} + +export function matchSidebarLabel(label: string, query: string): SidebarSearchMatch | null { + return matchSidebarLabelWithRegex(label, query, parseSlashDelimitedRegexQuery(query)); +} diff --git a/apps/desktop/src/lib/sidebarSearchTree.ts b/apps/desktop/src/lib/sidebarSearchTree.ts index 312c1323e..5ee91e8cc 100644 --- a/apps/desktop/src/lib/sidebarSearchTree.ts +++ b/apps/desktop/src/lib/sidebarSearchTree.ts @@ -1,5 +1,5 @@ import type { TreeNode, TreeNodeType } from "@/types/database"; -import { matchSidebarLabel } from "@/lib/sidebarSearch"; +import { createSidebarLabelMatcher, type SidebarLabelMatcher } from "@/lib/sidebarSearch"; const preserveMatchedSubtreeTypes = new Set(["database", "schema", "table", "view"]); @@ -19,13 +19,22 @@ export function filterSidebarTree( query: string, collapsedIds: ReadonlySet, searchableNodeTypes?: ReadonlySet, +): TreeNode[] { + return filterSidebarTreeWithMatcher(nodes, createSidebarLabelMatcher(query), collapsedIds, searchableNodeTypes); +} + +function filterSidebarTreeWithMatcher( + nodes: TreeNode[], + matchLabel: SidebarLabelMatcher, + collapsedIds: ReadonlySet, + searchableNodeTypes?: ReadonlySet, ): TreeNode[] { const filteredNodes: { node: TreeNode; score: number }[] = []; for (const node of nodes) { if (node.type === "object-browser" && node.hiddenChildren) { const matches = node.hiddenChildren - .map((child) => ({ node: child, score: matchSidebarLabel(normalizedLabel(child), query)?.score ?? 0 })) + .map((child) => ({ node: child, score: matchLabel(normalizedLabel(child))?.score ?? 0 })) .filter((match) => match.score > 0); filteredNodes.push(...matches); continue; @@ -33,12 +42,12 @@ export function filterSidebarTree( const label = normalizedLabel(node); const canSelfMatch = !searchableNodeTypes || searchableNodeTypes.has(node.type); - const selfMatch = canSelfMatch ? matchSidebarLabel(label, query) : null; + const selfMatch = canSelfMatch ? matchLabel(label) : null; const preservesSubtree = !!selfMatch && preserveMatchedSubtreeTypes.has(node.type); const filteredChildren = preservesSubtree ? node.children : node.children - ? filterSidebarTree(node.children, query, collapsedIds, searchableNodeTypes) + ? filterSidebarTreeWithMatcher(node.children, matchLabel, collapsedIds, searchableNodeTypes) : undefined; if (selfMatch || (filteredChildren && filteredChildren.length > 0)) { @@ -61,3 +70,13 @@ export function filterSidebarTree( filteredNodes.sort((a, b) => b.score - a.score); return filteredNodes.map((match) => match.node); } + +export function filterSidebarSearchRootsByConnectionState( + nodes: TreeNode[], + connectedIds: ReadonlySet, +): TreeNode[] { + return nodes.filter((node) => { + if (node.type === "connection-group" || node.type === "connection") return true; + return node.connectionId ? connectedIds.has(node.connectionId) : true; + }); +} diff --git a/packages/app-tests/sidebarSearchTree.test.ts b/packages/app-tests/sidebarSearchTree.test.ts index 056b6ff4f..21f68de7b 100644 --- a/packages/app-tests/sidebarSearchTree.test.ts +++ b/packages/app-tests/sidebarSearchTree.test.ts @@ -1,6 +1,9 @@ import { strict as assert } from "node:assert"; import test from "node:test"; -import { filterSidebarTree } from "../../apps/desktop/src/lib/sidebarSearchTree.ts"; +import { + filterSidebarSearchRootsByConnectionState, + filterSidebarTree, +} from "../../apps/desktop/src/lib/sidebarSearchTree.ts"; import type { TreeNode } from "../../apps/desktop/src/types/database.ts"; test("preserves loaded table children when the table itself matches search", () => { @@ -118,3 +121,30 @@ test("search scope excludes non-selected node self matches", () => { assert.equal(filtered.length, 0); }); + +test("connection search results stay visible before connecting", () => { + const nodes: TreeNode[] = [ + { + id: "conn:1", + label: "Orders local", + type: "connection", + connectionId: "conn:1", + isExpanded: false, + children: [], + }, + { + id: "conn:1:db", + label: "orders", + type: "database", + connectionId: "conn:1", + database: "orders", + }, + ]; + + const filtered = filterSidebarSearchRootsByConnectionState(nodes, new Set()); + + assert.deepEqual( + filtered.map((node) => node.id), + ["conn:1"], + ); +});