diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 5f2d0e351..22430fc61 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -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("columns"); +const activeTableInfoTab = ref("ddl"); const ddlContent = ref(""); +const ddlPreRef = ref(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(() => { -

+            

             
diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 699bbe136..76f24a8b0 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -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("columns"); +const activeTab = ref("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(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(); + }); } }); @@ -2263,11 +2286,11 @@ watch(activeTab, (tab) => {
+ DDL {{ t("structureEditor.columns") }} {{ t("structureEditor.indexes") }} {{ t("structureEditor.foreignKeys") }} {{ t("structureEditor.triggers") }} - DDL
@@ -2896,7 +2919,7 @@ watch(activeTab, (tab) => { {{ t("common.loading") }}
-

+            

           
         
       
diff --git a/apps/desktop/src/lib/table/tableMetadataCapabilities.ts b/apps/desktop/src/lib/table/tableMetadataCapabilities.ts index ad011fbbb..4c7e43f89 100644 --- a/apps/desktop/src/lib/table/tableMetadataCapabilities.ts +++ b/apps/desktop/src/lib/table/tableMetadataCapabilities.ts @@ -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 { + 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); +});