Fix:保留表结构编辑未保存草稿
This commit is contained in:
parent
7173f07968
commit
a65a5881d5
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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<typeof setTimeout> | undefined;
|
|||
let deferredSqlPreviewRefresh = false;
|
||||
let keydownListenerRegistered = false;
|
||||
let skipNextRefreshVersion = false;
|
||||
let restoringDraft = false;
|
||||
let syncingDraft = false;
|
||||
let draftHydrated = false;
|
||||
|
||||
function cloneDraftValue<T>(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) => {
|
|||
<Database :class="[structureIconClass, 'text-muted-foreground']" />
|
||||
<span class="min-w-0 flex-1 truncate font-medium">{{ targetLabel || t("editor.noDatabase") }}</span>
|
||||
<Badge variant="outline">{{ connection?.driver_label || databaseType }}</Badge>
|
||||
<Button v-if="!isCreateMode" variant="ghost" size="sm" :class="structureToolbarButtonClass" :disabled="loading || saving" @click="loadStructure()">
|
||||
<Button v-if="!isCreateMode" variant="ghost" size="sm" :class="structureToolbarButtonClass" :disabled="loading || saving" @click="reloadStructureFromDatabase">
|
||||
<RefreshCw :class="structureIconClass" />
|
||||
{{ t("structureEditor.refresh") }}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ const ORACLE_LIKE_METADATA_TYPES = new Set<string>(["oracle", "dameng", "oceanba
|
|||
const BACKGROUND_CLIENT_SESSION_SUFFIXES = ["count", "explain", "export"] as const;
|
||||
const CANCEL_QUERY_TIMEOUT_MS = 10_000;
|
||||
|
||||
function cloneTabDraft<T>(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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue