feat(transfer): support cross-database table paste
This commit is contained in:
1 parent
d928412bf6
commit
551f696cd5
14 files changed
+309
-43
No files matched your search
@@ -56,7 +56,7 @@ import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDia
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { ColumnInfo, ConnectionConfig, ForeignKeyInfo, IndexInfo, ObjectBrowserViewMode, ObjectBrowserViewport, ObjectInfo, ObjectSourceKind, ObjectStatistics, TableInfoTab, TreeNode, TriggerInfo } from "@/types/database";
|
||||
import { sortTablesByFkDependency, type TableWithFk } from "@/lib/table/tableDependencySort";
|
||||
import { isSchemaAware } from "@/lib/database/databaseCapabilities";
|
||||
import { isSchemaAware, supportsTransfer } from "@/lib/database/databaseCapabilities";
|
||||
import { supportsSchemaDiagram, supportsTableImport, supportsTableStructureEditing, supportsTableTruncate } from "@/lib/database/databaseFeatureSupport";
|
||||
import { codeMirrorSqlDialect, connectionObjectTreeNodeSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { getTableMetadataCapabilities, type TableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
|
||||
@@ -79,7 +79,7 @@ import { buildRenameObjectSql, supportsObjectRename } from "@/lib/table/objectRe
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { generateDatabaseExportId } from "@/lib/export/databaseExport";
|
||||
import { copyToClipboard, eventTargetAllowsAppClipboardShortcut } from "@/lib/common/clipboard";
|
||||
import { defaultPasteTableMode, pasteTableModeCopiesData, supportsWholeRowTableDataCopy, tableClipboardMatchesTarget, tableClipboardMenuState, tableDataCopyColumnOptions, type PasteTableMode, type TableClipboardContext } from "@/lib/table/tableClipboard";
|
||||
import { defaultPasteTableMode, pasteTableModeCopiesData, supportsWholeRowTableDataCopy, tableClipboardMatchesTarget, tableClipboardMenuState, tableClipboardSourceContext, tableDataCopyColumnOptions, type PasteTableMode, type TableClipboardContext } from "@/lib/table/tableClipboard";
|
||||
import { formatSqlInsert } from "@/lib/export/exportFormats";
|
||||
import { buildSingleDdlExportFileContent } from "@/lib/export/ddlExport";
|
||||
import { fetchTableDataForExport } from "@/lib/table/tableDataExport";
|
||||
@@ -1922,10 +1922,19 @@ function normalizedObjectBrowserTableClipboardEntries() {
|
||||
if (clipboard?.kind !== "table-copy") return [];
|
||||
return clipboard.tables.map((entry) => ({
|
||||
...entry,
|
||||
schema: normalizeObjectBrowserTableClipboardSchema(entry.schema, entry.database),
|
||||
schema: normalizeObjectBrowserTableClipboardSchema(entry.schema, entry.database, entry.connectionId),
|
||||
}));
|
||||
}
|
||||
|
||||
function canTransferTableClipboard(): boolean {
|
||||
const entries = normalizedObjectBrowserTableClipboardEntries();
|
||||
const target = pasteTableTargetContext();
|
||||
if (entries.length === 0 || props.connection.read_only) return false;
|
||||
const source = tableClipboardSourceContext(entries);
|
||||
const sourceConfig = source ? connectionStore.getConfig(source.connectionId) : undefined;
|
||||
return !!source && !!sourceConfig && supportsTransfer(sourceConfig.db_type) && supportsTransfer(props.connection.db_type) && !tableClipboardMatchesTarget(entries, target);
|
||||
}
|
||||
|
||||
function pasteTableTargetContext(): TableClipboardContext {
|
||||
return {
|
||||
connectionId: props.connection.id,
|
||||
@@ -1934,9 +1943,29 @@ function pasteTableTargetContext(): TableClipboardContext {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeObjectBrowserTableClipboardSchema(schema?: string, database = props.database): string | undefined {
|
||||
if (!isSchemaAware(effectiveDatabaseType.value) && effectiveDatabaseType.value !== "sqlite") return undefined;
|
||||
return connectionObjectTreeNodeSchema(props.connection, database, schema);
|
||||
function normalizeObjectBrowserTableClipboardSchema(schema?: string, database = props.database, connectionId = props.connection.id): string | undefined {
|
||||
const connection = connectionStore.getConfig(connectionId) ?? props.connection;
|
||||
if (!isSchemaAware(connection.db_type) && connection.db_type !== "sqlite") return undefined;
|
||||
return connectionObjectTreeNodeSchema(connection, database, schema);
|
||||
}
|
||||
|
||||
function openTransferFromTableClipboard(): boolean {
|
||||
const clipboard = connectionStore.treeClipboard;
|
||||
const entries = normalizedObjectBrowserTableClipboardEntries();
|
||||
const target = pasteTableTargetContext();
|
||||
if (clipboard?.kind !== "table-copy") return false;
|
||||
const source = tableClipboardSourceContext(entries);
|
||||
if (!source || tableClipboardMatchesTarget(entries, target)) return false;
|
||||
connectionStore.transferSource = {
|
||||
connectionId: source.connectionId,
|
||||
database: source.database,
|
||||
schema: source.schema ?? undefined,
|
||||
tables: clipboard.tables.map((entry) => entry.tableName),
|
||||
targetConnectionId: target.connectionId,
|
||||
targetDatabase: target.database,
|
||||
targetSchema: target.schema ?? undefined,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
function copySingleTableToClipboard(row: ObjectBrowserRow) {
|
||||
@@ -1956,6 +1985,10 @@ function copySingleTableToClipboard(row: ObjectBrowserRow) {
|
||||
|
||||
function openPasteTableDialog() {
|
||||
const clipboard = connectionStore.treeClipboard;
|
||||
if (canTransferTableClipboard()) {
|
||||
openTransferFromTableClipboard();
|
||||
return;
|
||||
}
|
||||
if (!canPasteTableClipboard() || clipboard?.kind !== "table-copy") {
|
||||
toast(t("contextMenu.noTableToPaste"), 2000);
|
||||
return;
|
||||
@@ -1964,7 +1997,7 @@ function openPasteTableDialog() {
|
||||
pasteTableEntries.value = clipboard.tables.map((entry) => ({
|
||||
sourceName: entry.tableName,
|
||||
targetName: `${entry.tableName}_copy`,
|
||||
schema: normalizeObjectBrowserTableClipboardSchema(entry.schema, entry.database),
|
||||
schema: normalizeObjectBrowserTableClipboardSchema(entry.schema, entry.database, entry.connectionId),
|
||||
}));
|
||||
showPasteDialog.value = true;
|
||||
}
|
||||
@@ -1979,7 +2012,7 @@ function onObjectBrowserKeydown(event: KeyboardEvent) {
|
||||
return;
|
||||
}
|
||||
if (eventTargetAllowsAppClipboardShortcut(event, "v")) {
|
||||
if (!canPasteTableClipboard()) return;
|
||||
if (!canPasteTableClipboard() && !canTransferTableClipboard()) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openPasteTableDialog();
|
||||
@@ -2469,12 +2502,16 @@ function exportDataSubmenu(item: ObjectBrowserRow): ContextMenuItem {
|
||||
}
|
||||
|
||||
function objectBrowserTableClipboardMenuState(item: ObjectBrowserRow) {
|
||||
return tableClipboardMenuState(normalizedObjectBrowserTableClipboardEntries(), {
|
||||
connectionId: props.connection.id,
|
||||
database: props.database,
|
||||
schema: normalizeObjectBrowserTableClipboardSchema(item.schema || selectedSchema.value),
|
||||
tableName: item.name,
|
||||
});
|
||||
return tableClipboardMenuState(
|
||||
normalizedObjectBrowserTableClipboardEntries(),
|
||||
{
|
||||
connectionId: props.connection.id,
|
||||
database: props.database,
|
||||
schema: normalizeObjectBrowserTableClipboardSchema(item.schema || selectedSchema.value),
|
||||
tableName: item.name,
|
||||
},
|
||||
canTransferTableClipboard(),
|
||||
);
|
||||
}
|
||||
|
||||
function tableClipboardMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
|
||||
@@ -14,11 +14,11 @@ describe("ObjectBrowser table clipboard context menu", () => {
|
||||
});
|
||||
|
||||
it("normalizes copied and target schemas before paste validation", () => {
|
||||
expect(objectBrowserSource).toMatch(/function normalizedObjectBrowserTableClipboardEntries\(\)[\s\S]*?normalizeObjectBrowserTableClipboardSchema\(entry\.schema, entry\.database\)/);
|
||||
expect(objectBrowserSource).toMatch(/function normalizedObjectBrowserTableClipboardEntries\(\)[\s\S]*?normalizeObjectBrowserTableClipboardSchema\(entry\.schema, entry\.database, entry\.connectionId\)/);
|
||||
expect(objectBrowserSource).toMatch(/function canPasteTableClipboard\(\)[\s\S]*?tableClipboardMatchesTarget\(normalizedObjectBrowserTableClipboardEntries\(\), pasteTableTargetContext\(\)\)/);
|
||||
expect(objectBrowserSource).toMatch(/function pasteTableTargetContext\(\)[\s\S]*?normalizeObjectBrowserTableClipboardSchema\(selectedSchema\.value\)/);
|
||||
expect(objectBrowserSource).toMatch(/pasteTableEntries\.value = clipboard\.tables\.map[\s\S]*?normalizeObjectBrowserTableClipboardSchema\(entry\.schema, entry\.database\)/);
|
||||
expect(objectBrowserSource).toMatch(/function normalizeObjectBrowserTableClipboardSchema[\s\S]*?!isSchemaAware\(effectiveDatabaseType\.value\)[\s\S]*?effectiveDatabaseType\.value !== "sqlite"[\s\S]*?return undefined/);
|
||||
expect(objectBrowserSource).toMatch(/pasteTableEntries\.value = clipboard\.tables\.map[\s\S]*?normalizeObjectBrowserTableClipboardSchema\(entry\.schema, entry\.database, entry\.connectionId\)/);
|
||||
expect(objectBrowserSource).toMatch(/function normalizeObjectBrowserTableClipboardSchema[\s\S]*?connectionStore\.getConfig\(connectionId\) \?\? props\.connection[\s\S]*?!isSchemaAware\(connection\.db_type\)[\s\S]*?connection\.db_type !== "sqlite"[\s\S]*?return undefined/);
|
||||
});
|
||||
|
||||
it("consumes only the clipboard used by a fully successful paste", () => {
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("ObjectBrowser cross-database table paste", () => {
|
||||
it("routes a foreign table clipboard to the shared data-transfer dialog", () => {
|
||||
expect(objectBrowserSource).toContain("function canTransferTableClipboard()");
|
||||
expect(objectBrowserSource).toContain("function openTransferFromTableClipboard()");
|
||||
expect(objectBrowserSource).toContain("targetConnectionId: target.connectionId");
|
||||
expect(objectBrowserSource).toContain("targetDatabase: target.database");
|
||||
expect(objectBrowserSource).toMatch(/function objectBrowserTableClipboardMenuState[\s\S]*?tableClipboardMenuState\([\s\S]*?canTransferTableClipboard\(\),\s*\)/);
|
||||
});
|
||||
|
||||
it("keeps the local duplicate-table paste path available", () => {
|
||||
expect(objectBrowserSource).toContain("if (canTransferTableClipboard()) {");
|
||||
expect(objectBrowserSource).toContain("if (!canPasteTableClipboard() && !canTransferTableClipboard()) return;");
|
||||
expect(objectBrowserSource).toContain("pasteTableMode.value = defaultPasteTableMode(effectiveDatabaseType.value);");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user