fix(sidebar): 修复搜索筛选树分组折叠在 scope-only 过滤下空操作
* 🐛 fix(sidebar): 修复搜索筛选树的分组折叠与重复点击切换
- 在保留搜索子树时应用临时折叠状态,避免空分组重新展开
- 仅在搜索筛选期间记录节点折叠操作
- 允许分组标签双击后的连续点击正常切换展开状态
- 补充侧边栏搜索及节点点击行为测试
* fix(sidebar): collapseAllTreeNodes 按 scope-only 过滤填充 searchCollapsedIds
---------
Co-authored-by: t8y2 <t8y2@users.noreply.github.com>
Co-authored-by: skyler <1156263951@qq.com>
This commit is contained in:
parent
b58c221e16
commit
fa6540b739
|
|
@ -223,7 +223,6 @@ function collectExpandedObjectSearchTargets(node: TreeNode, tasks: Promise<void>
|
|||
}
|
||||
}
|
||||
|
||||
const isSearching = computed(() => !!deferredSearchQuery.value);
|
||||
const sidebarFilterGuards = computed(() => resolveSidebarFilterGuards(showConnectedConnectionsOnly.value, searchQuery.value, hasSearchScopeFilter.value));
|
||||
// Connected-only filtering changes only root visibility, so descendant-local
|
||||
// features stay available while operations requiring the full root list pause.
|
||||
|
|
@ -1178,7 +1177,7 @@ function findSchemaNode(nodes: TreeNode[], connId: string, database: string, sch
|
|||
}
|
||||
|
||||
function onSearchToggle(node: TreeNode) {
|
||||
if (!isSearching.value || !node.children) return;
|
||||
if (!isTreeSearchFiltering.value || !node.children) return;
|
||||
const next = new Set(searchCollapsedIds.value);
|
||||
if (node.isExpanded) next.add(node.id);
|
||||
else next.delete(node.id);
|
||||
|
|
@ -1441,7 +1440,9 @@ watch(sidebarTableNameFilterOpen, (open) => {
|
|||
|
||||
function collapseAllTreeNodes() {
|
||||
store.collapseAllTreeNodes();
|
||||
if (isSearching.value) {
|
||||
// 与 onSearchToggle 一致:scope-only 过滤也要填充 searchCollapsedIds,
|
||||
// 否则 filteredNodes 会用空集合把所有分组重建成展开态,“全部折叠”空操作。
|
||||
if (isTreeSearchFiltering.value) {
|
||||
searchCollapsedIds.value = new Set(flatTreeIndex.value.expandableNodeIds);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,7 +751,7 @@ function runRowClickAction(clickDetail: number) {
|
|||
return;
|
||||
}
|
||||
const action = treeNodeRowAction(node.type, canExpand.value, settingsStore.editorSettings.sidebarActivation);
|
||||
if (!shouldRunTreeNodeRowAction(action, clickDetail)) return;
|
||||
if (!shouldRunTreeNodeRowAction(action, clickDetail, isGroupLabel(node))) return;
|
||||
if (action === "open-data") {
|
||||
scheduleOpenData(node);
|
||||
} else if (action === "open-source") {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,19 @@ export function reuseLiveSidebarTreeNodes(indexedNodes: TreeNode[], liveNodes: r
|
|||
return indexedNodes.map((node) => liveNodesById.get(node.id) ?? node);
|
||||
}
|
||||
|
||||
function applySearchCollapsedState(node: TreeNode, collapsedIds: ReadonlySet<string>): TreeNode {
|
||||
const children = node.children?.map((child) => applySearchCollapsedState(child, collapsedIds));
|
||||
const childrenChanged = children?.some((child, index) => child !== node.children?.[index]) ?? false;
|
||||
const collapsed = collapsedIds.has(node.id);
|
||||
if (!collapsed && !childrenChanged) return node;
|
||||
|
||||
return {
|
||||
...node,
|
||||
children: childrenChanged ? children : node.children,
|
||||
isExpanded: collapsed ? false : node.isExpanded,
|
||||
};
|
||||
}
|
||||
|
||||
function filterSidebarTreeWithMatcher(nodes: TreeNode[], matchLabel: SidebarLabelMatcher | undefined, collapsedIds: ReadonlySet<string>, searchableNodeTypes?: ReadonlySet<TreeNodeType>): TreeNode[] {
|
||||
const filteredNodes: { node: TreeNode; score: number }[] = [];
|
||||
|
||||
|
|
@ -57,7 +70,7 @@ function filterSidebarTreeWithMatcher(nodes: TreeNode[], matchLabel: SidebarLabe
|
|||
// A type-matched table keeps its loaded detail groups after the text query
|
||||
// is cleared instead of being rebuilt with an empty filtered child list.
|
||||
const preservesTypeMatchedTable = !matchLabel && !!selfMatch && node.type === "table";
|
||||
const filteredChildren = preservesSubtree ? node.children : node.children ? filterSidebarTreeWithMatcher(node.children, matchLabel, collapsedIds, searchableNodeTypes) : undefined;
|
||||
const filteredChildren = preservesSubtree ? node.children?.map((child) => applySearchCollapsedState(child, collapsedIds)) : node.children ? filterSidebarTreeWithMatcher(node.children, matchLabel, collapsedIds, searchableNodeTypes) : undefined;
|
||||
|
||||
if (selfMatch || (filteredChildren && filteredChildren.length > 0)) {
|
||||
if (!node.children || preservesTypeMatchedTable) {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ export function treeNodeRowAction(type: TreeNodeType, canExpand: boolean, activa
|
|||
return "none";
|
||||
}
|
||||
|
||||
export function shouldRunTreeNodeRowAction(action: TreeNodeRowAction, clickDetail: number): boolean {
|
||||
export function shouldRunTreeNodeRowAction(action: TreeNodeRowAction, clickDetail: number, allowRepeatedToggle = false): boolean {
|
||||
if (action === "toggle" && allowRepeatedToggle) return true;
|
||||
// Double-clicks emit a second click before dblclick; leave that event to the
|
||||
// double-click handler so expandable database rows do not toggle first.
|
||||
return action !== "none" && clickDetail <= 1;
|
||||
|
|
|
|||
|
|
@ -212,6 +212,45 @@ test("preserves loaded children when the connection itself matches search", () =
|
|||
assert.equal(filtered[0]?.children?.[0]?.children?.[0]?.label, "products");
|
||||
});
|
||||
|
||||
test("temporarily collapses an empty object group within a preserved search subtree", () => {
|
||||
const tablesGroup: TreeNode = {
|
||||
id: "conn:1:inventory:__tables",
|
||||
label: "tree.tables",
|
||||
type: "group-tables",
|
||||
connectionId: "conn:1",
|
||||
database: "inventory",
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
const nodes: TreeNode[] = [
|
||||
{
|
||||
id: "conn:1",
|
||||
label: "local-mysql",
|
||||
type: "connection",
|
||||
connectionId: "conn:1",
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: "conn:1:inventory",
|
||||
label: "inventory",
|
||||
type: "database",
|
||||
connectionId: "conn:1",
|
||||
database: "inventory",
|
||||
isExpanded: true,
|
||||
children: [tablesGroup],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const filtered = filterSidebarTree(nodes, "local", new Set([tablesGroup.id]));
|
||||
const filteredGroup = filtered[0]?.children?.[0]?.children?.[0];
|
||||
|
||||
assert.equal(filteredGroup?.isExpanded, false);
|
||||
assert.equal(tablesGroup.isExpanded, true);
|
||||
assert.equal(filterSidebarTree(nodes, "local", new Set())[0]?.children?.[0]?.children?.[0]?.isExpanded, true);
|
||||
});
|
||||
|
||||
test("matches table comments during sidebar search", () => {
|
||||
const nodes: TreeNode[] = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -73,10 +73,12 @@ test("async tree expansion does not restore a stale rendered clone state", () =>
|
|||
assert.equal(liveDatabase.isExpanded, true);
|
||||
});
|
||||
|
||||
test("local table search preserves live expansion state", () => {
|
||||
test("tree filters retain a temporary expansion state", () => {
|
||||
assert.match(connectionTree, /return \{ \.\.\.node, children: matchingChildren \};/);
|
||||
assert.doesNotMatch(connectionTree, /children: matchingChildren,\s*isExpanded:\s*true/);
|
||||
assert.match(connectionTree, /function onSearchToggle\(node: TreeNode\) \{\s*if \(!isTreeSearchFiltering\.value \|\| !node\.children\) return;/);
|
||||
assert.match(connectionTree, /function onNodeToggled\(node: TreeNode, wasExpanded: boolean\) \{\s*if \(isTreeSearchFiltering\.value\) return;\s*syncSidebarTreeNodeExpansion\(store\.treeNodes, node, !wasExpanded\)/);
|
||||
assert.match(runtimeHost, /shouldRunTreeNodeRowAction\(action, clickDetail, isGroupLabel\(node\)\)/);
|
||||
});
|
||||
|
||||
test("tree rebuilds keep a context menu only while its target row remains visible", () => {
|
||||
|
|
|
|||
|
|
@ -68,10 +68,12 @@ test("document browser helper covers Mongo collections and GridFS buckets", () =
|
|||
assert.equal(isDocumentBrowserTreeNode("redis-db"), false);
|
||||
});
|
||||
|
||||
test("double-click follow-up clicks do not run row actions", () => {
|
||||
test("double-click follow-up clicks do not repeat side-effecting row actions", () => {
|
||||
assert.equal(shouldRunTreeNodeRowAction("toggle", 1), true);
|
||||
assert.equal(shouldRunTreeNodeRowAction("toggle", 2), false);
|
||||
assert.equal(shouldRunTreeNodeRowAction("toggle", 3), false);
|
||||
assert.equal(shouldRunTreeNodeRowAction("toggle", 2, true), true);
|
||||
assert.equal(shouldRunTreeNodeRowAction("toggle", 3, true), true);
|
||||
assert.equal(shouldRunTreeNodeRowAction("open-data", 1), true);
|
||||
assert.equal(shouldRunTreeNodeRowAction("open-data", 2), false);
|
||||
assert.equal(shouldRunTreeNodeRowAction("open-source", 1), true);
|
||||
|
|
|
|||
Loading…
Reference in New Issue