From 3b54fa56df5e0e8e121b12b37fe8a71c1fab5a3c Mon Sep 17 00:00:00 2001 From: onenewcode Date: Wed, 15 Jul 2026 19:36:25 +0800 Subject: [PATCH] feat(structure): insert column after selected --- .../structure/TableStructureEditor.vue | 91 +++++++++++++++---- .../table/tableStructureEditorState.spec.ts | 14 +++ .../lib/table/tableStructureEditorState.ts | 8 ++ .../tableStructureEditorState.test.ts | 14 +++ 4 files changed, 108 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 72e3efc69a..78d361befa 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -66,6 +66,7 @@ import { mysqlEnumDataType, parseExtraToColumnExtra, rehydrateColumnDraftsFromMetadata, + resolveInsertColumnIndex, restoreDamengLengthUnitsAfterSave, splitDataType, toColumnNames, @@ -548,6 +549,7 @@ const indexColWidths = ref(loadStructureIndexColumnWidths(structureDensity.value const resizing = ref<{ col: number; startX: number; startW: number } | null>(null); const columnSearchInputRef = ref>(); const columnSearchText = ref(""); +const selectedColumnId = ref(null); const highlightedColumnId = ref(null); const indexSearchInputRef = ref>(); const indexSearchText = ref(""); @@ -1191,6 +1193,7 @@ function resetState() { sqliteSchemaRevision.value = undefined; foreignKeys.value = []; triggers.value = []; + selectedColumnId.value = null; ddlContent.value = ""; ddlFetched.value = false; newTableName.value = ""; @@ -1270,6 +1273,7 @@ async function loadStructure(silent = false, scope: StructureRefreshScope = FULL const nextColumnDrafts = createColumnDrafts(nextColumns, databaseType.value); const hydratedColumnDrafts = databaseType.value === "dameng" && options.damengLengthUnitsAfterSave ? restoreDamengLengthUnitsAfterSave(nextColumnDrafts, options.damengLengthUnitsAfterSave) : nextColumnDrafts; columns.value = applyStoredLocalColumnOrder(hydratedColumnDrafts); + if (!options.preserveDraft) selectedColumnId.value = null; } const nextTableComment = await tableCommentPromise; @@ -1325,6 +1329,20 @@ async function refreshStructureAfterSave(scope: StructureRefreshScope, damengLen } } +async function focusColumnNameInput(columnId: string) { + await nextTick(); + const row = Array.from(rootRef.value?.querySelectorAll("[data-column-row-index]") ?? []).find((element) => element.dataset.columnId === columnId); + const input = row?.querySelector("[data-column-name-input]"); + row?.scrollIntoView({ block: "nearest" }); + input?.focus(); + input?.select(); +} + +function onColumnRowActivate(column: EditableStructureColumn) { + if (column.markedForDrop || !columns.value.some((item) => item.id === column.id)) return; + selectedColumnId.value = column.id; +} + async function addColumn() { if (!canAddColumn.value) return; activeTab.value = "columns"; @@ -1343,14 +1361,11 @@ async function addColumn() { extra: {}, markedForDrop: false, }; - columns.value.push(column); - await nextTick(); - const newRows = rootRef.value?.querySelectorAll('[data-new-column-row="true"]'); - const row = newRows?.[newRows.length - 1]; - const input = row?.querySelector("[data-column-name-input]"); - row?.scrollIntoView({ block: "nearest" }); - input?.focus(); - input?.select(); + const insertAt = resolveInsertColumnIndex(columns.value, selectedColumnId.value); + columns.value.splice(insertAt, 0, column); + selectedColumnId.value = column.id; + if (usesLocalTableColumnOrder.value) persistLocalColumnOrder(); + await focusColumnNameInput(column.id); } function applyColumnTemplate(templateId: string) { @@ -1364,11 +1379,15 @@ function applyColumnTemplate(templateId: string) { createId: uuid, }); if (!templateColumns.length) return; - columns.value.push(...templateColumns); + const insertAt = resolveInsertColumnIndex(columns.value, selectedColumnId.value); + columns.value.splice(insertAt, 0, ...templateColumns); + selectedColumnId.value = templateColumns[templateColumns.length - 1]?.id ?? selectedColumnId.value; + if (usesLocalTableColumnOrder.value) persistLocalColumnOrder(); } function removeNewColumn(column: EditableStructureColumn) { columns.value = columns.value.filter((item) => item.id !== column.id); + if (selectedColumnId.value === column.id) selectedColumnId.value = null; } type ColumnDragState = { @@ -1741,10 +1760,12 @@ function onColumnDragEnd() { function columnRowClass(column: EditableStructureColumn, index: number) { const dragState = columnDragState.value; const isSearchMatch = filteredColumnRowIds.value.has(column.id); + const isSelected = selectedColumnId.value === column.id && !column.markedForDrop; return { "bg-destructive/5 opacity-60": column.markedForDrop, "structure-column-search-match": isSearchMatch, - "structure-column-search-current": highlightedColumnId.value === column.id, + // Reuse the existing search-current highlight for the active/selected row. + "structure-column-search-current": highlightedColumnId.value === column.id || isSelected, "opacity-55": dragState?.columnId === column.id, "bg-primary/5": dragState && (dragState.insertionIndex === index || dragState.insertionIndex === index + 1), "[&>td]:border-t-2 [&>td]:border-t-primary": dragState?.insertionIndex === index, @@ -1894,6 +1915,9 @@ function columnDragInsertionIndex(index: number, event: DragEvent): number { function toggleDropColumn(column: EditableStructureColumn) { if (!canDropColumn(column)) return; column.markedForDrop = !column.markedForDrop; + if (column.markedForDrop && selectedColumnId.value === column.id) { + selectedColumnId.value = null; + } } function isColumnNameDisabled(column: EditableStructureColumn): boolean { @@ -2431,12 +2455,23 @@ watch( ); watch(activeTab, () => { + selectedColumnId.value = null; highlightedColumnId.value = null; highlightedIndexId.value = null; restoreStructureScrollPosition(); syncDraftToParent(); }); +watch( + columns, + (items) => { + if (selectedColumnId.value && !items.some((column) => column.id === selectedColumnId.value)) { + selectedColumnId.value = null; + } + }, + { deep: false }, +); + watch(secondaryMetadataLoading, (value) => { if (value || !deferredSqlPreviewRefresh) return; scheduleSqlPreviewRefresh(); @@ -2636,7 +2671,18 @@ watch([activeTab, ddlLoading], ([tab, loading]) => { - +
{{ index + 1 }} @@ -2965,12 +3011,12 @@ watch([activeTab, ddlLoading], ([tab, loading]) => { :disabled="!canDropColumn(column)" :title="column.markedForDrop ? t('structureEditor.restore') : t('structureEditor.drop')" :aria-label="column.markedForDrop ? t('structureEditor.restore') : t('structureEditor.drop')" - @click="toggleDropColumn(column)" + @click.stop="toggleDropColumn(column)" > -
@@ -3263,20 +3309,27 @@ watch([activeTab, ddlLoading], ([tab, loading]) => { diff --git a/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts b/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts index 62ec5c7cc8..aca715905c 100644 --- a/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts @@ -18,6 +18,7 @@ import { mysqlEnumDataType, parseExtraToColumnExtra, rehydrateColumnDraftsFromMetadata, + resolveInsertColumnIndex, restoreDamengLengthUnitsAfterSave, splitDataType, } from "@/lib/table/tableStructureEditorState"; @@ -248,6 +249,19 @@ describe("tableStructureEditorState", () => { expect(hasExistingColumnTypeChange([column])).toBe(false); }); + it("inserts new columns after the selected row or appends when none is selected", () => { + const columns = [{ id: "a" }, { id: "b" }, { id: "c" }]; + + expect(resolveInsertColumnIndex(columns, null)).toBe(3); + expect(resolveInsertColumnIndex(columns, undefined)).toBe(3); + expect(resolveInsertColumnIndex(columns, "a")).toBe(1); + expect(resolveInsertColumnIndex(columns, "b")).toBe(2); + expect(resolveInsertColumnIndex(columns, "c")).toBe(3); + expect(resolveInsertColumnIndex(columns, "missing")).toBe(3); + expect(resolveInsertColumnIndex([], "a")).toBe(0); + expect(resolveInsertColumnIndex([{ id: "a", markedForDrop: true }, { id: "b" }], "a")).toBe(2); + }); + it("strips SQL Server metadata parentheses from editable defaults", () => { const drafts = createColumnDrafts( [ diff --git a/apps/desktop/src/lib/table/tableStructureEditorState.ts b/apps/desktop/src/lib/table/tableStructureEditorState.ts index c6e100b799..d93c2c0a15 100644 --- a/apps/desktop/src/lib/table/tableStructureEditorState.ts +++ b/apps/desktop/src/lib/table/tableStructureEditorState.ts @@ -1066,6 +1066,14 @@ export function defaultNewColumnDataType(dbType: DatabaseType | undefined, dataT return dbType === "sqlite" ? "text" : "varchar(255)"; } +/** Index at which to insert a new column (after the selected row, or append when none). */ +export function resolveInsertColumnIndex(columns: readonly { id: string; markedForDrop?: boolean }[], selectedColumnId: string | null | undefined): number { + if (!selectedColumnId) return columns.length; + // Dropped rows are not valid insertion anchors. + const index = columns.findIndex((column) => column.id === selectedColumnId && !column.markedForDrop); + return index >= 0 ? index + 1 : columns.length; +} + function isMysqlDeprecatedDefaultParameterType(baseType: string): boolean { const typeName = baseType.split(/\s+/)[0]; return ["tinyint", "smallint", "mediumint", "int", "integer", "bigint", "float", "double", "real"].includes(typeName ?? ""); diff --git a/packages/app-tests/tableStructureEditorState.test.ts b/packages/app-tests/tableStructureEditorState.test.ts index 6f3200a408..c662b3f289 100644 --- a/packages/app-tests/tableStructureEditorState.test.ts +++ b/packages/app-tests/tableStructureEditorState.test.ts @@ -21,6 +21,7 @@ import { normalizeDataTypeParams, parseExtraToColumnExtra, rehydrateColumnDraftsFromMetadata, + resolveInsertColumnIndex, toColumnNames, } from "../../apps/desktop/src/lib/table/tableStructureEditorState.ts"; import { firstStructureMetadataTab, isStructureMetadataTabSupported } from "../../apps/desktop/src/lib/table/tableMetadataCapabilities.ts"; @@ -52,6 +53,19 @@ const indexes: IndexInfo[] = [ { name: "idx_name", columns: ["name"], is_unique: false, is_primary: false }, ]; +test("resolves insert index after the selected column for all databases", () => { + const items = [{ id: "a" }, { id: "b" }, { id: "c" }]; + + assert.equal(resolveInsertColumnIndex(items, null), 3); + assert.equal(resolveInsertColumnIndex(items, undefined), 3); + assert.equal(resolveInsertColumnIndex(items, "a"), 1); + assert.equal(resolveInsertColumnIndex(items, "b"), 2); + assert.equal(resolveInsertColumnIndex(items, "c"), 3); + assert.equal(resolveInsertColumnIndex(items, "missing"), 3); + assert.equal(resolveInsertColumnIndex([], "a"), 0); + assert.equal(resolveInsertColumnIndex([{ id: "a", markedForDrop: true }, { id: "b" }], "a"), 2); +}); + test("creates editable column drafts from column metadata", () => { const drafts = createColumnDrafts(columns, "mysql");