fix(sidebar): copy selected table names to clipboard
This commit is contained in:
1 parent
e6224ec0b0
commit
08d73ce371
2 files changed
+34
-18
No files matched your search
@@ -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() {
|
function openPasteTableDialog() {
|
||||||
const clipboard = connectionStore.treeClipboard;
|
const clipboard = connectionStore.treeClipboard;
|
||||||
if (clipboard?.kind !== "table-copy" || !canPasteTreeClipboardToCurrentNode()) {
|
if (clipboard?.kind !== "table-copy" || !canPasteTreeClipboardToCurrentNode()) {
|
||||||
@@ -3939,7 +3922,8 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|||||||
if (isTableNotView.value) {
|
if (isTableNotView.value) {
|
||||||
items.push({ label: "", separator: true });
|
items.push({ label: "", separator: true });
|
||||||
items.push({ label: t("contextMenu.duplicateStructure"), action: duplicateStructure, icon: CopyPlus });
|
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) {
|
if (supportsTruncate.value) {
|
||||||
destructiveActions.push({
|
destructiveActions.push({
|
||||||
label: truncateMenuLabel(t("contextMenu.truncateTable")),
|
label: truncateMenuLabel(t("contextMenu.truncateTable")),
|
||||||
|
|||||||
@@ -2,6 +2,26 @@ import { strict as assert } from "node:assert";
|
|||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { test } from "vitest";
|
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", () => {
|
test("tree-level context menu opens with the current row items atomically", () => {
|
||||||
const connectionTree = readFileSync("apps/desktop/src/components/sidebar/ConnectionTree.vue", "utf8");
|
const connectionTree = readFileSync("apps/desktop/src/components/sidebar/ConnectionTree.vue", "utf8");
|
||||||
const contextMenu = readFileSync("apps/desktop/src/components/ui/CustomContextMenu.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, /runSidebarDataOpenImmediately/);
|
||||||
assert.match(connectionTree, /createSidebarActionTarget\(node\)/);
|
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"\)\)/);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user