diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index e422abecf7..7ee82b69d3 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -34,7 +34,17 @@ import { pruneTreeSelectionToVisibleNodeIds } from "@/lib/sidebar/sidebarTreeSel import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch"; import { isInternalDorisCatalog, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport"; import { connectionObjectTreeNodeSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; -import { activeTabSidebarTarget, findSidebarNodeForActiveTab, findSidebarNodeForTarget, findNodePathForTarget, scrollTopForSidebarNode, shouldScrollActiveSidebarSelection, type ActiveTabSidebarTarget, type SidebarNodeScrollAlign } from "@/lib/sidebar/sidebarActiveTabTarget"; +import { + activeTabSidebarTarget, + findSidebarConnectionNode, + findSidebarNodeForActiveTab, + findSidebarNodeForTarget, + findNodePathForTarget, + scrollTopForSidebarNode, + shouldScrollActiveSidebarSelection, + type ActiveTabSidebarTarget, + type SidebarNodeScrollAlign, +} from "@/lib/sidebar/sidebarActiveTabTarget"; import { findLoadedTableTargetForCandidate, queryContextTargetFromCandidate, queryCursorTableCandidate, type QueryCursorTableCandidate } from "@/lib/sql/queryCursorTableTarget"; import { createFlatTreeIndex, SIDEBAR_TREE_ROW_HEIGHT, SIDEBAR_TREE_PRERENDER_COUNT, SIDEBAR_TREE_SCROLL_BUFFER, flattenTree, shouldVirtualizeFlatTree, type FlatTreeNode } from "@/composables/useFlatTree"; import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext"; @@ -1414,7 +1424,7 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?: const loadOptions = force ? { force: true } : undefined; // Ensure databases are loaded under the connection - const connNode = store.treeNodes.find((n) => n.id === connId); + const connNode = findSidebarConnectionNode(store.treeNodes, connId); if (connNode && (force || !connNode.children || connNode.children.length === 0)) { try { if (config.db_type === "redis") { diff --git a/apps/desktop/src/lib/sidebar/sidebarActiveTabTarget.ts b/apps/desktop/src/lib/sidebar/sidebarActiveTabTarget.ts index cee76136e4..88f36a3de0 100644 --- a/apps/desktop/src/lib/sidebar/sidebarActiveTabTarget.ts +++ b/apps/desktop/src/lib/sidebar/sidebarActiveTabTarget.ts @@ -86,6 +86,22 @@ export type ActiveTabSidebarTarget = savedSqlId: string; }; +/** + * Resolve a connection root without walking its potentially large object tree. + * Grouped connections live below connection-group nodes, while a connection + * id can also appear on descendants, so only group children are traversed. + */ +export function findSidebarConnectionNode(nodes: readonly TreeNode[], connectionId: string): TreeNode | null { + for (const node of nodes) { + if (node.type === "connection" && node.id === connectionId) return node; + if (node.type === "connection-group" && node.children) { + const found = findSidebarConnectionNode(node.children, connectionId); + if (found) return found; + } + } + return null; +} + export function activeTabSidebarTarget(tab: QueryTab | undefined | null): ActiveTabSidebarTarget | null { if (!tab) return null; diff --git a/packages/app-tests/sidebarActiveTabTarget.test.ts b/packages/app-tests/sidebarActiveTabTarget.test.ts index 4a5b879f04..09c507b784 100644 --- a/packages/app-tests/sidebarActiveTabTarget.test.ts +++ b/packages/app-tests/sidebarActiveTabTarget.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import { test } from "vitest"; -import { activeTabSidebarTarget, findNodePathForTarget, findSidebarNodeForActiveTab, scrollTopForSidebarNode, shouldScrollActiveSidebarSelection } from "../../apps/desktop/src/lib/sidebar/sidebarActiveTabTarget.ts"; +import { activeTabSidebarTarget, findNodePathForTarget, findSidebarConnectionNode, findSidebarNodeForActiveTab, scrollTopForSidebarNode, shouldScrollActiveSidebarSelection } from "../../apps/desktop/src/lib/sidebar/sidebarActiveTabTarget.ts"; import type { FlatTreeNode } from "../../apps/desktop/src/composables/useFlatTree.ts"; import type { QueryTab, TreeNode } from "../../apps/desktop/src/types/database.ts"; @@ -8,6 +8,65 @@ function flat(node: TreeNode, depth = 0): FlatTreeNode { return { id: node.id, node, depth, type: node.type }; } +test("findSidebarConnectionNode resolves grouped roots without scanning descendants", () => { + const groupedConnection: TreeNode = { + id: "conn-grouped", + label: "Grouped", + type: "connection", + connectionId: "conn-grouped", + children: [ + { + id: "conn-grouped:app", + label: "app", + type: "database", + connectionId: "conn-grouped", + database: "app", + }, + ], + }; + const nestedGroups: TreeNode[] = [ + { + id: "group-outer", + label: "Outer", + type: "connection-group", + children: [ + { + id: "group-inner", + label: "Inner", + type: "connection-group", + children: [groupedConnection], + }, + ], + }, + ]; + + assert.equal(findSidebarConnectionNode(nestedGroups, "conn-grouped"), groupedConnection); + assert.equal(findSidebarConnectionNode(nestedGroups, "missing"), null); + assert.equal( + findSidebarConnectionNode( + [ + { + id: "conn-root", + label: "Root", + type: "connection", + connectionId: "conn-root", + children: [ + { + id: "conn-nested-decoy", + label: "Decoy", + type: "connection", + connectionId: "conn-grouped", + }, + ], + }, + ...nestedGroups, + ], + "conn-grouped", + ), + groupedConnection, + ); +}); + test("findNodePathForTarget handles loaded, unloaded, and MySQL schema fallback trees", () => { const cases = [ {