feat(structure): add copy-all-column-names to table designer

This commit is contained in:
zipg
2026-09-20 16:49:18 +08:00
committed by GitHub
parent 9ff5749235
commit 08cb9287de
11 changed files with 91 additions and 1 deletions
@@ -37,6 +37,7 @@ vi.mock("@lucide/vue", async () => {
Check: Icon,
ChevronDown: Icon,
ChevronUp: Icon,
ClipboardList: Icon,
Copy: Icon,
Database: Icon,
Info: Icon,
@@ -44,6 +44,7 @@ vi.mock("@lucide/vue", async () => {
Check: Icon,
ChevronDown: Icon,
ChevronUp: Icon,
ClipboardList: Icon,
Copy: Icon,
Database: Icon,
Info: Icon,
@@ -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(
<Copy :class="structureIconClass" />
{{ t("structureEditor.copyColumns") }}
</Button>
<Button v-if="activeTab === 'columns'" size="sm" variant="outline" :class="structureToolbarButtonClass" :disabled="copyableStructureColumnNames.length === 0" @click="copyColumnNamesDialogOpen = true">
<ClipboardList :class="structureIconClass" />
{{ t("grid.copyColumnNames") }}
</Button>
<Button v-if="isCreateMode && activeTab === 'columns'" size="sm" variant="outline" :class="structureToolbarButtonClass" :disabled="!canAddColumn" @click="applyColumnTemplate(PRESET_FIELDS_TEMPLATE_ID)">
<Copy :class="structureIconClass" />
{{ t("structureEditor.columnTemplates") }}
@@ -5340,6 +5361,7 @@ watch(
</Button>
</div>
<DataGridCopyColumnNamesDialog v-model:open="copyColumnNamesDialogOpen" :column-names="copyableStructureColumnNames" :database-type="databaseType" :column-comments="copyableStructureColumnComments" @copy="copyColumnNamesText" />
<Dialog v-model:open="copyColumnsDialogOpen">
<DialogContent class="max-w-xl">
<DialogHeader>
@@ -37,6 +37,7 @@ vi.mock("@lucide/vue", async () => {
Check: Icon,
ChevronDown: Icon,
ChevronUp: Icon,
ClipboardList: Icon,
Copy: Icon,
Database: Icon,
Info: Icon,
@@ -53,6 +53,7 @@ vi.mock("@lucide/vue", async () => {
ChevronLeft: Icon,
ChevronRight: Icon,
ChevronUp: Icon,
ClipboardList: Icon,
Copy: Icon,
Database: Icon,
Info: Icon,
@@ -38,6 +38,7 @@ vi.mock("@lucide/vue", async () => {
Check: Icon,
ChevronDown: Icon,
ChevronUp: Icon,
ClipboardList: Icon,
Copy: Icon,
Database: Icon,
Info: Icon,
@@ -43,6 +43,7 @@ vi.mock("@lucide/vue", async () => {
ChevronLeft: Icon,
ChevronRight: Icon,
ChevronUp: Icon,
ClipboardList: Icon,
Copy: Icon,
Database: Icon,
Info: Icon,
@@ -46,6 +46,7 @@ vi.mock("@lucide/vue", async () => {
ChevronLeft: Icon,
ChevronRight: Icon,
ChevronUp: Icon,
ClipboardList: Icon,
Copy: Icon,
Database: Icon,
Info: Icon,
@@ -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);
});
});
@@ -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<EditableStructureColumn, "name" | "markedForDrop">[]): 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<EditableStructureColumn, "name" | "comment" | "markedForDrop">[]): Map<string, string> {
const comments = new Map<string, string>();
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);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB