fix(sidebar): keep connection search results visible
This commit is contained in:
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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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"],
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user