fix(sidebar): keep connection search results visible

This commit is contained in:
t8y2 committed 2026-06-03 20:52:57 +08:00
1 parent 085a2f3870
commit 4d50af05fd
4 files changed
+68 -12

No files matched your search

@@ -6,7 +6,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore"; import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore"; import { useSettingsStore } from "@/stores/settingsStore";
import type { QueryTab, TreeNode, TreeNodeType } from "@/types/database"; 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 { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
import { usesTreeSchemaMode } from "@/lib/databaseFeatureSupport"; import { usesTreeSchemaMode } from "@/lib/databaseFeatureSupport";
import { import {
@@ -139,10 +139,7 @@ const filteredNodes = computed(() => {
const q = deferredSearchQuery.value; const q = deferredSearchQuery.value;
if (q) { if (q) {
nodes = filterSidebarTree(nodes, q, searchCollapsedIds.value, searchableNodeTypes.value); nodes = filterSidebarTree(nodes, q, searchCollapsedIds.value, searchableNodeTypes.value);
nodes = nodes.filter((node) => { nodes = filterSidebarSearchRootsByConnectionState(nodes, store.connectedIds);
if (node.type === "connection-group") return true;
return node.connectionId ? store.connectedIds.has(node.connectionId) : true;
});
} }
return nodes; return nodes;
+12 -2
View File
@@ -14,6 +14,8 @@ export interface SidebarSearchMatch {
score: number; score: number;
} }
export type SidebarLabelMatcher = (label: string) => SidebarSearchMatch | null;
function isWordBoundary(text: string, index: number): boolean { function isWordBoundary(text: string, index: number): boolean {
if (index === 0) return true; if (index === 0) return true;
const prev = text[index - 1]; const prev = text[index - 1];
@@ -48,10 +50,9 @@ function matchesSubsequence(text: string, query: string): boolean {
return j === query.length; 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; if (!query) return null;
const regex = parseSlashDelimitedRegexQuery(query);
if (regex) return regex.test(label) ? { kind: "regex", score: 95 } : null; if (regex) return regex.test(label) ? { kind: "regex", score: 95 } : null;
if (label === query) return { kind: "exact", score: 100 }; if (label === query) return { kind: "exact", score: 100 };
@@ -63,3 +64,12 @@ export function matchSidebarLabel(label: string, query: string): SidebarSearchMa
return null; 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));
}
+23 -4
View File
@@ -1,5 +1,5 @@
import type { TreeNode, TreeNodeType } from "@/types/database"; 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"]); const preserveMatchedSubtreeTypes = new Set(["database", "schema", "table", "view"]);
@@ -19,13 +19,22 @@ export function filterSidebarTree(
query: string, query: string,
collapsedIds: ReadonlySet<string>, collapsedIds: ReadonlySet<string>,
searchableNodeTypes?: ReadonlySet<TreeNodeType>, searchableNodeTypes?: ReadonlySet<TreeNodeType>,
): TreeNode[] {
return filterSidebarTreeWithMatcher(nodes, createSidebarLabelMatcher(query), collapsedIds, searchableNodeTypes);
}
function filterSidebarTreeWithMatcher(
nodes: TreeNode[],
matchLabel: SidebarLabelMatcher,
collapsedIds: ReadonlySet<string>,
searchableNodeTypes?: ReadonlySet<TreeNodeType>,
): TreeNode[] { ): TreeNode[] {
const filteredNodes: { node: TreeNode; score: number }[] = []; const filteredNodes: { node: TreeNode; score: number }[] = [];
for (const node of nodes) { for (const node of nodes) {
if (node.type === "object-browser" && node.hiddenChildren) { if (node.type === "object-browser" && node.hiddenChildren) {
const matches = 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); .filter((match) => match.score > 0);
filteredNodes.push(...matches); filteredNodes.push(...matches);
continue; continue;
@@ -33,12 +42,12 @@ export function filterSidebarTree(
const label = normalizedLabel(node); const label = normalizedLabel(node);
const canSelfMatch = !searchableNodeTypes || searchableNodeTypes.has(node.type); 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 preservesSubtree = !!selfMatch && preserveMatchedSubtreeTypes.has(node.type);
const filteredChildren = preservesSubtree const filteredChildren = preservesSubtree
? node.children ? node.children
: node.children : node.children
? filterSidebarTree(node.children, query, collapsedIds, searchableNodeTypes) ? filterSidebarTreeWithMatcher(node.children, matchLabel, collapsedIds, searchableNodeTypes)
: undefined; : undefined;
if (selfMatch || (filteredChildren && filteredChildren.length > 0)) { if (selfMatch || (filteredChildren && filteredChildren.length > 0)) {
@@ -61,3 +70,13 @@ export function filterSidebarTree(
filteredNodes.sort((a, b) => b.score - a.score); filteredNodes.sort((a, b) => b.score - a.score);
return filteredNodes.map((match) => match.node); return filteredNodes.map((match) => match.node);
} }
export function filterSidebarSearchRootsByConnectionState(
nodes: TreeNode[],
connectedIds: ReadonlySet<string>,
): TreeNode[] {
return nodes.filter((node) => {
if (node.type === "connection-group" || node.type === "connection") return true;
return node.connectionId ? connectedIds.has(node.connectionId) : true;
});
}
+31 -1
View File
@@ -1,6 +1,9 @@
import { strict as assert } from "node:assert"; import { strict as assert } from "node:assert";
import test from "node:test"; 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"; import type { TreeNode } from "../../apps/desktop/src/types/database.ts";
test("preserves loaded table children when the table itself matches search", () => { 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); 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"],
);
});