fix(sidebar): sync tree node expansion state after async load
This commit is contained in:
parent
74f7ce31b6
commit
c0b2cfc2ca
|
|
@ -1184,9 +1184,9 @@ function onSearchToggle(node: TreeNode) {
|
|||
searchCollapsedIds.value = next;
|
||||
}
|
||||
|
||||
function onNodeToggled(node: TreeNode, wasExpanded: boolean) {
|
||||
function onNodeToggled(node: TreeNode, expanded: boolean) {
|
||||
if (isTreeSearchFiltering.value) return;
|
||||
syncSidebarTreeNodeExpansion(store.treeNodes, node, !wasExpanded);
|
||||
syncSidebarTreeNodeExpansion(store.treeNodes, node, expanded);
|
||||
}
|
||||
|
||||
function openSidebarContextMenu(event: MouseEvent, node: TreeNode, openContextMenu: (event: MouseEvent, itemsOverride?: ContextMenuItem[]) => void) {
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ const emit = defineEmits<{
|
|||
"rename-started": [];
|
||||
"group-created": [groupId: string];
|
||||
"request-group-rename": [groupId: string];
|
||||
"node-toggled": [node: TreeNode, wasExpanded: boolean];
|
||||
"node-toggled": [node: TreeNode, expanded: boolean];
|
||||
"search-toggle": [node: TreeNode];
|
||||
"context-menu": [event: MouseEvent, node: TreeNode, items: ContextMenuItem[]];
|
||||
"open-ddl": [node: TreeNode];
|
||||
|
|
@ -4754,13 +4754,13 @@ function activateRuntimeNode(node: TreeNode) {
|
|||
activeNode.value = node;
|
||||
}
|
||||
|
||||
// Async loaders can rebuild a connection node while awaiting the backend.
|
||||
// Publish the live tree node so a stale rendered row cannot reset expansion.
|
||||
// Async loaders can rebuild a connection node while awaiting the backend. Keep
|
||||
// the live node active for later actions, but publish the rendered node so the
|
||||
// tree owner can synchronize display projections without losing the toggle.
|
||||
function emitNodeToggled(node: TreeNode, wasExpanded: boolean, expandedOverride?: boolean) {
|
||||
const liveNode = findSidebarActionTarget(connectionStore.treeNodes, createSidebarActionTarget(node)) ?? node;
|
||||
if (expandedOverride !== undefined) liveNode.isExpanded = expandedOverride;
|
||||
activeNode.value = liveNode;
|
||||
emit("node-toggled", liveNode, wasExpanded);
|
||||
emit("node-toggled", node, expandedOverride ?? !wasExpanded);
|
||||
}
|
||||
|
||||
function activateActionTarget(target: SidebarActionTarget) {
|
||||
|
|
|
|||
81
apps/desktop/src/components/sidebar/__tests__/SidebarTreeRuntimeHost.expansion.spec.ts
vendored
Normal file
81
apps/desktop/src/components/sidebar/__tests__/SidebarTreeRuntimeHost.expansion.spec.ts
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, ref, type App } from "vue";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { syncSidebarTreeNodeExpansion } from "@/lib/sidebar/sidebarTreeExpansion";
|
||||
import SidebarTreeRuntimeHost from "@/components/sidebar/SidebarTreeRuntimeHost.vue";
|
||||
|
||||
const connectionStore = {
|
||||
treeNodes: [] as TreeNode[],
|
||||
sidebarSearchQuery: "",
|
||||
canUseLoadedTreeNodeToggle: vi.fn(() => true),
|
||||
releaseCollapsedTreeNodeChildren: vi.fn(),
|
||||
getConfig: vi.fn(() => ({ db_type: "mysql" })),
|
||||
};
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
CONNECTION_ATTEMPT_CANCELLED_MESSAGE: "connection attempt cancelled",
|
||||
useConnectionStore: () => connectionStore,
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/queryStore", () => ({ useQueryStore: () => ({}) }));
|
||||
vi.mock("@/stores/settingsStore", () => ({ useSettingsStore: () => ({ editorSettings: {} }) }));
|
||||
vi.mock("@/stores/savedSqlStore", () => ({ useSavedSqlStore: () => ({}) }));
|
||||
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
|
||||
vi.mock("@/composables/useSqlHighlighter", () => ({ useSqlHighlighter: () => ({ highlight: vi.fn() }) }));
|
||||
vi.mock("@/composables/useSidebarDataOpenRuntime", () => ({ useSidebarDataOpenRuntime: () => ({ openData: vi.fn() }) }));
|
||||
vi.mock("@/composables/useDatabaseOptions", () => ({ useDatabaseOptions: () => ({ getDatabaseOptions: vi.fn() }) }));
|
||||
vi.mock("@/composables/useSidebarConnectionMutationRuntime", () => ({ useSidebarConnectionMutationRuntime: () => ({}) }));
|
||||
vi.mock("@/composables/useSidebarDatabaseSpecificMutationRuntime", () => ({ useSidebarDatabaseSpecificMutationRuntime: () => ({}) }));
|
||||
vi.mock("@/composables/useSidebarTableMutationRuntime", () => ({ useSidebarTableMutationRuntime: () => ({}) }));
|
||||
vi.mock("@/composables/useSidebarTreeExportRuntime", () => ({ useSidebarTreeExportRuntime: () => ({}) }));
|
||||
vi.mock("@/composables/useSidebarTreeToolRuntime", () => ({ useSidebarTreeToolRuntime: () => ({}) }));
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const app of mountedApps.splice(0)) app.unmount();
|
||||
document.body.innerHTML = "";
|
||||
connectionStore.treeNodes = [];
|
||||
connectionStore.sidebarSearchQuery = "";
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("SidebarTreeRuntimeHost expansion", () => {
|
||||
it("publishes a rendered group collapse and synchronizes the live tree", async () => {
|
||||
const liveGroup: TreeNode = {
|
||||
id: "connection:database:__tables",
|
||||
label: "tree.tables",
|
||||
type: "group-tables",
|
||||
connectionId: "connection",
|
||||
database: "database",
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
const renderedGroup: TreeNode = { ...liveGroup };
|
||||
connectionStore.treeNodes = [liveGroup];
|
||||
|
||||
const host = ref<InstanceType<typeof SidebarTreeRuntimeHost> | null>(null);
|
||||
const toggled = vi.fn((node: TreeNode, expanded: boolean) => {
|
||||
syncSidebarTreeNodeExpansion(connectionStore.treeNodes, node, expanded);
|
||||
});
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup: () => () => h(SidebarTreeRuntimeHost, { ref: host, node: renderedGroup, depth: 0, onNodeToggled: toggled }),
|
||||
}),
|
||||
);
|
||||
mountedApps.push(app);
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
|
||||
host.value?.toggleNode(renderedGroup);
|
||||
await nextTick();
|
||||
|
||||
expect(toggled).toHaveBeenCalledWith(renderedGroup, false);
|
||||
expect(liveGroup.isExpanded).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -4,9 +4,10 @@ import { describe, expect, it } from "vitest";
|
|||
const runtimeSource = readFileSync(new URL("../SidebarTreeRuntimeHost.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("cross-database table paste", () => {
|
||||
it("publishes the live node after async tree loads", () => {
|
||||
it("keeps the live node active after async tree loads", () => {
|
||||
expect(runtimeSource).toContain("function emitNodeToggled(node: TreeNode, wasExpanded: boolean, expandedOverride?: boolean)");
|
||||
expect(runtimeSource).toContain("findSidebarActionTarget(connectionStore.treeNodes, createSidebarActionTarget(node)) ?? node");
|
||||
expect(runtimeSource).toContain("activeNode.value = liveNode");
|
||||
expect(runtimeSource).toContain("emitNodeToggled(node, wasExpanded, false)");
|
||||
expect(runtimeSource).toMatch(/await connectionStore\.loadMongoDatabases\(node\.connectionId\);[\s\S]*?emitNodeToggled\(node, wasExpanded\)/);
|
||||
expect(runtimeSource).toContain("connectionStore.cancelTreeNodeLoad(node.id)");
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ 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(connectionTree, /function onNodeToggled\(node: TreeNode, expanded: boolean\) \{\s*if \(isTreeSearchFiltering\.value\) return;\s*syncSidebarTreeNodeExpansion\(store\.treeNodes, node, expanded\)/);
|
||||
assert.match(runtimeHost, /shouldRunTreeNodeRowAction\(action, clickDetail, isGroupLabel\(node\)\)/);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue