mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(structure): add copy-all-column-names to table designer
This commit is contained in:
+1
@@ -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>
|
||||
|
||||
Vendored
+1
@@ -37,6 +37,7 @@ vi.mock("@lucide/vue", async () => {
|
||||
Check: Icon,
|
||||
ChevronDown: Icon,
|
||||
ChevronUp: Icon,
|
||||
ClipboardList: Icon,
|
||||
Copy: Icon,
|
||||
Database: Icon,
|
||||
Info: Icon,
|
||||
|
||||
+1
@@ -53,6 +53,7 @@ vi.mock("@lucide/vue", async () => {
|
||||
ChevronLeft: Icon,
|
||||
ChevronRight: Icon,
|
||||
ChevronUp: Icon,
|
||||
ClipboardList: Icon,
|
||||
Copy: Icon,
|
||||
Database: Icon,
|
||||
Info: Icon,
|
||||
|
||||
Vendored
+1
@@ -38,6 +38,7 @@ vi.mock("@lucide/vue", async () => {
|
||||
Check: Icon,
|
||||
ChevronDown: Icon,
|
||||
ChevronUp: Icon,
|
||||
ClipboardList: Icon,
|
||||
Copy: Icon,
|
||||
Database: Icon,
|
||||
Info: Icon,
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ vi.mock("@lucide/vue", async () => {
|
||||
ChevronLeft: Icon,
|
||||
ChevronRight: Icon,
|
||||
ChevronUp: Icon,
|
||||
ClipboardList: Icon,
|
||||
Copy: Icon,
|
||||
Database: Icon,
|
||||
Info: Icon,
|
||||
|
||||
Vendored
+1
@@ -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 |
Reference in New Issue
Block a user