fix(sidebar): restore tree node collapsing

This commit is contained in:
zipg 2026-07-30 14:48:18 +08:00 committed by GitHub
parent f82749163d
commit 24d7dc48ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 5 deletions

View File

@ -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"

View File

@ -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;
}

View File

@ -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<FlatTreeNode\[]>/);
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", () => {