fix(sidebar): copy selected table names to clipboard

This commit is contained in:
t8y2 2026-07-22 23:10:39 +08:00
parent e6224ec0b0
commit 08d73ce371
2 changed files with 34 additions and 18 deletions

View File

@ -2837,23 +2837,6 @@ async function confirmPasteTable() {
}
}
function copyTableToClipboard() {
const node = activeNode.value;
if (node.type !== "table" || !node.connectionId || !node.database) return;
connectionStore.treeClipboard = {
kind: "table-copy",
tables: [
{
connectionId: node.connectionId,
database: node.database,
schema: node.schema,
tableName: node.label,
},
],
};
toast(t("contextMenu.pasteTableClipboardUpdated"), 2000);
}
function openPasteTableDialog() {
const clipboard = connectionStore.treeClipboard;
if (clipboard?.kind !== "table-copy" || !canPasteTreeClipboardToCurrentNode()) {
@ -3939,7 +3922,8 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
if (isTableNotView.value) {
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.duplicateStructure"), action: duplicateStructure, icon: CopyPlus });
items.push({ label: t("contextMenu.copyTable"), action: copyTableToClipboard, icon: Copy });
// Keep menu copy aligned with keyboard copy so frozen multi-selection and single-row fallback stay compatible.
items.push({ label: t("contextMenu.copyTable"), action: copySelectedNames, icon: Copy });
if (supportsTruncate.value) {
destructiveActions.push({
label: truncateMenuLabel(t("contextMenu.truncateTable")),

View File

@ -2,6 +2,26 @@ import { strict as assert } from "node:assert";
import { readFileSync } from "node:fs";
import { test } from "vitest";
function functionBody(source: string, name: string): string {
const signature = `function ${name}(`;
const asyncSignature = `async ${signature}`;
const signatureIndex = source.indexOf(asyncSignature) >= 0 ? source.indexOf(asyncSignature) : source.indexOf(signature);
assert.notEqual(signatureIndex, -1, `Could not find function ${name}`);
const bodyStart = source.indexOf("{", signatureIndex);
assert.notEqual(bodyStart, -1, `Could not find body for ${name}`);
let depth = 0;
for (let index = bodyStart; index < source.length; index += 1) {
const char = source[index];
if (char === "{") depth += 1;
if (char === "}") {
depth -= 1;
if (depth === 0) return source.slice(bodyStart + 1, index);
}
}
throw new Error(`Could not parse body for ${name}`);
}
test("tree-level context menu opens with the current row items atomically", () => {
const connectionTree = readFileSync("apps/desktop/src/components/sidebar/ConnectionTree.vue", "utf8");
const contextMenu = readFileSync("apps/desktop/src/components/ui/CustomContextMenu.vue", "utf8");
@ -39,3 +59,15 @@ test("tree host owns sidebar data-open generations", () => {
assert.match(connectionTree, /runSidebarDataOpenImmediately/);
assert.match(connectionTree, /createSidebarActionTarget\(node\)/);
});
test("table copy menu uses the shared single and multi-selection clipboard path", () => {
const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8");
const copySelectedNamesBody = functionBody(runtimeHost, "copySelectedNames");
assert.match(runtimeHost, /label: t\("contextMenu\.copyTable"\), action: copySelectedNames, icon: Copy/);
assert.doesNotMatch(runtimeHost, /function copyTableToClipboard\(/);
assert.match(copySelectedNamesBody, /const selectedNodes = selectedTreeNodesInVisibleOrder\(\)/);
assert.match(copySelectedNamesBody, /selectedNodes\.length > 1 && selectedNodes\.some\(\(node\) => node\.id === activeNode\.value\.id\) \? selectedNodes : \[activeNode\.value\]/);
assert.match(copySelectedNamesBody, /updateTreeClipboardForNodes\(nodes\)/);
assert.match(copySelectedNamesBody, /copyToClipboard\(nodes\.map\(copyNameForTreeNode\)\.join\("\\n"\)\)/);
});