feat(editor): add table copy/paste keyboard shortcuts

* feat: add table copy/paste with configurable structure and data options

- Implement Navicat-style table copy/paste functionality
- Support multi-table selection and batch operations
- Configurable paste modes: structure+data (default), structure only, data only
- Backend: add build_copy_table_data_sql API (INSERT INTO ... SELECT)
- TreeItem.vue: copy/paste context menus, paste dialog with mode selection
- ObjectBrowser.vue: batch toolbar buttons, context menu, paste dialog
- Refactor TreeClipboard type to support multi-table entries
- Add i18n keys for all 7 locale files (en, zh-CN, zh-TW, ja, es, it, pt-BR)

* Fix table paste shortcuts

* Fix Kafka MQ sidebar detection

* Restrict table paste scope

* Support data copy for primary SQL databases
This commit is contained in:
miracle authored and GitHub committed 2026-07-01 12:49:37 +08:00
1 parent 5d2ce9f189
commit 0c91fecf5d
26 files changed
+943 -34

No files matched your search

+1
View File
@@ -168,6 +168,7 @@ export const buildDropDatabaseSql = forward("buildDropDatabaseSql");
export const buildCreateSchemaSql = forward("buildCreateSchemaSql");
export const buildDropSchemaSql = forward("buildDropSchemaSql");
export const buildDuplicateTableStructureSql = forward("buildDuplicateTableStructureSql");
export const buildCopyTableDataSql = forward("buildCopyTableDataSql");
export const buildExecutableObjectSourceStatements = forward("buildExecutableObjectSourceStatements");
export const buildExecutableObjectSourceSql = forward("buildExecutableObjectSourceSql");
export const buildEditableObjectSource = forward("buildEditableObjectSource");
+5
View File
@@ -86,6 +86,11 @@ export function eventTargetAllowsNativeClipboard(event: ClipboardShortcutEvent,
return isPlainClipboardShortcut(event, "c") && hasNativeClipboardSelection(env);
}
export function eventTargetAllowsAppClipboardShortcut(event: ClipboardShortcutEvent, key = "v"): boolean {
if (!isPlainClipboardShortcut(event, key)) return false;
return !closestElement(event.target, EDITABLE_CLIPBOARD_TARGET_SELECTOR);
}
export async function readTextFromClipboard(env: ClipboardEnvironment = globalThis as unknown as ClipboardEnvironment): Promise<string> {
if (isTauriRuntime(env as unknown as Record<string, unknown>)) {
try {
+14
View File
@@ -45,6 +45,16 @@ export interface DuplicateTableStructureSqlOptions {
targetName: string;
}
export interface CopyTableDataSqlOptions {
databaseType?: DatabaseType;
schema?: string | null;
sourceName: string;
targetName: string;
columns?: string[];
postgresOverridingSystemValue?: boolean;
sqlserverIdentityInsert?: boolean;
}
export function buildDropObjectSql(options: DropObjectSqlOptions): Promise<string> {
return api.buildDropObjectSql(options);
}
@@ -101,6 +111,10 @@ export function buildDuplicateTableStructureSql(options: DuplicateTableStructure
return api.buildDuplicateTableStructureSql(options);
}
export function buildCopyTableDataSql(options: CopyTableDataSqlOptions): Promise<string> {
return api.buildCopyTableDataSql(options);
}
function quotePostgresIdentifier(value: string): string {
return `"${value.replace(/"/g, '""')}"`;
}
+5 -1
View File
@@ -108,7 +108,7 @@ import type { BuildEditableObjectSourceSqlInput, BuildRoutineRenameObjectSourceI
import type { BuildViewDdlInput } from "@/lib/viewDdl";
import type { BuildRenameObjectSqlOptions } from "@/lib/objectRenameSql";
import type { CreateDatabaseSqlOptions } from "@/lib/createDatabaseSql";
import type { DatabaseNameSqlOptions, DropTableChildObjectSqlOptions, DropObjectSqlOptions, DuplicateTableStructureSqlOptions, SchemaNameSqlOptions, TableAdminSqlOptions } from "@/lib/dbAdminSql";
import type { DatabaseNameSqlOptions, DropTableChildObjectSqlOptions, DropObjectSqlOptions, DuplicateTableStructureSqlOptions, CopyTableDataSqlOptions, SchemaNameSqlOptions, TableAdminSqlOptions } from "@/lib/dbAdminSql";
import type { BuildDatabaseSqlExportOptions, BuildExportInsertStatementsOptions } from "@/lib/databaseExport";
import type { DataCompareFromTablesOptions, DataCompareFromTablesPreparation, DataCompareSyncPlan, DataCompareSyncPlanOptions, DataComparePreparation, DataComparePreparationOptions } from "@/lib/dataCompare";
import { apiUrl } from "@/lib/webPath";
@@ -760,6 +760,10 @@ export async function buildDuplicateTableStructureSql(options: DuplicateTableStr
return post("/api/query/build-duplicate-table-structure-sql", { options });
}
export async function buildCopyTableDataSql(options: CopyTableDataSqlOptions): Promise<string> {
return post("/api/query/build-copy-table-data-sql", { options });
}
export async function buildExecutableObjectSourceStatements(input: BuildEditableObjectSourceSqlInput): Promise<string[]> {
return post("/api/query/build-executable-object-source-statements", { input });
}
+66
View File
@@ -0,0 +1,66 @@
import type { ColumnInfo, DatabaseType } from "@/types/database";
export type PasteTableMode = "structure-and-data" | "structure-only" | "data-only";
export interface TableClipboardContext {
connectionId: string;
database: string;
schema?: string | null;
}
export interface TableDataCopyColumnOptions {
columns: string[];
postgresOverridingSystemValue: boolean;
sqlserverIdentityInsert: boolean;
}
function normalizeSchema(schema: string | null | undefined): string {
return schema?.trim() ?? "";
}
export function tableClipboardEntryMatchesTarget(entry: TableClipboardContext, target: TableClipboardContext): boolean {
return entry.connectionId === target.connectionId && entry.database === target.database && normalizeSchema(entry.schema) === normalizeSchema(target.schema);
}
export function tableClipboardMatchesTarget(entries: TableClipboardContext[], target: TableClipboardContext | null): boolean {
return !!target && entries.length > 0 && entries.every((entry) => tableClipboardEntryMatchesTarget(entry, target));
}
export function supportsWholeRowTableDataCopy(databaseType: DatabaseType | undefined): boolean {
return !!databaseType;
}
export function defaultPasteTableMode(databaseType: DatabaseType | undefined): PasteTableMode {
return supportsWholeRowTableDataCopy(databaseType) ? "structure-and-data" : "structure-only";
}
export function pasteTableModeCopiesData(mode: PasteTableMode): boolean {
return mode === "structure-and-data" || mode === "data-only";
}
export function tableDataCopyColumnOptions(databaseType: DatabaseType | undefined, columns: ColumnInfo[]): TableDataCopyColumnOptions {
const writableColumns = columns.filter((column) => isWritableTableDataCopyColumn(databaseType, column));
return {
columns: writableColumns.map((column) => column.name),
postgresOverridingSystemValue: databaseType === "postgres" && writableColumns.some(isIdentityColumn),
sqlserverIdentityInsert: databaseType === "sqlserver" && writableColumns.some(isIdentityColumn),
};
}
function isWritableTableDataCopyColumn(databaseType: DatabaseType | undefined, column: ColumnInfo): boolean {
const extra = (column.extra ?? "").toLowerCase();
if (databaseType === "mysql") {
return !extra.includes("generated");
}
if (databaseType === "postgres") {
return !extra.includes("generated always as (");
}
if (databaseType === "sqlserver") {
return !extra.includes("computed");
}
return !extra.includes("computed") && !(extra.includes("generated") && !extra.includes("identity"));
}
function isIdentityColumn(column: ColumnInfo): boolean {
return (column.extra ?? "").toLowerCase().includes("identity");
}
+5 -1
View File
@@ -55,7 +55,7 @@ import type { BuildEditableObjectSourceSqlInput, BuildRoutineRenameObjectSourceI
import type { BuildViewDdlInput } from "@/lib/viewDdl";
import type { BuildRenameObjectSqlOptions } from "@/lib/objectRenameSql";
import type { CreateDatabaseSqlOptions } from "@/lib/createDatabaseSql";
import type { DatabaseNameSqlOptions, DropTableChildObjectSqlOptions, DropObjectSqlOptions, DuplicateTableStructureSqlOptions, SchemaNameSqlOptions, TableAdminSqlOptions } from "@/lib/dbAdminSql";
import type { DatabaseNameSqlOptions, DropTableChildObjectSqlOptions, DropObjectSqlOptions, DuplicateTableStructureSqlOptions, CopyTableDataSqlOptions, SchemaNameSqlOptions, TableAdminSqlOptions } from "@/lib/dbAdminSql";
import type { BuildDatabaseSqlExportOptions, BuildExportInsertStatementsOptions } from "@/lib/databaseExport";
export interface AgentDriverInfo {
@@ -747,6 +747,10 @@ export async function buildDuplicateTableStructureSql(options: DuplicateTableStr
return invoke("build_duplicate_table_structure_sql", { options });
}
export async function buildCopyTableDataSql(options: CopyTableDataSqlOptions): Promise<string> {
return invoke("build_copy_table_data_sql", { options });
}
export async function buildExecutableObjectSourceStatements(input: BuildEditableObjectSourceSqlInput): Promise<string[]> {
return invoke("build_executable_object_source_statements", { input });
}