mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(table): open copy dialog from clone action
This commit is contained in:
@@ -436,6 +436,7 @@ const batchEmptyPlan = ref<BatchTableEmptyPlanItem<ObjectBrowserRow>[]>([]);
|
||||
const showPasteDialog = ref(false);
|
||||
const pasteTableMode = ref<PasteTableMode>("structure-and-data");
|
||||
const pasteTableEntries = ref<{ sourceName: string; targetName: string; schema?: string; tableComment?: string | null }[]>([]);
|
||||
const pasteTableFromClipboard = ref(false);
|
||||
const pasteTableDataCopySupported = computed(() => supportsWholeRowTableDataCopy(effectiveDatabaseType.value));
|
||||
const objectColumnWidths = ref<Record<ObjectBrowserColumnKey, number>>({
|
||||
select: 34,
|
||||
@@ -3094,10 +3095,18 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "
|
||||
}
|
||||
}
|
||||
|
||||
function requestDuplicateStructure(row: ObjectBrowserRow) {
|
||||
duplicateTarget.value = row;
|
||||
duplicateTableName.value = `${row.name}_copy`;
|
||||
showDuplicateDialog.value = true;
|
||||
function requestCopyAsNewTable(row: ObjectBrowserRow) {
|
||||
pasteTableFromClipboard.value = false;
|
||||
pasteTableMode.value = defaultPasteTableMode(effectiveDatabaseType.value);
|
||||
pasteTableEntries.value = [
|
||||
{
|
||||
sourceName: row.name,
|
||||
targetName: `${row.name}_copy`,
|
||||
schema: normalizeObjectBrowserTableClipboardSchema(row.schema || selectedSchema.value),
|
||||
tableComment: row.comment,
|
||||
},
|
||||
];
|
||||
showPasteDialog.value = true;
|
||||
}
|
||||
|
||||
function captureTableCopyContext() {
|
||||
@@ -3300,6 +3309,7 @@ function openPasteTableDialog() {
|
||||
toast(t("contextMenu.noTableToPaste"), 2000);
|
||||
return;
|
||||
}
|
||||
pasteTableFromClipboard.value = true;
|
||||
pasteTableMode.value = defaultPasteTableMode(effectiveDatabaseType.value);
|
||||
pasteTableEntries.value = clipboard.tables.map((entry) => ({
|
||||
sourceName: entry.tableName,
|
||||
@@ -3335,7 +3345,9 @@ async function confirmPasteTableImpl() {
|
||||
const entries = pasteTableEntries.value.filter((entry) => entry.targetName.trim()).map((entry) => ({ ...entry }));
|
||||
if (entries.length === 0) return;
|
||||
const context = captureTableCopyContext();
|
||||
const clipboardAtPasteStart = connectionStore.treeClipboard;
|
||||
const shouldConsumeTableClipboard = pasteTableFromClipboard.value;
|
||||
const clipboardAtPasteStart = shouldConsumeTableClipboard ? connectionStore.treeClipboard : null;
|
||||
pasteTableFromClipboard.value = false;
|
||||
const mode = pasteTableMode.value;
|
||||
const copyData = pasteTableModeCopiesData(mode) && pasteTableDataCopySupported.value;
|
||||
showPasteDialog.value = false;
|
||||
@@ -3444,7 +3456,7 @@ async function confirmPasteTableImpl() {
|
||||
}
|
||||
const pasteFeedback = tablePasteFeedback(successCount, pasteFailCount, firstPasteError);
|
||||
if (pasteFailCount === 0) {
|
||||
if (connectionStore.treeClipboard === clipboardAtPasteStart) {
|
||||
if (shouldConsumeTableClipboard && connectionStore.treeClipboard === clipboardAtPasteStart) {
|
||||
connectionStore.treeClipboard = null;
|
||||
}
|
||||
toast(t("contextMenu.batchPasteSuccess", { count: successCount }), 3000);
|
||||
@@ -4375,7 +4387,7 @@ function getTableMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
{ sidebarActionId: "contextMenu.exportStructure", label: t("contextMenu.exportStructure"), action: () => exportStructure(item), icon: FileCode },
|
||||
...(canOpenDataDictionary.value ? [{ sidebarActionId: "dataDictionary.title", label: t("dataDictionary.title"), action: () => openDataDictionary(item), icon: FileText }] : []),
|
||||
{ label: "", separator: true },
|
||||
{ sidebarActionId: "contextMenu.duplicateStructure", label: t("contextMenu.duplicateStructure"), action: () => requestDuplicateStructure(item), icon: CopyPlus },
|
||||
{ sidebarActionId: "contextMenu.duplicateStructure", label: t("contextMenu.duplicateStructure"), action: () => requestCopyAsNewTable(item), icon: CopyPlus },
|
||||
...tableClipboardMenuItems(item),
|
||||
{ label: "", separator: true },
|
||||
{ sidebarActionId: "common.more", label: t("common.more"), icon: ListTree, children: moreActions },
|
||||
@@ -5457,7 +5469,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<Dialog v-model:open="showPasteDialog">
|
||||
<DialogContent class="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ pasteTableEntries.length > 1 ? t("contextMenu.batchPasteTitle") : t("contextMenu.pasteTableConfirmTitle") }}</DialogTitle>
|
||||
<DialogTitle>{{ pasteTableFromClipboard ? (pasteTableEntries.length > 1 ? t("contextMenu.batchPasteTitle") : t("contextMenu.pasteTableConfirmTitle")) : t("contextMenu.duplicateNameTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-2">
|
||||
|
||||
@@ -90,6 +90,7 @@ const {
|
||||
showPasteDialog,
|
||||
pasteTableEntries,
|
||||
pasteTableMode,
|
||||
pasteTableFromClipboard,
|
||||
pasteTableDataCopySupported,
|
||||
confirmPasteTable,
|
||||
showCreateDatabaseDialog,
|
||||
@@ -608,7 +609,7 @@ watch(
|
||||
<Dialog v-model:open="showPasteDialog">
|
||||
<DialogContent class="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ pasteTableEntries.length > 1 ? t("contextMenu.batchPasteTitle") : t("contextMenu.pasteTableConfirmTitle") }}</DialogTitle>
|
||||
<DialogTitle>{{ pasteTableFromClipboard ? (pasteTableEntries.length > 1 ? t("contextMenu.batchPasteTitle") : t("contextMenu.pasteTableConfirmTitle")) : t("contextMenu.duplicateNameTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-2">
|
||||
|
||||
@@ -333,6 +333,7 @@ import {
|
||||
showPasteDialog,
|
||||
pasteTableMode,
|
||||
pasteTableEntries,
|
||||
pasteTableFromClipboard,
|
||||
showCreateDatabaseDialog,
|
||||
createDatabaseName,
|
||||
createDatabaseCharset,
|
||||
@@ -1515,6 +1516,7 @@ function requestPasteTreeClipboard(): boolean {
|
||||
if (clipboard?.kind !== "table-copy") return false;
|
||||
if (canTransferTreeClipboardToCurrentNode()) return openTransferFromTreeClipboard();
|
||||
if (!canPasteTreeClipboardToCurrentNode()) return false;
|
||||
pasteTableFromClipboard.value = true;
|
||||
pasteTableMode.value = defaultPasteTableMode(currentDatabaseType());
|
||||
pasteTableEntries.value = clipboard.tables.map((entry) => ({
|
||||
sourceName: entry.tableName,
|
||||
@@ -5247,12 +5249,22 @@ async function confirmDropSchema() {
|
||||
}
|
||||
}
|
||||
|
||||
function duplicateStructure(source: TreeNode = activeNode.value) {
|
||||
function copyAsNewTable(source: TreeNode = activeNode.value) {
|
||||
if (!isDuplicateStructureSource(source)) return;
|
||||
if (databaseTypeForNode(source) === "victoriametrics") return;
|
||||
duplicateStructureSource.value = source;
|
||||
duplicateTableName.value = `${source.label}_copy`;
|
||||
showDuplicateDialog.value = true;
|
||||
pasteTableFromClipboard.value = false;
|
||||
pasteTableMode.value = defaultPasteTableMode(databaseTypeForNode(source));
|
||||
pasteTableEntries.value = [
|
||||
{
|
||||
sourceName: source.label,
|
||||
targetName: `${source.label}_copy`,
|
||||
connectionId: source.connectionId,
|
||||
database: source.database,
|
||||
schema: normalizeTreeClipboardSchema(source.connectionId, source.database, source.schema),
|
||||
tableComment: source.comment,
|
||||
},
|
||||
];
|
||||
showPasteDialog.value = true;
|
||||
}
|
||||
|
||||
function isDuplicateStructureSource(node: TreeNode): node is DuplicateStructureSource {
|
||||
@@ -5324,7 +5336,9 @@ async function confirmPasteTableImpl() {
|
||||
.filter((entry) => entry.targetName.trim())
|
||||
.map((entry) => ({ ...entry, databaseType: entry.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(entry.connectionId)) : undefined, identifierQuote: connectionStore.connectionIdentifierQuote?.(entry.connectionId) }));
|
||||
if (entries.length === 0) return;
|
||||
const clipboardAtPasteStart = connectionStore.treeClipboard;
|
||||
const shouldConsumeTableClipboard = pasteTableFromClipboard.value;
|
||||
const clipboardAtPasteStart = shouldConsumeTableClipboard ? connectionStore.treeClipboard : null;
|
||||
pasteTableFromClipboard.value = false;
|
||||
const mode = pasteTableMode.value;
|
||||
const copyData = pasteTableModeCopiesData(mode) && pasteTableDataCopySupported.value;
|
||||
showPasteDialog.value = false;
|
||||
@@ -5491,7 +5505,7 @@ async function confirmPasteTableImpl() {
|
||||
}
|
||||
const pasteFeedback = tablePasteFeedback(successCount, pasteFailCount, firstPasteError);
|
||||
if (pasteFailCount === 0) {
|
||||
if (connectionStore.treeClipboard === clipboardAtPasteStart) {
|
||||
if (shouldConsumeTableClipboard && connectionStore.treeClipboard === clipboardAtPasteStart) {
|
||||
connectionStore.treeClipboard = null;
|
||||
}
|
||||
toast(t("contextMenu.batchPasteSuccess", { count: successCount }), 3000);
|
||||
@@ -5518,6 +5532,7 @@ function openPasteTableDialog() {
|
||||
toast(t("contextMenu.noTableToPaste"), 2000);
|
||||
return;
|
||||
}
|
||||
pasteTableFromClipboard.value = true;
|
||||
pasteTableMode.value = defaultPasteTableMode(currentDatabaseType());
|
||||
pasteTableEntries.value = clipboard.tables.map((entry) => ({
|
||||
sourceName: entry.tableName,
|
||||
@@ -6228,6 +6243,7 @@ function objectDialogCapabilities() {
|
||||
showPasteDialog,
|
||||
pasteTableEntries,
|
||||
pasteTableMode,
|
||||
pasteTableFromClipboard,
|
||||
pasteTableDataCopySupported: pasteTableDataCopySupported.value,
|
||||
confirmPasteTable,
|
||||
};
|
||||
@@ -7355,7 +7371,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
items.push(copyStructureAsSubmenu());
|
||||
if (isTableNotView.value) {
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ sidebarActionId: "contextMenu.duplicateStructure", label: t("contextMenu.duplicateStructure"), action: duplicateStructure, icon: CopyPlus });
|
||||
items.push({ sidebarActionId: "contextMenu.duplicateStructure", label: t("contextMenu.duplicateStructure"), action: copyAsNewTable, icon: CopyPlus });
|
||||
// Keep menu copy aligned with keyboard copy so frozen multi-selection and single-row fallback stay compatible.
|
||||
items.push(...treeTableClipboardMenuItems(node));
|
||||
if (supportsVacuum.value) {
|
||||
|
||||
@@ -89,6 +89,7 @@ export const duplicateStructureSource = ref<DuplicateStructureSource | null>(nul
|
||||
export const showPasteDialog = ref(false);
|
||||
export const pasteTableMode = ref<PasteTableMode>("structure-and-data");
|
||||
export const pasteTableEntries = ref<Array<{ sourceName: string; targetName: string; connectionId: string; database: string; schema?: string; tableComment?: string | null }>>([]);
|
||||
export const pasteTableFromClipboard = ref(false);
|
||||
export const showCreateDatabaseDialog = ref(false);
|
||||
export const createDatabaseName = ref("");
|
||||
export const createDatabaseCharset = ref("utf8mb4");
|
||||
|
||||
+28
-2
@@ -66,12 +66,38 @@ test("successful tree table paste consumes only the clipboard used to start it",
|
||||
const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8");
|
||||
const confirmPasteTableBody = functionBody(runtimeHost, "confirmPasteTableImpl");
|
||||
|
||||
assert.match(confirmPasteTableBody, /const clipboardAtPasteStart = connectionStore\.treeClipboard/);
|
||||
assert.match(confirmPasteTableBody, /const shouldConsumeTableClipboard = pasteTableFromClipboard\.value/);
|
||||
assert.match(confirmPasteTableBody, /const clipboardAtPasteStart = shouldConsumeTableClipboard \? connectionStore\.treeClipboard : null/);
|
||||
assert.match(confirmPasteTableBody, /if \(pasteFailCount === 0\)/);
|
||||
assert.match(confirmPasteTableBody, /connectionStore\.treeClipboard === clipboardAtPasteStart/);
|
||||
assert.match(confirmPasteTableBody, /shouldConsumeTableClipboard && connectionStore\.treeClipboard === clipboardAtPasteStart/);
|
||||
assert.match(confirmPasteTableBody, /connectionStore\.treeClipboard = null/);
|
||||
});
|
||||
|
||||
test("tree table clone opens the full copy dialog without replacing the clipboard", () => {
|
||||
const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8");
|
||||
const copyAsNewTableBody = functionBody(runtimeHost, "copyAsNewTable");
|
||||
|
||||
assert.match(copyAsNewTableBody, /pasteTableFromClipboard\.value = false/);
|
||||
assert.match(copyAsNewTableBody, /pasteTableMode\.value = defaultPasteTableMode/);
|
||||
assert.match(copyAsNewTableBody, /targetName: `\$\{source\.label\}_copy`/);
|
||||
assert.match(copyAsNewTableBody, /showPasteDialog\.value = true/);
|
||||
assert.doesNotMatch(copyAsNewTableBody, /treeClipboard\s*=/);
|
||||
assert.match(runtimeHost, /sidebarActionId: "contextMenu\.duplicateStructure"[\s\S]*?action: copyAsNewTable/);
|
||||
});
|
||||
|
||||
test("object browser table clone opens the full copy dialog without consuming the clipboard", () => {
|
||||
const objectBrowser = readFileSync("apps/desktop/src/components/objects/ObjectBrowser.vue", "utf8");
|
||||
const copyAsNewTableBody = functionBody(objectBrowser, "requestCopyAsNewTable");
|
||||
const confirmPasteTableBody = functionBody(objectBrowser, "confirmPasteTableImpl");
|
||||
|
||||
assert.match(copyAsNewTableBody, /pasteTableFromClipboard\.value = false/);
|
||||
assert.match(copyAsNewTableBody, /targetName: `\$\{row\.name\}_copy`/);
|
||||
assert.match(copyAsNewTableBody, /showPasteDialog\.value = true/);
|
||||
assert.doesNotMatch(copyAsNewTableBody, /treeClipboard\s*=/);
|
||||
assert.match(confirmPasteTableBody, /shouldConsumeTableClipboard && connectionStore\.treeClipboard === clipboardAtPasteStart/);
|
||||
assert.match(objectBrowser, /sidebarActionId: "contextMenu\.duplicateStructure"[\s\S]*?requestCopyAsNewTable\(item\)/);
|
||||
});
|
||||
|
||||
test("tree table paste keeps the clipboard when production confirmation is cancelled", () => {
|
||||
const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8");
|
||||
const confirmPasteTableBody = functionBody(runtimeHost, "confirmPasteTableImpl");
|
||||
|
||||
Reference in New Issue
Block a user