fix(structure): default edit view to DDL

This commit is contained in:
gggaiitx 2026-07-10 18:14:18 +08:00 committed by GitHub
parent 34cf69132d
commit 2f3d1de46a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 139 additions and 31 deletions

View File

@ -7677,8 +7677,21 @@ function clampCellDetailPanelSize(value: number, layout = cellDetailPanelLayout.
// Table info drawers are tied to a single grid instance. Keeping this state
// module-global leaks the drawer into other kept-alive tabs.
const showTableInfo = ref(false);
const activeTableInfoTab = ref<TableInfoTab>("columns");
const activeTableInfoTab = ref<TableInfoTab>("ddl");
const ddlContent = ref("");
const ddlPreRef = ref<HTMLPreElement | null>(null);
function onDdlKeydown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
e.preventDefault();
const el = ddlPreRef.value;
if (!el) return;
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
}
const ddlLoading = ref(false);
const ddlWidth = ref(settingsStore.editorSettings.tableInfoDrawerWidth);
const detailPanelHeight = ref(settingsStore.editorSettings.cellDetailDrawerWidth);
@ -7717,6 +7730,14 @@ watch(activeTableInfoTab, () => {
searchQuery.value = "";
});
watch([activeTableInfoTab, ddlLoading], ([tab, loading]) => {
if (tab === "ddl" && !loading) {
void nextTick(() => {
ddlPreRef.value?.focus();
});
}
});
watch(
() => settingsStore.editorSettings.tableInfoDrawerWidth,
(width) => {
@ -7779,6 +7800,9 @@ const mongoConnectionConfig = computed(() => connectionStore.getConfig(props.con
const canManageMongoIndexes = computed(() => resolvedDatabaseType.value === "mongodb" && !!props.connectionId && !!props.database && !!props.tableMeta?.tableName && mongoConnectionConfig.value?.db_type === "mongodb" && mongoConnectionConfig.value?.driver_profile !== "mongodb-legacy");
const tableInfoTabs = computed(() => {
const tabs: TableInfoTabItem[] = [];
if (tableMetadataCapabilities.value.ddl) {
tabs.push({ id: "ddl", label: "DDL", icon: Code2 });
}
if (tableMetadataCapabilities.value.columns) {
tabs.push({
id: "columns",
@ -7801,9 +7825,6 @@ const tableInfoTabs = computed(() => {
if (tableMetadataCapabilities.value.triggers) {
tabs.push({ id: "triggers", label: t("grid.tableInfoTriggers"), icon: RotateCcw, count: triggers.value.length });
}
if (tableMetadataCapabilities.value.ddl) {
tabs.push({ id: "ddl", label: "DDL", icon: Code2 });
}
return tabs;
});
const tableInfoTabListStyle = computed(() => ({
@ -10168,7 +10189,16 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</div>
</div>
<pre v-else-if="activeTableInfoTab === 'ddl' && !ddlLoading" data-native-clipboard class="flex-1 min-w-0 text-xs font-mono p-3 overflow-auto ddl-code leading-5 select-text" :class="ddlWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'" v-html="filteredDdlContent"></pre>
<pre
v-else-if="activeTableInfoTab === 'ddl' && !ddlLoading"
ref="ddlPreRef"
data-native-clipboard
tabindex="0"
class="flex-1 min-w-0 text-xs font-mono p-3 overflow-auto ddl-code leading-5 select-text outline-none"
:class="ddlWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'"
v-html="filteredDdlContent"
@keydown="onDdlKeydown"
></pre>
<div v-else class="flex-1 flex items-center justify-center">
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
</div>

View File

@ -25,7 +25,7 @@ import { queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
import { type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql";
import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/lib/table/tableColumnTemplates";
import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
import { getTableMetadataCapabilities, firstStructureMetadataTab, isStructureMetadataTabSupported } from "@/lib/table/tableMetadataCapabilities";
import { canAddTableStructureColumn, getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import type { TableInfoTab, TableStructureEditorDraft, TableStructureEditorTarget, TableStructureEditorViewport } from "@/types/database";
@ -107,7 +107,7 @@ const emit = defineEmits<{
openSettings: [initialTab?: string, initialSection?: string];
}>();
const activeTab = ref<TableInfoTab>("columns");
const activeTab = ref<TableInfoTab>("ddl");
const loading = ref(false);
const saving = ref(false);
const postSaveRefreshing = ref(false);
@ -117,6 +117,19 @@ const foreignKeysLoading = ref(false);
const triggersLoading = ref(false);
const ddlContent = ref("");
const ddlLoading = ref(false);
const ddlPreRef = ref<HTMLPreElement | null>(null);
function onDdlKeydown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
e.preventDefault();
const el = ddlPreRef.value;
if (!el) return;
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
}
const ddlFetched = ref(false);
async function fetchDdl() {
@ -1095,7 +1108,6 @@ function clearDraft() {
}
function resetState() {
activeTab.value = "columns";
loading.value = false;
saving.value = false;
postSaveRefreshing.value = false;
@ -2075,7 +2087,8 @@ onMounted(() => {
void loadDynamicDataTypeOptions();
if (props.draft?.initialized) {
restoreDraft(props.draft);
applyInitialStructureTab();
// A restored draft owns its saved tab unless navigation explicitly requested another one.
applyInitialStructureTab(false);
applyInitialStructureTarget();
void hydrateRestoredDraftFromDatabase().then(() => applyInitialStructureTarget());
} else if (isCreateMode.value) {
@ -2106,27 +2119,25 @@ onBeforeUnmount(() => {
persistStructureDensity();
});
function firstStructureMetadataTab(capabilities = tableMetadataCapabilities.value) {
if (capabilities.columns) return "columns";
if (capabilities.indexes) return "indexes";
if (capabilities.foreignKeys) return "foreignKeys";
if (capabilities.triggers) return "triggers";
if (capabilities.ddl && !isCreateMode.value) return "ddl";
return "columns";
function localFirstStructureMetadataTab(capabilities = tableMetadataCapabilities.value) {
return firstStructureMetadataTab(capabilities, isCreateMode.value);
}
function isStructureMetadataTabSupported(tab: TableInfoTab, capabilities = tableMetadataCapabilities.value) {
return (tab === "columns" && capabilities.columns) || (tab === "indexes" && capabilities.indexes) || (tab === "foreignKeys" && capabilities.foreignKeys) || (tab === "triggers" && capabilities.triggers) || (tab === "ddl" && capabilities.ddl && !isCreateMode.value);
function localIsStructureMetadataTabSupported(tab: TableInfoTab, capabilities = tableMetadataCapabilities.value) {
return isStructureMetadataTabSupported(tab, capabilities, isCreateMode.value);
}
function resolveStructureMetadataTab(tab: TableInfoTab | undefined, capabilities = tableMetadataCapabilities.value): TableInfoTab {
if (tab && isStructureMetadataTabSupported(tab, capabilities)) return tab;
return firstStructureMetadataTab(capabilities);
if (tab && localIsStructureMetadataTabSupported(tab, capabilities)) return tab;
return localFirstStructureMetadataTab(capabilities);
}
function applyInitialStructureTab() {
if (!props.initialTab) return;
activeTab.value = resolveStructureMetadataTab(props.initialTab);
function applyInitialStructureTab(useDefault = true) {
if (props.initialTab) {
activeTab.value = resolveStructureMetadataTab(props.initialTab);
} else if (useDefault) {
activeTab.value = resolveStructureMetadataTab(undefined);
}
}
function initialTargetKey(target: TableStructureEditorTarget): string {
@ -2166,7 +2177,7 @@ function applyInitialStructureTarget() {
}
watch(tableMetadataCapabilities, (capabilities) => {
if (!isStructureMetadataTabSupported(activeTab.value, capabilities)) activeTab.value = firstStructureMetadataTab(capabilities);
if (!localIsStructureMetadataTabSupported(activeTab.value, capabilities)) activeTab.value = localFirstStructureMetadataTab(capabilities);
});
watch([() => props.initialTab, () => props.initialTabRequestId, () => props.initialTarget], () => {
@ -2218,9 +2229,21 @@ watch(refreshVersion, (version, previous) => {
void loadStructure(true);
});
watch(activeTab, (tab) => {
if (tab === "ddl") {
void fetchDdl();
watch(
activeTab,
(tab) => {
if (tab === "ddl") {
void fetchDdl();
}
},
{ immediate: true },
);
watch([activeTab, ddlLoading], ([tab, loading]) => {
if (tab === "ddl" && !loading) {
void nextTick(() => {
ddlPreRef.value?.focus();
});
}
});
</script>
@ -2263,11 +2286,11 @@ watch(activeTab, (tab) => {
<Tabs v-model="activeTab" class="flex h-full min-h-0 flex-col">
<div class="flex shrink-0 items-center justify-between gap-2 border-b px-2 py-[var(--structure-header-py)]">
<TabsList>
<TabsTrigger v-if="tableMetadataCapabilities.ddl && !isCreateMode" value="ddl">DDL</TabsTrigger>
<TabsTrigger v-if="tableMetadataCapabilities.columns" value="columns">{{ t("structureEditor.columns") }}</TabsTrigger>
<TabsTrigger v-if="tableMetadataCapabilities.indexes" value="indexes">{{ t("structureEditor.indexes") }}</TabsTrigger>
<TabsTrigger v-if="tableMetadataCapabilities.foreignKeys" value="foreignKeys">{{ t("structureEditor.foreignKeys") }}</TabsTrigger>
<TabsTrigger v-if="tableMetadataCapabilities.triggers" value="triggers">{{ t("structureEditor.triggers") }}</TabsTrigger>
<TabsTrigger v-if="tableMetadataCapabilities.ddl && !isCreateMode" value="ddl">DDL</TabsTrigger>
</TabsList>
<div class="flex shrink-0 items-center gap-1.5">
<div class="flex items-center gap-1.5">
@ -2896,7 +2919,7 @@ watch(activeTab, (tab) => {
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("common.loading") }}
</div>
<pre v-else class="m-0 min-h-0 flex-1 whitespace-pre p-3 font-mono text-xs leading-5 select-text" v-html="ddlContent ? (sqlHighlighter?.(ddlContent) ?? ddlContent) : t('structureEditor.emptyReadonly')"></pre>
<pre v-else ref="ddlPreRef" tabindex="0" class="m-0 min-h-0 flex-1 whitespace-pre p-3 font-mono text-xs leading-5 select-text outline-none" v-html="ddlContent ? (sqlHighlighter?.(ddlContent) ?? ddlContent) : t('structureEditor.emptyReadonly')" @keydown="onDdlKeydown"></pre>
</TabsContent>
</Tabs>
</div>

View File

@ -1,4 +1,4 @@
import type { DatabaseType } from "@/types/database";
import type { DatabaseType, TableInfoTab } from "@/types/database";
export interface TableMetadataCapabilities {
columns: boolean;
@ -71,3 +71,16 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
export function getTableMetadataCapabilities(dbType?: DatabaseType): TableMetadataCapabilities {
return { ...defaultCapabilities, ...(dbType ? capabilityByType[dbType] : undefined) };
}
export function firstStructureMetadataTab(capabilities: TableMetadataCapabilities, isCreateMode: boolean): TableInfoTab {
if (!isCreateMode && capabilities.ddl) return "ddl";
if (capabilities.columns) return "columns";
if (capabilities.indexes) return "indexes";
if (capabilities.foreignKeys) return "foreignKeys";
if (capabilities.triggers) return "triggers";
return "columns";
}
export function isStructureMetadataTabSupported(tab: TableInfoTab, capabilities: TableMetadataCapabilities, isCreateMode: boolean): boolean {
return (tab === "columns" && capabilities.columns) || (tab === "indexes" && capabilities.indexes) || (tab === "foreignKeys" && capabilities.foreignKeys) || (tab === "triggers" && capabilities.triggers) || (tab === "ddl" && capabilities.ddl && !isCreateMode);
}

View File

@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "vitest";
import {
applyManticoreDdlColumnExtras,
@ -21,7 +22,8 @@ import {
rehydrateColumnDraftsFromMetadata,
toColumnNames,
} from "../../apps/desktop/src/lib/table/tableStructureEditorState.ts";
import type { ColumnInfo, IndexInfo } from "../../apps/desktop/src/types/database.ts";
import { firstStructureMetadataTab, isStructureMetadataTabSupported } from "../../apps/desktop/src/lib/table/tableMetadataCapabilities.ts";
import type { ColumnInfo, IndexInfo, TableInfoTab } from "../../apps/desktop/src/types/database.ts";
const columns: ColumnInfo[] = [
{
@ -432,3 +434,43 @@ test("allows Manticore Search column properties only before the column exists",
assert.equal(canEditManticoreColumnProperties("manticoresearch", true), false);
assert.equal(canEditManticoreColumnProperties("mysql", false), false);
});
const fullCapabilities = { columns: true, indexes: true, foreignKeys: true, triggers: true, ddl: true };
const noDdlCapabilities = { columns: true, indexes: true, foreignKeys: true, triggers: true, ddl: false };
test("defaults to DDL tab for edit mode with full capabilities", () => {
assert.equal(firstStructureMetadataTab(fullCapabilities, false), "ddl");
});
test("defaults to columns tab for create mode", () => {
assert.equal(firstStructureMetadataTab(fullCapabilities, true), "columns");
});
test("falls back to columns tab when DDL is not available in edit mode", () => {
assert.equal(firstStructureMetadataTab(noDdlCapabilities, false), "columns");
});
test("preserves a restored structure draft tab without an explicit initial tab", () => {
const source = readFileSync("apps/desktop/src/components/structure/TableStructureEditor.vue", "utf8");
const restoredDraftBlock = source.match(/if \(props\.draft\?\.initialized\) \{[\s\S]*?\n \} else if/);
assert.ok(restoredDraftBlock);
assert.match(restoredDraftBlock[0], /restoreDraft\(props\.draft\);[\s\S]*applyInitialStructureTab\(false\);/);
});
test("supports DDL tab in edit mode", () => {
assert.equal(isStructureMetadataTabSupported("ddl", fullCapabilities, false), true);
});
test("does not support DDL tab in create mode", () => {
assert.equal(isStructureMetadataTabSupported("ddl", fullCapabilities, true), false);
});
test("does not support DDL tab when capability is disabled", () => {
assert.equal(isStructureMetadataTabSupported("ddl", noDdlCapabilities, false), false);
});
test("supports columns tab in both modes", () => {
assert.equal(isStructureMetadataTabSupported("columns", fullCapabilities, false), true);
assert.equal(isStructureMetadataTabSupported("columns", fullCapabilities, true), true);
});