fix(sidebar): preserve objects for ancestor search matches

Closes #5613
This commit is contained in:
t8y2 committed 2026-08-13 12:15:46 +00:00
1 parent cce36ae058
commit e0a7bc996d
9 files changed
+282 -41

No files matched your search

@@ -4,7 +4,13 @@ import { objectTypesForGroupNode } from "@/lib/table/tableTree";
export interface SidebarObjectGroupLoaders {
loadTriggers(connectionId: string, database: string, table: string, schema?: string, nodeId?: string, catalog?: string): Promise<void>;
loadObjectGroupChildren(node: TreeNode): Promise<void>;
loadObjectGroupChildren(node: TreeNode, options?: SidebarObjectGroupLoadOptions): Promise<void>;
}
export interface SidebarObjectGroupLoadOptions {
searchFilter?: string;
allowGlobalSearchMismatch?: boolean;
expectedSidebarSearchQuery?: string;
}
/**
@@ -12,13 +18,13 @@ export interface SidebarObjectGroupLoaders {
* Table trigger groups are special because they are scoped to one table;
* schema-level trigger groups are regular database object groups.
*/
export async function loadSidebarObjectGroup(node: TreeNode, loaders: SidebarObjectGroupLoaders): Promise<boolean> {
export async function loadSidebarObjectGroup(node: TreeNode, loaders: SidebarObjectGroupLoaders, options?: SidebarObjectGroupLoadOptions): Promise<boolean> {
if (node.type === "group-triggers" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await loaders.loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
return true;
}
if (!objectTypesForGroupNode(node.type)) return false;
await loaders.loadObjectGroupChildren(node);
await loaders.loadObjectGroupChildren(node, options);
return true;
}
@@ -22,6 +22,36 @@ function normalizedLabel(node: TreeNode): string {
return node.label;
}
function findNodePath(nodes: readonly TreeNode[], targetNodeId: string, ancestors: readonly TreeNode[] = []): readonly TreeNode[] | undefined {
for (const node of nodes) {
const path = [...ancestors, node];
if (node.id === targetNodeId) return path;
if (node.children) {
const childPath = findNodePath(node.children, targetNodeId, path);
if (childPath) return childPath;
}
}
return undefined;
}
function nodePreservesSearchSubtree(node: TreeNode, matchLabel: SidebarLabelMatcher, searchableNodeTypes?: ReadonlySet<TreeNodeType>): boolean {
if (searchableNodeTypes && !searchableNodeTypes.has(node.type)) return false;
return preserveMatchedSubtreeTypes.has(node.type) && !!bestMatch(matchLabel, normalizedLabel(node), node.comment, node.searchAliases);
}
export function createSidebarSearchSubtreePreserver(query: string, searchableNodeTypes?: ReadonlySet<TreeNodeType>): (node: TreeNode) => boolean {
const matchLabel = query ? createSidebarLabelMatcher(query) : undefined;
return (node) => !!matchLabel && nodePreservesSearchSubtree(node, matchLabel, searchableNodeTypes);
}
export function resolveSidebarObjectSearchFilter(nodes: readonly TreeNode[], targetNodeId: string, query: string, searchableNodeTypes?: ReadonlySet<TreeNodeType>): string {
if (!query) return query;
const matchLabel = createSidebarLabelMatcher(query);
const path = findNodePath(nodes, targetNodeId);
const preservesSubtree = path?.some((node) => nodePreservesSearchSubtree(node, matchLabel, searchableNodeTypes));
return preservesSubtree ? "" : query;
}
export function filterSidebarTree(nodes: TreeNode[], query: string, collapsedIds: ReadonlySet<string>, searchableNodeTypes?: ReadonlySet<TreeNodeType>): TreeNode[] {
const matchLabel = query ? createSidebarLabelMatcher(query) : undefined;
if (!matchLabel && searchableNodeTypes === undefined) return nodes;
@@ -4,6 +4,7 @@ import type { TreeNode } from "@/types/database";
export interface SidebarTreeContext {
getVisibleNodes: () => TreeNode[];
getVisibleNodeIndex: (id: string) => number;
getTreeLoadSearchOptions?: (node: TreeNode) => { searchFilter: string; allowGlobalSearchMismatch: true; expectedSidebarSearchQuery: string } | undefined;
setTableSearchQuery?: (parentNodeId: string, query: string, local: boolean) => void;
refreshTableSearchIndex?: (parentNodeId: string) => void;
registerPasteHandler?: (nodeId: string, callback: () => void) => () => void;