From 24d7dc48ad925c91faea7c02417c3ddba0b0f1ae Mon Sep 17 00:00:00 2001 From: zipg Date: Thu, 30 Jul 2026 14:48:18 +0800 Subject: [PATCH] fix(sidebar): restore tree node collapsing --- .../src/components/sidebar/ConnectionTree.vue | 14 +++++--- .../src/lib/sidebar/sidebarTreeExpansion.ts | 9 +++++ .../app-tests/sidebarTreeAffordances.test.ts | 35 +++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/src/lib/sidebar/sidebarTreeExpansion.ts diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 10942d530..53b869d45 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -40,6 +40,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D import { codeMirrorSqlDialect } from "@/lib/database/jdbcDialect"; import { sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter"; import { createSidebarActionTarget, findSidebarActionTarget, matchesSidebarActionTarget, type SidebarActionTarget } from "@/lib/sidebar/sidebarActionTarget"; +import { syncSidebarTreeNodeExpansion } from "@/lib/sidebar/sidebarTreeExpansion"; import type { SidebarDangerDialogRequest } from "@/lib/sidebar/sidebarDangerDialog"; import { resetSidebarTreeDialogState } from "./sidebarTreeDialogState"; import { SidebarDangerConfirmDialog, SidebarDdlViewDialog, SidebarObjectSourceDialog, SidebarProcedureExecutionDialog, SidebarVisibleDatabasesDialog, SidebarVisibleSchemasDialog } from "./sidebarAsyncDialogs"; @@ -387,7 +388,7 @@ function filterLocallySearchedTables(nodes: TreeNode[]): TreeNode[] { children, ) : children.filter((child) => localTableSearchChildTypes.has(child.type) && !!matchSidebarLabel(child.label.toLowerCase(), query.toLowerCase())); - return { ...node, children: matchingChildren, isExpanded: true }; + return { ...node, children: matchingChildren }; }); } @@ -670,10 +671,7 @@ watch(flatNodes, (nodes) => { } } stickyScrollTop.value = 0; - void nextTick(() => { - treeScrollerRef.value?.forceUpdate(true); - scheduleSidebarScrollMetricsUpdate(); - }); + void nextTick(scheduleSidebarScrollMetricsUpdate); }); const sidebarTreeOverflowClass = computed(() => (settingsStore.editorSettings.sidebarAllowHorizontalScroll ? "overflow-x-auto sidebar-tree-horizontal-scroll" : "overflow-x-hidden")); @@ -1186,6 +1184,11 @@ function onSearchToggle(node: TreeNode) { searchCollapsedIds.value = next; } +function onNodeToggled(node: TreeNode) { + if (isTreeSearchFiltering.value) return; + syncSidebarTreeNodeExpansion(store.treeNodes, node); +} + function openSidebarContextMenu(event: MouseEvent, node: TreeNode, openContextMenu: (event: MouseEvent, itemsOverride?: ContextMenuItem[]) => void) { const items = sidebarTreeRuntime.buildContextMenu(node); sidebarContextMenuTarget.value = createSidebarActionTarget(node); @@ -1666,6 +1669,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes }); :node="sidebarTreeRuntimeInitialNode" :depth="0" @search-toggle="onSearchToggle" + @node-toggled="onNodeToggled" @open-ddl="openSidebarDdl" @open-object-source="openSidebarObjectSource" @open-procedure="openSidebarProcedure" diff --git a/apps/desktop/src/lib/sidebar/sidebarTreeExpansion.ts b/apps/desktop/src/lib/sidebar/sidebarTreeExpansion.ts new file mode 100644 index 000000000..06eeef538 --- /dev/null +++ b/apps/desktop/src/lib/sidebar/sidebarTreeExpansion.ts @@ -0,0 +1,9 @@ +import type { TreeNode } from "@/types/database"; +import { findSidebarActionTarget } from "@/lib/sidebar/sidebarActionTarget"; + +export function syncSidebarTreeNodeExpansion(nodes: readonly TreeNode[], renderedNode: TreeNode): boolean { + const liveNode = findSidebarActionTarget(nodes, renderedNode); + if (!liveNode || liveNode === renderedNode || liveNode.isExpanded === renderedNode.isExpanded) return false; + liveNode.isExpanded = renderedNode.isExpanded; + return true; +} diff --git a/packages/app-tests/sidebarTreeAffordances.test.ts b/packages/app-tests/sidebarTreeAffordances.test.ts index 750c1cb5b..dbbfb75df 100644 --- a/packages/app-tests/sidebarTreeAffordances.test.ts +++ b/packages/app-tests/sidebarTreeAffordances.test.ts @@ -1,6 +1,8 @@ import { strict as assert } from "node:assert"; import { readFileSync } from "node:fs"; import { test } from "vitest"; +import { syncSidebarTreeNodeExpansion } from "../../apps/desktop/src/lib/sidebar/sidebarTreeExpansion.ts"; +import type { TreeNode } from "../../apps/desktop/src/types/database.ts"; const treeItem = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8"); const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8"); @@ -24,6 +26,39 @@ test("complex tree changes retain the full rebuild fallback", () => { assert.match(connectionTree, /const flatNodes = computed/); assert.match(connectionTree, /flattenTree\(filteredNodes\.value\)/); assert.match(connectionTree, /watch\(flatNodes,/); + assert.doesNotMatch(connectionTree, /treeScrollerRef\.value\?\.(?:forceUpdate|updateVisibleItems)/); + assert.match(connectionTree, /@node-toggled="onNodeToggled"/); +}); + +test("tree toggles synchronize filtered node clones with the live sidebar tree", () => { + const expandedConnection: TreeNode = { + id: "connection-1", + label: "Connection 1", + type: "connection", + connectionId: "connection-1", + isExpanded: true, + }; + const collapsedClone: TreeNode = { ...expandedConnection, isExpanded: false }; + const collapsedConnection: TreeNode = { + id: "connection-2", + label: "Connection 2", + type: "connection", + connectionId: "connection-2", + isExpanded: false, + }; + const expandedClone: TreeNode = { ...collapsedConnection, isExpanded: true }; + + assert.equal(syncSidebarTreeNodeExpansion([expandedConnection], collapsedClone), true); + assert.equal(expandedConnection.isExpanded, false); + assert.equal(syncSidebarTreeNodeExpansion([collapsedConnection], expandedClone), true); + assert.equal(collapsedConnection.isExpanded, true); + assert.equal(syncSidebarTreeNodeExpansion([expandedConnection], expandedConnection), false); +}); + +test("local table search preserves live expansion state", () => { + assert.match(connectionTree, /return \{ \.\.\.node, children: matchingChildren \};/); + assert.doesNotMatch(connectionTree, /children: matchingChildren,\s*isExpanded:\s*true/); + assert.match(connectionTree, /function onNodeToggled\(node: TreeNode\) \{\s*if \(isTreeSearchFiltering\.value\) return;\s*syncSidebarTreeNodeExpansion/); }); test("tree rebuilds keep a context menu only while its target row remains visible", () => {