diff --git a/apps/desktop/src/components/structure/TableStructureEditor.charsetCollation.spec.ts b/apps/desktop/src/components/structure/TableStructureEditor.charsetCollation.spec.ts index 2a235cf1d4..515e0d01e8 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.charsetCollation.spec.ts +++ b/apps/desktop/src/components/structure/TableStructureEditor.charsetCollation.spec.ts @@ -37,6 +37,7 @@ vi.mock("@lucide/vue", async () => { Check: Icon, ChevronDown: Icon, ChevronUp: Icon, + ClipboardList: Icon, Copy: Icon, Database: Icon, Info: Icon, diff --git a/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts b/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts index 4ee08eabac..9e67d77c2e 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts +++ b/apps/desktop/src/components/structure/TableStructureEditor.primaryKey.spec.ts @@ -44,6 +44,7 @@ vi.mock("@lucide/vue", async () => { Check: Icon, ChevronDown: Icon, ChevronUp: Icon, + ClipboardList: Icon, Copy: Icon, Database: Icon, Info: Icon, diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 902e602040..7ac344cb3d 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -10,7 +10,7 @@ import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { AlertTriangle, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Copy, Database, Info, KeyRound, ListChevronsUpDown, Loader2, Maximize2, Pencil, Plus, RefreshCw, RotateCcw, Save, Search, Settings, SlidersHorizontal, Trash2, UserRound, X } from "@lucide/vue"; +import { AlertTriangle, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, ClipboardList, Copy, Database, Info, KeyRound, ListChevronsUpDown, Loader2, Maximize2, Pencil, Plus, RefreshCw, RotateCcw, Save, Search, Settings, SlidersHorizontal, Trash2, UserRound, X } from "@lucide/vue"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -34,6 +34,7 @@ import { joinSqlStatementsForScript } from "@/lib/sql/sqlBatchScript"; import { formatGeneratedDdlIdentifierQuotes, omitDdlDatabaseQualifier, omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay"; import { splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges"; import { copyToClipboard } from "@/lib/common/clipboard"; +import DataGridCopyColumnNamesDialog from "@/components/grid/DataGridCopyColumnNamesDialog.vue"; import { formatSqlForDisplay, sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sql/sqlFormatter"; import { queryTimeoutSecsForConcurrentIndex, queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout"; import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage"; @@ -104,6 +105,8 @@ import { structureColumnSelectionRange, isSyntheticContextMenuClick, resolveColumnSelectionActiveId, + structureColumnCommentsForCopy, + structureColumnNamesForCopy, tableStructureIdentifierComparisonKey, toColumnNames, } from "@/lib/table/tableStructureEditorState"; @@ -3706,6 +3709,20 @@ async function recordStructureHistory(sql: string, start: number, success: boole } } +const copyColumnNamesDialogOpen = ref(false); +// Fields marked for drop no longer exist after saving, so they are excluded. +const copyableStructureColumnNames = computed(() => structureColumnNamesForCopy(columns.value)); +const copyableStructureColumnComments = computed(() => structureColumnCommentsForCopy(columns.value)); + +async function copyColumnNamesText(text: string) { + try { + await copyToClipboard(text); + toast(t("grid.copied")); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } +} + async function copyPreviewSql() { if (sqlPreviewPending.value || sqlPreviewLoading.value || !previewSqlText.value.trim()) return; try { @@ -4438,6 +4455,10 @@ watch( {{ t("structureEditor.copyColumns") }} + + diff --git a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.concurrentTransition.spec.ts b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.concurrentTransition.spec.ts index 21d5950053..0b871ae9b4 100644 --- a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.concurrentTransition.spec.ts +++ b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.concurrentTransition.spec.ts @@ -37,6 +37,7 @@ vi.mock("@lucide/vue", async () => { Check: Icon, ChevronDown: Icon, ChevronUp: Icon, + ClipboardList: Icon, Copy: Icon, Database: Icon, Info: Icon, diff --git a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTab.spec.ts b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTab.spec.ts index 21d36b6086..20b54694e7 100644 --- a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTab.spec.ts +++ b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTab.spec.ts @@ -53,6 +53,7 @@ vi.mock("@lucide/vue", async () => { ChevronLeft: Icon, ChevronRight: Icon, ChevronUp: Icon, + ClipboardList: Icon, Copy: Icon, Database: Icon, Info: Icon, diff --git a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTabLifecycle.spec.ts b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTabLifecycle.spec.ts index e590f34f61..db0a475237 100644 --- a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTabLifecycle.spec.ts +++ b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTabLifecycle.spec.ts @@ -38,6 +38,7 @@ vi.mock("@lucide/vue", async () => { Check: Icon, ChevronDown: Icon, ChevronUp: Icon, + ClipboardList: Icon, Copy: Icon, Database: Icon, Info: Icon, diff --git a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.initialTab.spec.ts b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.initialTab.spec.ts index bf4c0ff079..08e249b440 100644 --- a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.initialTab.spec.ts +++ b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.initialTab.spec.ts @@ -43,6 +43,7 @@ vi.mock("@lucide/vue", async () => { ChevronLeft: Icon, ChevronRight: Icon, ChevronUp: Icon, + ClipboardList: Icon, Copy: Icon, Database: Icon, Info: Icon, diff --git a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.metadataRevalidation.spec.ts b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.metadataRevalidation.spec.ts index c3f3aa3aa4..83cea5db69 100644 --- a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.metadataRevalidation.spec.ts +++ b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.metadataRevalidation.spec.ts @@ -46,6 +46,7 @@ vi.mock("@lucide/vue", async () => { ChevronLeft: Icon, ChevronRight: Icon, ChevronUp: Icon, + ClipboardList: Icon, Copy: Icon, Database: Icon, Info: Icon, diff --git a/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts b/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts index 96713cb755..6beaa3ef1e 100644 --- a/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts @@ -26,6 +26,8 @@ import { resolveInsertColumnIndex, restoreCharacterLengthUnitsAfterSave, splitDataType, + structureColumnCommentsForCopy, + structureColumnNamesForCopy, tableStructureIdentifierComparisonKey, } from "@/lib/table/tableStructureEditorState"; @@ -703,3 +705,42 @@ describe("tableStructureEditorState", () => { expect(isMysqlCharacterDataType("geometry")).toBe(false); }); }); + +describe("structureColumnNamesForCopy", () => { + const column = (name: string, markedForDrop = false) => ({ name, markedForDrop }); + + it("keeps the visible field order", () => { + expect(structureColumnNamesForCopy([column("id"), column("name"), column("note")])).toEqual(["id", "name", "note"]); + }); + + it("drops fields marked for drop and blank names", () => { + expect(structureColumnNamesForCopy([column("id"), column("drop_me", true), column(" "), column(" name ")])).toEqual(["id", "name"]); + }); + + it("returns nothing for an empty table", () => { + expect(structureColumnNamesForCopy([])).toEqual([]); + }); +}); + +describe("structureColumnCommentsForCopy", () => { + it("maps trimmed names to trimmed comments", () => { + const comments = structureColumnCommentsForCopy([ + { name: "id", comment: " 主键 ", markedForDrop: false }, + { name: "name", comment: "名称", markedForDrop: false }, + ]); + expect([...comments]).toEqual([ + ["id", "主键"], + ["name", "名称"], + ]); + }); + + it("skips dropped fields, blank comments and blank names", () => { + const comments = structureColumnCommentsForCopy([ + { name: "gone", comment: "已删除", markedForDrop: true }, + { name: "empty", comment: " ", markedForDrop: false }, + { name: "blank-name", comment: null, markedForDrop: false }, + { name: " ", comment: "没有字段名", markedForDrop: false }, + ]); + expect(comments.size).toBe(0); + }); +}); diff --git a/apps/desktop/src/lib/table/tableStructureEditorState.ts b/apps/desktop/src/lib/table/tableStructureEditorState.ts index 63e0e3124d..2039c732a3 100644 --- a/apps/desktop/src/lib/table/tableStructureEditorState.ts +++ b/apps/desktop/src/lib/table/tableStructureEditorState.ts @@ -1,6 +1,26 @@ import type { ColumnInfo, DatabaseConnectionInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo } from "@/types/database.ts"; import type { ColumnExtra, EditableStructureColumn, EditableStructureForeignKey, EditableStructureIndex, EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql.ts"; +/** + * Column names offered by the structure editor's "copy all column names" action. + * Fields marked for drop disappear on save, so they are not offered. + */ +export function structureColumnNamesForCopy(columns: readonly Pick[]): string[] { + return columns.filter((column) => !column.markedForDrop && column.name.trim()).map((column) => column.name.trim()); +} + +/** Comment lookup for the same action, keyed by the trimmed column name. */ +export function structureColumnCommentsForCopy(columns: readonly Pick[]): Map { + const comments = new Map(); + for (const column of columns) { + if (column.markedForDrop) continue; + const name = column.name.trim(); + const comment = column.comment?.trim(); + if (name && comment) comments.set(name, comment); + } + return comments; +} + export function hasExistingColumnTypeChange(columns: readonly EditableStructureColumn[]): boolean { return columns.some((column) => !!column.original && !column.markedForDrop && column.dataType !== column.original.data_type); } diff --git a/docs/screenshots/issue-9034-copy-all-column-names.png b/docs/screenshots/issue-9034-copy-all-column-names.png new file mode 100644 index 0000000000..e1f2e0f760 Binary files /dev/null and b/docs/screenshots/issue-9034-copy-all-column-names.png differ