fix(sidebar): avoid synchronous selection pruning
This commit is contained in:
4 files changed
+59
-12
No files matched your search
@@ -30,7 +30,7 @@ import { copyNameForTreeNode, objectSourceTargetForTreeNode } from "@/lib/sideba
|
||||
import { supportsTypeObjectSource } from "@/lib/database/databaseObjectCapabilities";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { connectionPasteTargetGroupId, copySelectedConnectionsToClipboards, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection";
|
||||
import { pruneTreeSelectionToVisibleNodes } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { pruneTreeSelectionToVisibleNodeIds } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch";
|
||||
import { isInternalDorisCatalog, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
|
||||
import { connectionObjectTreeNodeSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
@@ -807,11 +807,13 @@ const selectableVisibleNodes = computed<TreeNode[]>(() => flatTreeIndex.value.se
|
||||
const selectableVisibleNodeIndexById = computed(() => flatTreeIndex.value.selectableVisibleNodeIndexById);
|
||||
const useVirtualTree = computed(() => shouldVirtualizeFlatTree(flatNodes.value.length));
|
||||
const activeTab = computed(() => queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId));
|
||||
const hasTreeMultiSelection = computed(() => store.connectionMultiSelectActive || store.selectedTreeNodeIds.length > 1);
|
||||
|
||||
watch(
|
||||
visibleNodes,
|
||||
(nodes) => {
|
||||
const next = pruneTreeSelectionToVisibleNodes(nodes, {
|
||||
() => (hasTreeMultiSelection.value ? flatTreeIndex.value.nodeById : null),
|
||||
(visibleNodeIds) => {
|
||||
if (!visibleNodeIds) return;
|
||||
const next = pruneTreeSelectionToVisibleNodeIds(visibleNodeIds, {
|
||||
nodeIds: store.selectedTreeNodeIds,
|
||||
activeNodeId: store.selectedTreeNodeId,
|
||||
anchorNodeId: store.treeSelectionAnchorId,
|
||||
@@ -823,7 +825,7 @@ watch(
|
||||
store.treeSelectionAnchorId = next.anchorNodeId;
|
||||
if (!next.nodeIds.length) store.connectionMultiSelectActive = false;
|
||||
},
|
||||
{ flush: "sync" },
|
||||
{ flush: "post" },
|
||||
);
|
||||
|
||||
// --- Sticky database header ---
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const connectionTreeSource = readFileSync(new URL("../ConnectionTree.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("ConnectionTree selection pruning", () => {
|
||||
it("does not track the visible tree outside multi-selection", () => {
|
||||
expect(connectionTreeSource).toContain("store.connectionMultiSelectActive || store.selectedTreeNodeIds.length > 1");
|
||||
expect(connectionTreeSource).toContain("hasTreeMultiSelection.value ? flatTreeIndex.value.nodeById : null");
|
||||
expect(connectionTreeSource).not.toMatch(/watch\(\s*visibleNodes,/);
|
||||
});
|
||||
|
||||
it("defers selection cleanup until after the tree render", () => {
|
||||
expect(connectionTreeSource).toMatch(/pruneTreeSelectionToVisibleNodeIds[\s\S]*?\{ flush: "post" \}/);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterSidebarModifierSelectionIds, selectedSidebarBatchTargets, supportsSidebarModifierSelection } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { filterSidebarModifierSelectionIds, pruneTreeSelectionToVisibleNodeIds, selectedSidebarBatchTargets, supportsSidebarModifierSelection } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
|
||||
function node(id: string, type: TreeNode["type"]): TreeNode {
|
||||
@@ -32,6 +32,28 @@ describe("sidebar modifier selection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("visible sidebar selection pruning", () => {
|
||||
it("checks only selected identifiers against the existing visible-node index", () => {
|
||||
const has = vi.fn((id: string) => id === "table-2");
|
||||
|
||||
expect(
|
||||
pruneTreeSelectionToVisibleNodeIds(
|
||||
{ has },
|
||||
{
|
||||
nodeIds: ["table-1", "table-2", "table-3"],
|
||||
activeNodeId: "table-3",
|
||||
anchorNodeId: "table-1",
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
nodeIds: ["table-2"],
|
||||
activeNodeId: "table-2",
|
||||
anchorNodeId: "table-2",
|
||||
});
|
||||
expect(has).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sidebar batch action targets", () => {
|
||||
const table = (id: string, connectionId = "connection-1", database = "database-1"): TreeNode => ({
|
||||
id,
|
||||
|
||||
@@ -6,6 +6,10 @@ export type VisibleTreeSelection = {
|
||||
anchorNodeId: string | null;
|
||||
};
|
||||
|
||||
export type VisibleTreeNodeIdLookup = {
|
||||
has(nodeId: string): boolean;
|
||||
};
|
||||
|
||||
export function supportsSidebarModifierSelection(node: Pick<TreeNode, "type">): boolean {
|
||||
return node.type !== "database" && node.type !== "schema" && !node.type.startsWith("group-");
|
||||
}
|
||||
@@ -21,14 +25,17 @@ export function selectedTreeNodesInVisibleOrder(visibleNodes: TreeNode[], select
|
||||
return visibleNodes.filter((node) => ids.has(node.id));
|
||||
}
|
||||
|
||||
export function pruneTreeSelectionToVisibleNodes(visibleNodes: readonly TreeNode[], selection: VisibleTreeSelection): VisibleTreeSelection {
|
||||
const visibleIds = new Set(visibleNodes.map((node) => node.id));
|
||||
const nodeIds = selection.nodeIds.filter((id) => visibleIds.has(id));
|
||||
const activeNodeId = selection.activeNodeId && visibleIds.has(selection.activeNodeId) ? selection.activeNodeId : (nodeIds[0] ?? null);
|
||||
const anchorNodeId = selection.anchorNodeId && visibleIds.has(selection.anchorNodeId) ? selection.anchorNodeId : activeNodeId;
|
||||
export function pruneTreeSelectionToVisibleNodeIds(visibleNodeIds: VisibleTreeNodeIdLookup, selection: VisibleTreeSelection): VisibleTreeSelection {
|
||||
const nodeIds = selection.nodeIds.filter((id) => visibleNodeIds.has(id));
|
||||
const activeNodeId = selection.activeNodeId && visibleNodeIds.has(selection.activeNodeId) ? selection.activeNodeId : (nodeIds[0] ?? null);
|
||||
const anchorNodeId = selection.anchorNodeId && visibleNodeIds.has(selection.anchorNodeId) ? selection.anchorNodeId : activeNodeId;
|
||||
return { nodeIds, activeNodeId, anchorNodeId };
|
||||
}
|
||||
|
||||
export function pruneTreeSelectionToVisibleNodes(visibleNodes: readonly TreeNode[], selection: VisibleTreeSelection): VisibleTreeSelection {
|
||||
return pruneTreeSelectionToVisibleNodeIds(new Set(visibleNodes.map((node) => node.id)), selection);
|
||||
}
|
||||
|
||||
export function selectedSidebarBatchTargets(activeNode: TreeNode, selectedNodes: readonly TreeNode[], canUseTarget: (node: TreeNode) => boolean): TreeNode[] {
|
||||
if (selectedNodes.length <= 1 || !selectedNodes.some((node) => node.id === activeNode.id)) return [];
|
||||
const first = selectedNodes[0];
|
||||
|
||||
Reference in new issue
Block a user