fix(sidebar): sync create database dialog state

This commit is contained in:
Freedom authored and GitHub committed 2026-07-17 18:14:11 +08:00
1 parent 08cd8903f4
commit c88fad6bd8
5 files changed
+108 -9

No files matched your search

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, nextTick, watch, onBeforeUnmount, inject, reactive, shallowRef } from "vue";
import { createRoutedSidebarDialogController } from "./sidebarDialogControllerRouting";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
import { useSidebarDataOpenRuntime } from "@/composables/useSidebarDataOpenRuntime";
import { useSidebarConnectionMutationRuntime } from "@/composables/useSidebarConnectionMutationRuntime";
@@ -404,14 +405,15 @@ function routeTreeItemDialogController() {
const controller = getTreeItemDialogController();
const target = createSidebarActionTarget(activeNode.value);
sidebarFormTarget.value = target;
const routedController = reactive<Record<string, any>>({ ...controller, node: target });
for (const [key, value] of Object.entries(controller)) {
if (typeof value !== "function") continue;
routedController[key] = (...args: unknown[]) => {
activateActionTarget(target);
return value(...args);
};
}
const routedController = createRoutedSidebarDialogController(controller, {
node: target,
wrapAction: (action) => {
return (...args: unknown[]) => {
activateActionTarget(target);
return action(...args);
};
},
});
routedController.pasteTableDataCopySupported = pasteTableDataCopySupported.value;
routedController.canSetCreateDatabaseCharset = canSetCreateDatabaseCharset.value;
routedController.canEditDatabaseCharsetCollation = canEditDatabaseCharsetCollation.value;
@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import { reactive, ref, toRefs } from "vue";
import { createRoutedSidebarDialogController } from "@/components/sidebar/sidebarDialogControllerRouting";
describe("createRoutedSidebarDialogController", () => {
it("keeps dialog open flags linked to the shared module refs", () => {
const showCreateDatabaseDialog = ref(false);
const createDatabaseName = ref("");
const controller = reactive({
showCreateDatabaseDialog,
createDatabaseName,
confirmCreateDatabase: () => "ok",
});
let wrappedCalls = 0;
const routed = createRoutedSidebarDialogController(controller, {
node: { id: "connection:1" },
wrapAction: (action) => {
return (...args) => {
wrappedCalls += 1;
return action(...args);
};
},
});
const { showCreateDatabaseDialog: dialogOpen, createDatabaseName: dialogName } = toRefs(routed);
showCreateDatabaseDialog.value = true;
expect(dialogOpen.value).toBe(true);
dialogOpen.value = false;
expect(showCreateDatabaseDialog.value).toBe(false);
dialogName.value = "analytics";
expect(createDatabaseName.value).toBe("analytics");
expect(routed.confirmCreateDatabase()).toBe("ok");
expect(wrappedCalls).toBe(1);
expect(routed.node).toEqual({ id: "connection:1" });
});
it("does not leave shared open flags stuck after a disconnected-style cancel", () => {
const showCreateDatabaseDialog = ref(false);
const controller = reactive({ showCreateDatabaseDialog });
// Reproduce the old bug: spreading a reactive proxy unwraps refs.
const broken = reactive({ ...controller, node: { id: "connection:1" } });
showCreateDatabaseDialog.value = true;
expect(broken.showCreateDatabaseDialog).toBe(false);
broken.showCreateDatabaseDialog = false;
expect(showCreateDatabaseDialog.value).toBe(true);
// Fixed routing keeps cancel and open in sync.
const routed = createRoutedSidebarDialogController(controller, {
node: { id: "connection:1" },
wrapAction: (action) => action,
});
showCreateDatabaseDialog.value = true;
expect(routed.showCreateDatabaseDialog).toBe(true);
routed.showCreateDatabaseDialog = false;
expect(showCreateDatabaseDialog.value).toBe(false);
});
});
@@ -0,0 +1,29 @@
import { reactive, toRaw } from "vue";
/**
* Build a per-open dialog controller without unwrapping shared dialog refs.
*
* Spreading a reactive controller (`{ ...controller }`) unwraps nested refs into
* plain booleans. The dialog UI then mutates a disconnected copy, while actions
* still write the module-level refs — so cancel leaves `showCreateDatabaseDialog`
* stuck `true`, and the next sidebar right-click remounts an already-open dialog.
*/
export function createRoutedSidebarDialogController(
controller: Record<string, unknown>,
options: {
node: unknown;
wrapAction: (action: (...args: unknown[]) => unknown) => (...args: unknown[]) => unknown;
},
): Record<string, any> {
const routedController = reactive<Record<string, any>>({});
for (const [key, value] of Object.entries(toRaw(controller))) {
if (key === "node") continue;
if (typeof value === "function") {
routedController[key] = options.wrapAction(value as (...args: unknown[]) => unknown);
continue;
}
routedController[key] = value;
}
routedController.node = options.node;
return routedController;
}