diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index dcb82b50f..742dfb409 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -1459,7 +1459,7 @@ async function duplicateConnection() { const config = connectionStore.getConfig(connId); if (!config) return; const newConfig = { ...config, id: uuid(), name: `${config.name} (Copy)` }; - await connectionStore.addConnection(newConfig); + await connectionStore.addConnection(newConfig, connectionStore.groupIdForConnection(connId)); toast(t("connection.duplicated"), 2000); } diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 7833931b4..7e48c5e5f 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -9,6 +9,7 @@ import { emptyLayout, appendConnectionToLayout, removeConnectionFromSidebarLayout, + findConnectionLocation, createGroup as createGroupOp, renameGroup as renameGroupOp, deleteGroup as deleteGroupOp, @@ -994,7 +995,7 @@ export const useConnectionStore = defineStore("connection", () => { if (scope === "root") rebuildTreeNodes(); } - async function addConnection(config: ConnectionConfig) { + async function addConnection(config: ConnectionConfig, targetGroupId?: string | null) { const normalized = normalizeConnection(config); const existing = connections.value.findIndex((c) => c.id === normalized.id); const nextConnections = [...connections.value]; @@ -1002,7 +1003,8 @@ export const useConnectionStore = defineStore("connection", () => { nextConnections[existing] = normalized; } else { nextConnections.push(normalized); - sidebarLayout.value = appendConnectionToLayout(sidebarLayout.value, normalized.id, newConnectionGroupId.value); + const groupId = targetGroupId !== undefined ? targetGroupId : newConnectionGroupId.value; + sidebarLayout.value = appendConnectionToLayout(sidebarLayout.value, normalized.id, groupId); } await persistConnections(nextConnections); connections.value = nextConnections; @@ -4057,6 +4059,9 @@ export const useConnectionStore = defineStore("connection", () => { moveConnectionToGroup(connectionId: string, groupId: string | null) { updateLayoutAndRebuild(moveConnectionToGroupOp(sidebarLayout.value, connectionId, groupId)); }, + groupIdForConnection(connectionId: string): string | null { + return findConnectionLocation(sidebarLayout.value, connectionId)?.groupId ?? null; + }, reorderSidebarEntry(draggedId: string, targetId: string, position: DropPosition) { updateLayoutAndRebuild(reorderEntryOp(sidebarLayout.value, draggedId, targetId, position)); }, diff --git a/packages/app-tests/connectionStoreGroupedConnect.test.ts b/packages/app-tests/connectionStoreGroupedConnect.test.ts index 039a0f124..da02b3700 100644 --- a/packages/app-tests/connectionStoreGroupedConnect.test.ts +++ b/packages/app-tests/connectionStoreGroupedConnect.test.ts @@ -100,6 +100,59 @@ test("connecting a grouped connection updates it in place instead of adding a ro } }); +test("duplicating a grouped connection keeps the copy in the same group", async () => { + const originalFetch = globalThis.fetch; + const storage = installMemoryStorage(); + const originalConnection = conn("conn-1", "Grouped MySQL"); + let savedConnections: ConnectionConfig[] = [originalConnection]; + let savedLayout: SidebarLayout | null = { + groups: [{ id: "group-1", name: "Group", collapsed: false }], + order: [{ type: "group", id: "group-1", connectionIds: ["conn-1"] }], + }; + + globalThis.fetch = (async (input, init) => { + const url = String(input); + if (url === "/api/connection/list") { + return new Response(JSON.stringify(savedConnections), { status: 200, headers: { "Content-Type": "application/json" } }); + } + if (url === "/api/layout/sidebar") { + if (init?.method === "POST") { + savedLayout = JSON.parse(String(init.body ?? "null")); + return new Response("null", { status: 200, headers: { "Content-Type": "application/json" } }); + } + return new Response(JSON.stringify(savedLayout), { status: 200, headers: { "Content-Type": "application/json" } }); + } + if (url === "/api/connection/save") { + savedConnections = JSON.parse(String(init?.body ?? "[]")); + return new Response("null", { status: 200, headers: { "Content-Type": "application/json" } }); + } + return new Response("null", { status: 200, headers: { "Content-Type": "application/json" } }); + }) as typeof fetch; + + try { + setActivePinia(createPinia()); + const store = useConnectionStore(); + await store.initFromDisk(); + + const copy = { ...originalConnection, id: "conn-copy", name: "Grouped MySQL (Copy)" }; + await store.addConnection(copy, store.groupIdForConnection(originalConnection.id)); + + assert.deepEqual( + store.treeNodes.map((node) => node.id), + ["group-1"], + ); + assert.equal(store.treeNodes[0].type, "connection-group"); + assert.deepEqual( + store.treeNodes[0].children?.map((node) => node.id), + ["conn-1", "conn-copy"], + ); + assert.equal(countConnectionNodes(store.treeNodes, "conn-copy"), 1); + } finally { + globalThis.fetch = originalFetch; + storage.restore(); + } +}); + test("importing grouped dbx connections remaps exported layout to new connection ids", async () => { const originalFetch = globalThis.fetch; const storage = installMemoryStorage();