mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
fix(sidebar): preserve searched table children
This commit is contained in:
@@ -4,7 +4,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { Search, X, ListFilter, Check, FolderPlus } from "lucide-vue-next";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { matchSidebarLabel } from "@/lib/sidebarSearch";
|
||||
import { filterSidebarTree } from "@/lib/sidebarSearchTree";
|
||||
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
|
||||
import {
|
||||
SIDEBAR_TREE_ROW_HEIGHT,
|
||||
@@ -90,54 +90,6 @@ function clearTypeFilter() {
|
||||
selectedTypes.value = [];
|
||||
}
|
||||
|
||||
const normalizedLabelCache = new WeakMap<TreeNode, { label: string; normalized: string }>();
|
||||
|
||||
function normalizedLabel(node: TreeNode): string {
|
||||
const cached = normalizedLabelCache.get(node);
|
||||
if (cached?.label === node.label) return cached.normalized;
|
||||
|
||||
const normalized = node.label.toLowerCase();
|
||||
normalizedLabelCache.set(node, { label: node.label, normalized });
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function filterTree(nodes: TreeNode[], q: string): 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), q)?.score ?? 0 }))
|
||||
.filter((m) => m.score > 0);
|
||||
filteredNodes.push(...matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = normalizedLabel(node);
|
||||
const selfMatch = matchSidebarLabel(label, q);
|
||||
const filteredChildren = node.children ? filterTree(node.children, q) : undefined;
|
||||
|
||||
if (selfMatch || (filteredChildren && filteredChildren.length > 0)) {
|
||||
if (!node.children) {
|
||||
filteredNodes.push({ node, score: selfMatch?.score ?? 0 });
|
||||
} else {
|
||||
const children = filteredChildren ?? [];
|
||||
filteredNodes.push({
|
||||
node: {
|
||||
...node,
|
||||
children,
|
||||
isExpanded: children.length > 0 && !searchCollapsedIds.value.has(node.id),
|
||||
},
|
||||
score: selfMatch?.score ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filteredNodes.sort((a, b) => b.score - a.score);
|
||||
return filteredNodes.map((m) => m.node);
|
||||
}
|
||||
|
||||
function matchesType(node: TreeNode): boolean {
|
||||
if (node.type === "connection-group") {
|
||||
return node.children?.some(matchesType) ?? false;
|
||||
@@ -163,7 +115,7 @@ const filteredNodes = computed(() => {
|
||||
|
||||
const q = deferredSearchQuery.value;
|
||||
if (q) {
|
||||
nodes = filterTree(nodes, q);
|
||||
nodes = filterSidebarTree(nodes, q, searchCollapsedIds.value);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { matchSidebarLabel } from "@/lib/sidebarSearch";
|
||||
|
||||
const preserveMatchedSubtreeTypes = new Set(["table", "view"]);
|
||||
|
||||
const normalizedLabelCache = new WeakMap<TreeNode, { label: string; normalized: string }>();
|
||||
|
||||
function normalizedLabel(node: TreeNode): string {
|
||||
const cached = normalizedLabelCache.get(node);
|
||||
if (cached?.label === node.label) return cached.normalized;
|
||||
|
||||
const normalized = node.label.toLowerCase();
|
||||
normalizedLabelCache.set(node, { label: node.label, normalized });
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function filterSidebarTree(nodes: TreeNode[], query: string, collapsedIds: ReadonlySet<string>): 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 }))
|
||||
.filter((match) => match.score > 0);
|
||||
filteredNodes.push(...matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = normalizedLabel(node);
|
||||
const selfMatch = matchSidebarLabel(label, query);
|
||||
const preservesSubtree = !!selfMatch && preserveMatchedSubtreeTypes.has(node.type);
|
||||
const filteredChildren = preservesSubtree
|
||||
? node.children
|
||||
: node.children
|
||||
? filterSidebarTree(node.children, query, collapsedIds)
|
||||
: undefined;
|
||||
|
||||
if (selfMatch || (filteredChildren && filteredChildren.length > 0)) {
|
||||
if (!node.children) {
|
||||
filteredNodes.push({ node, score: selfMatch?.score ?? 0 });
|
||||
} else {
|
||||
const children = filteredChildren ?? [];
|
||||
filteredNodes.push({
|
||||
node: {
|
||||
...node,
|
||||
children,
|
||||
isExpanded: children.length > 0 && !collapsedIds.has(node.id),
|
||||
},
|
||||
score: selfMatch?.score ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filteredNodes.sort((a, b) => b.score - a.score);
|
||||
return filteredNodes.map((match) => match.node);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import { filterSidebarTree } from "../src/lib/sidebarSearchTree.ts";
|
||||
import type { TreeNode } from "../src/types/database.ts";
|
||||
|
||||
test("preserves loaded table children when the table itself matches search", () => {
|
||||
const nodes: TreeNode[] = [
|
||||
{
|
||||
id: "conn:db",
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: "conn",
|
||||
database: "app",
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: "conn:db:orders",
|
||||
label: "orders",
|
||||
type: "table",
|
||||
connectionId: "conn",
|
||||
database: "app",
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: "conn:db:orders:__columns",
|
||||
label: "tree.columns",
|
||||
type: "group-columns",
|
||||
connectionId: "conn",
|
||||
database: "app",
|
||||
tableName: "orders",
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: "conn:db:orders:__columns:id",
|
||||
label: "id",
|
||||
type: "column",
|
||||
connectionId: "conn",
|
||||
database: "app",
|
||||
tableName: "orders",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const filtered = filterSidebarTree(nodes, "orders", new Set());
|
||||
|
||||
const table = filtered[0]?.children?.[0];
|
||||
assert.equal(table?.label, "orders");
|
||||
assert.equal(table?.children?.[0]?.label, "tree.columns");
|
||||
assert.equal(table?.children?.[0]?.children?.[0]?.label, "id");
|
||||
});
|
||||
Reference in New Issue
Block a user