From 7e0f77eded7fbded1cbc63650132c2c5af92caaa Mon Sep 17 00:00:00 2001 From: zhangsan Date: Sat, 25 Jul 2026 19:39:25 +0800 Subject: [PATCH] fix(sidebar): refresh table list after batch paste --- .../sidebar/SidebarTreeRuntimeHost.vue | 19 ++++++++++++++----- .../app-tests/sidebarContextMenuHost.test.ts | 13 +++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 2f6f53a9f..b029134e8 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -2808,7 +2808,7 @@ async function confirmPasteTable() { showPasteDialog.value = false; let successCount = 0; let failCount = 0; - const refreshedConnections = new Set(); + const refreshTargets = new Map(); for (const entry of entries) { const targetName = entry.targetName.trim(); try { @@ -2840,15 +2840,24 @@ async function confirmPasteTable() { } successCount++; const refreshKey = `${entry.connectionId}:${entry.database}:${entry.schema || ""}`; - if (!refreshedConnections.has(refreshKey)) { - refreshedConnections.add(refreshKey); - await connectionStore.refreshObjectListTreeNode(entry.connectionId, entry.database, entry.schema); - } + refreshTargets.set(refreshKey, { + connectionId: entry.connectionId, + database: entry.database, + schema: entry.schema, + }); } catch (e: any) { failCount++; console.error(`Failed to paste table "${entry.sourceName}" -> "${targetName}":`, e); } } + for (const refreshTarget of refreshTargets.values()) { + try { + await connectionStore.refreshObjectListTreeNode(refreshTarget.connectionId, refreshTarget.database, refreshTarget.schema); + } catch (e: any) { + failCount++; + console.error(`Failed to refresh pasted tables for "${refreshTarget.database}"${refreshTarget.schema ? ` schema "${refreshTarget.schema}"` : ""}:`, e); + } + } if (failCount === 0) { toast(t("contextMenu.batchPasteSuccess", { count: successCount }), 3000); } else { diff --git a/packages/app-tests/sidebarContextMenuHost.test.ts b/packages/app-tests/sidebarContextMenuHost.test.ts index 2941bfb10..746934094 100644 --- a/packages/app-tests/sidebarContextMenuHost.test.ts +++ b/packages/app-tests/sidebarContextMenuHost.test.ts @@ -71,3 +71,16 @@ test("table copy menu uses the shared single and multi-selection clipboard path" assert.match(copySelectedNamesBody, /updateTreeClipboardForNodes\(nodes\)/); assert.match(copySelectedNamesBody, /copyToClipboard\(nodes\.map\(copyNameForTreeNode\)\.join\("\\n"\)\)/); }); + +test("batch table paste refreshes each object list after all tables are processed", () => { + const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8"); + const confirmPasteTableBody = functionBody(runtimeHost, "confirmPasteTable"); + const pasteLoopIndex = confirmPasteTableBody.indexOf("for (const entry of entries)"); + const refreshLoopIndex = confirmPasteTableBody.indexOf("for (const refreshTarget of refreshTargets.values())"); + + assert.notEqual(pasteLoopIndex, -1); + assert.notEqual(refreshLoopIndex, -1); + assert.ok(refreshLoopIndex > pasteLoopIndex, "object-list refresh must run after the table paste loop"); + assert.doesNotMatch(confirmPasteTableBody.slice(pasteLoopIndex, refreshLoopIndex), /refreshObjectListTreeNode/); + assert.match(confirmPasteTableBody.slice(refreshLoopIndex), /refreshObjectListTreeNode\(refreshTarget\.connectionId, refreshTarget\.database, refreshTarget\.schema\)/); +});