From a65a5881d5679d7247434ca4fcf92ae94eab06c4 Mon Sep 17 00:00:00 2001 From: zipg Date: Mon, 29 Jun 2026 12:11:59 +0800 Subject: [PATCH] =?UTF-8?q?Fix=EF=BC=9A=E4=BF=9D=E7=95=99=E8=A1=A8?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E7=BC=96=E8=BE=91=E6=9C=AA=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E8=8D=89=E7=A8=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/ContentArea.vue | 2 + .../structure/TableStructureEditor.vue | 89 ++++++++++++++++++- apps/desktop/src/stores/queryStore.ts | 5 ++ apps/desktop/src/types/database.ts | 13 +++ packages/app-tests/queryStore.test.ts | 38 ++++++++ 5 files changed, 143 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index dc134a274..663e1c7a1 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -1262,6 +1262,8 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe :database="activeTab.database" :schema="activeTab.schema" :table-name="activeTab.structureTableName || ''" + :draft="activeTab.structureDraft" + @update:draft="(draft) => (activeTab.structureDraft = draft)" @saved="(commentChanged) => emit('structureEditorSaved', commentChanged)" @close="emit('structureEditorClose')" @open-settings="(initialTab, initialSection) => emit('openSettings', initialTab, initialSection)" diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index e025b6120..92478cc42 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -27,6 +27,7 @@ import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/li import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities"; import { canAddTableStructureColumn, getTableStructureCapabilities } from "@/lib/tableStructureCapabilities"; import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/jdbcDialect"; +import type { TableStructureEditorDraft } from "@/types/database"; import { buildStructureTargetLabel, combineDataTypeForDatabase, @@ -79,9 +80,11 @@ const props = defineProps<{ database: string; schema?: string; tableName: string; + draft?: TableStructureEditorDraft; }>(); const emit = defineEmits<{ + "update:draft": [draft: TableStructureEditorDraft | undefined]; saved: [commentChanged: boolean]; close: []; openSettings: [initialTab?: string, initialSection?: string]; @@ -597,6 +600,54 @@ let sqlPreviewDebounceTimer: ReturnType | undefined; let deferredSqlPreviewRefresh = false; let keydownListenerRegistered = false; let skipNextRefreshVersion = false; +let restoringDraft = false; +let syncingDraft = false; +let draftHydrated = false; + +function cloneDraftValue(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +function createCurrentDraft(initialized = true): TableStructureEditorDraft { + return { + activeTab: activeTab.value as TableStructureEditorDraft["activeTab"], + newTableName: newTableName.value, + tableComment: tableComment.value, + originalTableComment: originalTableComment.value, + columns: cloneDraftValue(columns.value), + indexes: cloneDraftValue(indexes.value), + foreignKeys: cloneDraftValue(foreignKeys.value), + triggers: cloneDraftValue(triggers.value), + initialized, + }; +} + +function syncDraftToParent() { + if (!draftHydrated) return; + if (restoringDraft || syncingDraft) return; + syncingDraft = true; + emit("update:draft", createCurrentDraft()); + syncingDraft = false; +} + +function restoreDraft(draft: TableStructureEditorDraft) { + restoringDraft = true; + activeTab.value = draft.activeTab || "columns"; + newTableName.value = draft.newTableName || ""; + tableComment.value = draft.tableComment || ""; + originalTableComment.value = draft.originalTableComment || ""; + columns.value = cloneDraftValue(draft.columns || []); + indexes.value = cloneDraftValue(draft.indexes || []); + foreignKeys.value = cloneDraftValue(draft.foreignKeys || []); + triggers.value = cloneDraftValue(draft.triggers || []); + restoringDraft = false; + draftHydrated = true; +} + +function markDraftHydratedAndSync() { + draftHydrated = true; + syncDraftToParent(); +} function hasPendingStructureChanges(): boolean { if (isCreateMode.value) { @@ -729,6 +780,11 @@ const canApply = computed( () => !loading.value && !saving.value && !postSaveRefreshing.value && !secondaryMetadataLoading.value && !sqlPreviewLoading.value && pendingStatements.value.length > 0 && warnings.value.length === 0 && !!props.connectionId && (isCreateMode.value ? !!newTableName.value.trim() : !!props.tableName), ); +function clearDraft() { + draftHydrated = false; + emit("update:draft", undefined); +} + function resetState() { activeTab.value = "columns"; loading.value = false; @@ -752,6 +808,12 @@ function resetState() { originalTableComment.value = ""; } +async function reloadStructureFromDatabase() { + if (isCreateMode.value) return; + draftHydrated = false; + await loadStructure(false, FULL_STRUCTURE_REFRESH_SCOPE, true, { blockSecondaryMetadata: true }); +} + function setSecondaryMetadataLoading(scope: StructureRefreshScope, value: boolean) { if (scope.indexes && tableMetadataCapabilities.value.indexes) indexesLoading.value = value; if (scope.foreignKeys && tableMetadataCapabilities.value.foreignKeys) foreignKeysLoading.value = value; @@ -772,7 +834,7 @@ async function fetchTableCommentValue(connectionId: string, database: string, sc } } -async function loadStructure(silent = false, scope: StructureRefreshScope = FULL_STRUCTURE_REFRESH_SCOPE, showErrors = true, options: { blockSecondaryMetadata?: boolean } = {}) { +async function loadStructure(silent = false, scope: StructureRefreshScope = FULL_STRUCTURE_REFRESH_SCOPE, showErrors = true, options: { blockSecondaryMetadata?: boolean; preserveDraft?: boolean } = {}) { const connectionId = props.connectionId; const database = props.database; const schema = metadataSchema.value; @@ -783,6 +845,7 @@ async function loadStructure(silent = false, scope: StructureRefreshScope = FULL setSecondaryMetadataLoading(scope, true); errorMessage.value = ""; let secondaryMetadataScheduled = false; + let loadedSuccessfully = false; try { await store.ensureConnected(connectionId); @@ -831,6 +894,7 @@ async function loadStructure(silent = false, scope: StructureRefreshScope = FULL if (options.blockSecondaryMetadata) { await secondaryMetadataPromise; } + loadedSuccessfully = true; } catch (e: any) { if (showErrors) { errorMessage.value = e?.message || String(e); @@ -842,6 +906,9 @@ async function loadStructure(silent = false, scope: StructureRefreshScope = FULL setSecondaryMetadataLoading(scope, false); } if (!silent) loading.value = false; + if (!options.preserveDraft && loadedSuccessfully && requestId === structureLoadRequestId) { + markDraftHydratedAndSync(); + } } } @@ -1211,6 +1278,7 @@ async function applyChanges() { ddlFetched.value = false; ddlContent.value = ""; if (isCreateMode.value) { + clearDraft(); emit("saved", tableComment.value !== originalTableComment.value); emit("close"); } else { @@ -1280,13 +1348,21 @@ onMounted(() => { resetState(); registerStructureEditorShortcuts(); void loadDynamicDataTypeOptions(); - void loadStructure(); + if (props.draft?.initialized) { + restoreDraft(props.draft); + } else if (isCreateMode.value) { + markDraftHydratedAndSync(); + } else { + void loadStructure(false, FULL_STRUCTURE_REFRESH_SCOPE, true, { blockSecondaryMetadata: true }); + } }); onActivated(() => { registerStructureEditorShortcuts(); void loadDynamicDataTypeOptions(); - if (!isCreateMode.value) void loadStructure(true); + if (props.draft?.initialized && !draftHydrated) { + restoreDraft(props.draft); + } }); onDeactivated(unregisterStructureEditorShortcuts); onBeforeUnmount(() => { @@ -1322,10 +1398,15 @@ watch( [isCreateMode, databaseType, () => props.schema, () => props.tableName, newTableName, tableComment, columns, indexes, foreignKeys, triggers], () => { scheduleSqlPreviewRefresh(); + syncDraftToParent(); }, { deep: true, immediate: true }, ); +watch(activeTab, () => { + syncDraftToParent(); +}); + watch(secondaryMetadataLoading, (value) => { if (value || !deferredSqlPreviewRefresh) return; scheduleSqlPreviewRefresh(); @@ -1359,7 +1440,7 @@ watch(activeTab, (tab) => { {{ targetLabel || t("editor.noDatabase") }} {{ connection?.driver_label || databaseType }} - diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 88a15c55f..57e059190 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -54,6 +54,10 @@ const ORACLE_LIKE_METADATA_TYPES = new Set(["oracle", "dameng", "oceanba const BACKGROUND_CLIENT_SESSION_SUFFIXES = ["count", "explain", "export"] as const; const CANCEL_QUERY_TIMEOUT_MS = 10_000; +function cloneTabDraft(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + interface BuildQueryResultExportRequestOptions { exportId: string; filePath: string; @@ -909,6 +913,7 @@ export const useQueryStore = defineStore("query", () => { nacosNamespace: original.nacosNamespace, nacosNamespaceName: original.nacosNamespaceName, structureTableName: original.structureTableName, + structureDraft: original.structureDraft ? cloneTabDraft(original.structureDraft) : undefined, objectBrowser: original.objectBrowser ? { ...original.objectBrowser } : undefined, objectSource: original.objectSource ? { ...original.objectSource } : undefined, tableMeta: original.tableMeta ? { ...original.tableMeta, columns: [...original.tableMeta.columns], primaryKeys: [...original.tableMeta.primaryKeys] } : undefined, diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index b905504c4..59e2f2f8e 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -559,6 +559,18 @@ export interface TreeNode { export type TableInfoTab = "columns" | "indexes" | "foreignKeys" | "triggers" | "ddl"; +export interface TableStructureEditorDraft { + activeTab: TableInfoTab; + newTableName: string; + tableComment: string; + originalTableComment: string; + columns: import("@/lib/tableStructureEditorSql").EditableStructureColumn[]; + indexes: import("@/lib/tableStructureEditorSql").EditableStructureIndex[]; + foreignKeys: import("@/lib/tableStructureEditorSql").EditableStructureForeignKey[]; + triggers: import("@/lib/tableStructureEditorSql").EditableStructureTrigger[]; + initialized: boolean; +} + export interface QueryTab { id: string; title: string; @@ -619,6 +631,7 @@ export interface QueryTab { nacosNamespace?: string; nacosNamespaceName?: string; structureTableName?: string; + structureDraft?: TableStructureEditorDraft; objectBrowser?: { schema?: string; objectType?: "tables"; diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 15e608145..b1d73c8e1 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -2606,6 +2606,44 @@ test("table structure refresh versions are scoped by table target", () => { assert.equal(store.tableStructureRefreshVersion("conn-1", "db", "public", "orders"), 0); }); +test("duplicating a table structure tab clones its unsaved draft", () => { + setActivePinia(createPinia()); + const store = useQueryStore(); + + const tabId = store.openTableStructure("conn-1", "db", "public", "users"); + const tab = store.tabs.find((item) => item.id === tabId)!; + tab.structureDraft = { + activeTab: "columns", + newTableName: "", + tableComment: "", + originalTableComment: "", + columns: [ + { + id: "new:1", + name: "draft_name", + dataType: "varchar(255)", + isNullable: true, + defaultValue: "", + comment: "", + isPrimaryKey: false, + extra: {}, + markedForDrop: false, + }, + ], + indexes: [], + foreignKeys: [], + triggers: [], + initialized: true, + }; + + store.duplicateTab(tabId); + + const copy = store.tabs.find((item) => item.id !== tabId && item.mode === "structure")!; + assert.deepEqual(copy.structureDraft, tab.structureDraft); + copy.structureDraft!.columns[0]!.name = "copy_only"; + assert.equal(tab.structureDraft.columns[0]!.name, "draft_name"); +}); + test("reorderTab keeps pinned tabs before unpinned tabs after reorder", () => { setActivePinia(createPinia()); const store = useQueryStore();