From c6135d6bb2b4239bf83303abad3f9535387a0ee5 Mon Sep 17 00:00:00 2001 From: zipg Date: Thu, 23 Jul 2026 19:03:14 +0800 Subject: [PATCH] fix(grid): persist table info tab preference --- apps/desktop/src/components/grid/DataGrid.vue | 28 +++++-- .../table/tableInfoColumnNavigation.spec.ts | 13 +++ .../table/tableInfoTabPreference.spec.ts | 80 +++++++++++++++++++ .../lib/table/tableInfoColumnNavigation.ts | 5 ++ .../src/lib/table/tableInfoTabPreference.ts | 6 ++ apps/desktop/src/stores/settingsStore.ts | 12 ++- packages/app-tests/settingsStore.test.ts | 4 + 7 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/table/tableInfoColumnNavigation.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/table/tableInfoTabPreference.spec.ts create mode 100644 apps/desktop/src/lib/table/tableInfoColumnNavigation.ts create mode 100644 apps/desktop/src/lib/table/tableInfoTabPreference.ts diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index e364b98304..3cf6ade2bd 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -64,6 +64,8 @@ import EnumCellEditor from "@/components/grid/EnumCellEditor.vue"; import type { QueryResult, ColumnInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo, TableInfoTab } from "@/types/database"; import { tableObjectSourceKind } from "@/lib/table/tableObjectSourceKind"; import { tableColumnDefaultDisplayValue } from "@/lib/table/tableColumnDefaultPresentation"; +import { shouldNavigateFromTableInfoColumnClick } from "@/lib/table/tableInfoColumnNavigation"; +import { tableInfoTabForDrawerToggle } from "@/lib/table/tableInfoTabPreference"; import * as api from "@/lib/backend/api"; import { formatElapsedSeconds } from "@/lib/common/elapsedTime"; import { dataGridCellDisplayText, dataGridCellEditorText } from "@/lib/dataGrid/dataGridCellCoercion"; @@ -1758,6 +1760,11 @@ function scrollToTableInfoColumn(columnName: string) { const columnIndex = props.result.columns.findIndex((column, index) => matchesTableInfoColumn(column, props.sourceColumns?.[index], columnName)); scrollToColumnIndex(columnIndex); } + +function onTableInfoColumnClick(columnName: string) { + if (!shouldNavigateFromTableInfoColumnClick(window.getSelection())) return; + scrollToTableInfoColumn(columnName); +} function scrollToColumnIndex(columnIndex: number) { if (columnIndex < 0 || !displayableColumnIndexes.value.includes(columnIndex)) return; @@ -6680,7 +6687,7 @@ 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("ddl"); +const activeTableInfoTab = ref(settingsStore.editorSettings.tableInfoActiveTab); const ddlContent = ref(""); const ddlPreRef = ref(null); function onDdlKeydown(e: KeyboardEvent) { @@ -6842,19 +6849,24 @@ const tableInfoTabListStyle = computed(() => ({ gridTemplateColumns: `repeat(${tableInfoTabs.value.length}, minmax(0, 1fr))`, })); -async function toggleTableInfo(tab: TableInfoTab = activeTableInfoTab.value) { - if (showTableInfo.value && activeTableInfoTab.value === tab) { +async function toggleTableInfo(tab?: TableInfoTab) { + // Kept-alive grids retain local state, so only a closed drawer should refresh + // from the shared preference; an open drawer keeps its current working tab. + const nextTab = tableInfoTabForDrawerToggle(showTableInfo.value, activeTableInfoTab.value, settingsStore.editorSettings.tableInfoActiveTab, tab); + if (showTableInfo.value && activeTableInfoTab.value === nextTab) { showTableInfo.value = false; return; } showTableInfo.value = true; - await selectTableInfoTab(tab); + await selectTableInfoTab(nextTab); } async function selectTableInfoTab(tab: TableInfoTab) { - const nextTab = tableInfoTabs.value.some((item) => item.id === tab) ? tab : tableInfoTabs.value[0]?.id; + const tabSupported = tableInfoTabs.value.some((item) => item.id === tab); + const nextTab = tabSupported ? tab : tableInfoTabs.value[0]?.id; if (!nextTab) return; activeTableInfoTab.value = nextTab; + if (tabSupported) settingsStore.updateEditorSettings({ tableInfoActiveTab: tab }); if (nextTab === "ddl") await fetchDdl(); else if (nextTab === "indexes") await fetchIndexes(); else if (nextTab === "foreignKeys") await fetchForeignKeys(); @@ -8562,7 +8574,7 @@ const gridContextMenuItems = computed(() => {
-
+
{{ tableMeta?.tableName }} @@ -8635,12 +8647,12 @@ const gridContextMenuItems = computed(() => { role="button" tabindex="0" :title="column.name" - @click="scrollToTableInfoColumn(column.name)" + @click="onTableInfoColumnClick(column.name)" @keydown.enter.prevent="scrollToTableInfoColumn(column.name)" @keydown.space.prevent="scrollToTableInfoColumn(column.name)" > {{ index + 1 }} - + {{ column.name }} diff --git a/apps/desktop/src/lib/__tests__/table/tableInfoColumnNavigation.spec.ts b/apps/desktop/src/lib/__tests__/table/tableInfoColumnNavigation.spec.ts new file mode 100644 index 0000000000..31b3d30a9d --- /dev/null +++ b/apps/desktop/src/lib/__tests__/table/tableInfoColumnNavigation.spec.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { shouldNavigateFromTableInfoColumnClick } from "@/lib/table/tableInfoColumnNavigation"; + +describe("shouldNavigateFromTableInfoColumnClick", () => { + it("keeps ordinary row clicks navigable", () => { + expect(shouldNavigateFromTableInfoColumnClick(null)).toBe(true); + expect(shouldNavigateFromTableInfoColumnClick({ isCollapsed: true, toString: () => "" })).toBe(true); + }); + + it("does not navigate after selecting a column name", () => { + expect(shouldNavigateFromTableInfoColumnClick({ isCollapsed: false, toString: () => "customer_name" })).toBe(false); + }); +}); diff --git a/apps/desktop/src/lib/__tests__/table/tableInfoTabPreference.spec.ts b/apps/desktop/src/lib/__tests__/table/tableInfoTabPreference.spec.ts new file mode 100644 index 0000000000..3fc4e613d2 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/table/tableInfoTabPreference.spec.ts @@ -0,0 +1,80 @@ +// @vitest-environment happy-dom + +import { defineComponent, h, KeepAlive, nextTick, ref, type Ref } from "vue"; +import { describe, expect, it } from "vitest"; +import { mountComponent } from "@/components/grid/__tests__/vueHostHarness"; +import type { TableInfoTab } from "@/types/database"; +import { tableInfoTabForDrawerToggle } from "@/lib/table/tableInfoTabPreference"; + +interface CachedGridState { + drawerOpen: Ref; + activeTab: Ref; + toggle: (requestedTab?: TableInfoTab) => void; +} + +describe("tableInfoTabForDrawerToggle", () => { + it("refreshes a cached grid on open while preserving an already-open drawer", async () => { + const preferredTab = ref("ddl"); + const activeGridId = ref("first"); + const grids = new Map(); + + const CachedGrid = defineComponent({ + props: { gridId: { type: String, required: true } }, + setup(props) { + const drawerOpen = ref(false); + const activeTab = ref(preferredTab.value); + const toggle = (requestedTab?: TableInfoTab) => { + const nextTab = tableInfoTabForDrawerToggle(drawerOpen.value, activeTab.value, preferredTab.value, requestedTab); + if (drawerOpen.value && activeTab.value === nextTab) { + drawerOpen.value = false; + return; + } + drawerOpen.value = true; + activeTab.value = nextTab; + preferredTab.value = nextTab; + }; + grids.set(props.gridId, { drawerOpen, activeTab, toggle }); + return () => h("div", props.gridId); + }, + }); + const Host = defineComponent({ + setup() { + return () => h(KeepAlive, { max: 2 }, [h(CachedGrid, { key: activeGridId.value, gridId: activeGridId.value })]); + }, + }); + const mounted = mountComponent(Host, {}); + + activeGridId.value = "second"; + await nextTick(); + expect(grids.get("second")?.activeTab.value).toBe("ddl"); + + activeGridId.value = "first"; + await nextTick(); + grids.get("first")?.toggle("columns"); + expect(preferredTab.value).toBe("columns"); + + activeGridId.value = "second"; + await nextTick(); + grids.get("second")?.toggle(); + expect(grids.get("second")?.activeTab.value).toBe("columns"); + + activeGridId.value = "first"; + await nextTick(); + grids.get("first")?.toggle("indexes"); + expect(preferredTab.value).toBe("indexes"); + + activeGridId.value = "second"; + await nextTick(); + expect(grids.get("second")?.activeTab.value).toBe("columns"); + grids.get("second")?.toggle(); + grids.get("second")?.toggle(); + expect(grids.get("second")?.activeTab.value).toBe("indexes"); + + mounted.unmount(); + }); + + it("keeps explicit tab requests authoritative", () => { + expect(tableInfoTabForDrawerToggle(false, "ddl", "columns", "foreignKeys")).toBe("foreignKeys"); + expect(tableInfoTabForDrawerToggle(true, "ddl", "columns", "foreignKeys")).toBe("foreignKeys"); + }); +}); diff --git a/apps/desktop/src/lib/table/tableInfoColumnNavigation.ts b/apps/desktop/src/lib/table/tableInfoColumnNavigation.ts new file mode 100644 index 0000000000..6ebb26a2dc --- /dev/null +++ b/apps/desktop/src/lib/table/tableInfoColumnNavigation.ts @@ -0,0 +1,5 @@ +type TableInfoTextSelection = Pick; + +export function shouldNavigateFromTableInfoColumnClick(selection?: TableInfoTextSelection | null): boolean { + return !selection || selection.isCollapsed || selection.toString().length === 0; +} diff --git a/apps/desktop/src/lib/table/tableInfoTabPreference.ts b/apps/desktop/src/lib/table/tableInfoTabPreference.ts new file mode 100644 index 0000000000..318f99640b --- /dev/null +++ b/apps/desktop/src/lib/table/tableInfoTabPreference.ts @@ -0,0 +1,6 @@ +import type { TableInfoTab } from "@/types/database"; + +export function tableInfoTabForDrawerToggle(drawerOpen: boolean, activeTab: TableInfoTab, preferredTab: TableInfoTab, requestedTab?: TableInfoTab): TableInfoTab { + if (requestedTab) return requestedTab; + return drawerOpen ? activeTab : preferredTab; +} diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 8478cfde15..5abeb1288b 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -10,7 +10,7 @@ import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort"; import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig"; import { normalizeSqlVariableSyntaxOverrides, type SqlVariableSyntaxOverrides } from "@/lib/sql/sqlVariableSyntax"; import type { SidebarActivation } from "@/lib/sidebar/treeNodeClick"; -import type { SqlSnippet } from "@/types/database"; +import type { SqlSnippet, TableInfoTab } from "@/types/database"; import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion"; import { setDebugLoggingEnabled } from "@/lib/backend/debugLog"; import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS, normalizeTableColumnTemplateFields } from "@/lib/table/tableColumnTemplates"; @@ -422,6 +422,7 @@ export interface EditorSettings { tableFontFamily: string; tableFontSize: number; structureEditorDensity: StructureEditorDensity; + tableInfoActiveTab: TableInfoTab; tableInfoDrawerWidth: number; cellDetailDrawerWidth: number; cellDetailPanelLayout: CellDetailPanelLayout; @@ -583,6 +584,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { tableFontFamily: DEFAULT_DATA_GRID_FONT_FAMILY, tableFontSize: TABLE_FONT_SIZE_DEFAULT, structureEditorDensity: "compact", + tableInfoActiveTab: "ddl", tableInfoDrawerWidth: 320, cellDetailDrawerWidth: 380, cellDetailPanelLayout: "bottom", @@ -780,6 +782,12 @@ function normalizeToolbarItems(items: Partial | undefined): Toolba }; } +const TABLE_INFO_TABS = new Set(["ddl", "columns", "indexes", "foreignKeys", "triggers"]); + +function normalizeTableInfoTab(value: unknown): TableInfoTab { + return typeof value === "string" && TABLE_INFO_TABS.has(value as TableInfoTab) ? (value as TableInfoTab) : DEFAULT_EDITOR_SETTINGS.tableInfoActiveTab; +} + export function normalizeEditorSettings(settings: Partial, existing?: EditorSettings): EditorSettings { const sqlSemanticDiagnosticsMode = normalizeSqlSemanticDiagnosticsMode(settings.sqlSemanticDiagnosticsMode, settings.sqlSemanticDiagnosticsEnabled); const savedExecuteModeDefaultVersion = settings.executeModeDefaultVersion; @@ -853,6 +861,7 @@ export function normalizeEditorSettings(settings: Partial, exist tableFontFamily: normalizeFontFamily(settings.tableFontFamily, DEFAULT_EDITOR_SETTINGS.tableFontFamily), tableFontSize: normalizeTableFontSize(settings.tableFontSize), structureEditorDensity: normalizeStructureEditorDensity(settings.structureEditorDensity), + tableInfoActiveTab: normalizeTableInfoTab(settings.tableInfoActiveTab), tableInfoDrawerWidth: normalizeDrawerWidth(settings.tableInfoDrawerWidth, 240, DEFAULT_EDITOR_SETTINGS.tableInfoDrawerWidth), cellDetailDrawerWidth: normalizeDrawerWidth(settings.cellDetailDrawerWidth, 260, DEFAULT_EDITOR_SETTINGS.cellDetailDrawerWidth), cellDetailPanelLayout: normalizeCellDetailPanelLayout(settings.cellDetailPanelLayout), @@ -1222,6 +1231,7 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.tableFontFamily !== undefined) editorSettings.value.tableFontFamily = normalizeFontFamily(partial.tableFontFamily, DEFAULT_EDITOR_SETTINGS.tableFontFamily); if (partial.tableFontSize !== undefined) editorSettings.value.tableFontSize = normalizeTableFontSize(partial.tableFontSize); if (partial.structureEditorDensity !== undefined) editorSettings.value.structureEditorDensity = normalizeStructureEditorDensity(partial.structureEditorDensity); + if (partial.tableInfoActiveTab !== undefined) editorSettings.value.tableInfoActiveTab = normalizeTableInfoTab(partial.tableInfoActiveTab); if (partial.tableInfoDrawerWidth !== undefined) editorSettings.value.tableInfoDrawerWidth = normalizeDrawerWidth(partial.tableInfoDrawerWidth, 240, DEFAULT_EDITOR_SETTINGS.tableInfoDrawerWidth); if (partial.cellDetailDrawerWidth !== undefined) editorSettings.value.cellDetailDrawerWidth = normalizeDrawerWidth(partial.cellDetailDrawerWidth, 260, DEFAULT_EDITOR_SETTINGS.cellDetailDrawerWidth); if (partial.cellDetailPanelLayout !== undefined) editorSettings.value.cellDetailPanelLayout = normalizeCellDetailPanelLayout(partial.cellDetailPanelLayout); diff --git a/packages/app-tests/settingsStore.test.ts b/packages/app-tests/settingsStore.test.ts index f3f89fd1a8..9be26e3bbd 100644 --- a/packages/app-tests/settingsStore.test.ts +++ b/packages/app-tests/settingsStore.test.ts @@ -354,10 +354,12 @@ test("normalizes table column template fields", () => { }); test("normalizes grid drawer widths", () => { + assert.equal(DEFAULT_EDITOR_SETTINGS.tableInfoActiveTab, "ddl"); assert.equal(DEFAULT_EDITOR_SETTINGS.tableInfoDrawerWidth, 320); assert.equal(DEFAULT_EDITOR_SETTINGS.cellDetailDrawerWidth, 380); assert.equal(DEFAULT_EDITOR_SETTINGS.cellDetailPanelLayout, "bottom"); assert.equal(DEFAULT_EDITOR_SETTINGS.cellDetailJsonFormatted, false); + assert.equal(normalizeEditorSettings({}).tableInfoActiveTab, "ddl"); assert.equal(normalizeEditorSettings({}).tableInfoDrawerWidth, 320); assert.equal(normalizeEditorSettings({}).cellDetailDrawerWidth, 380); assert.equal(normalizeEditorSettings({}).cellDetailPanelLayout, "bottom"); @@ -365,6 +367,8 @@ test("normalizes grid drawer widths", () => { assert.equal(normalizeEditorSettings({ tableInfoDrawerWidth: 200 } as any).tableInfoDrawerWidth, 240); assert.equal(normalizeEditorSettings({ cellDetailDrawerWidth: 200 } as any).cellDetailDrawerWidth, 260); assert.equal(normalizeEditorSettings({ tableInfoDrawerWidth: 1000 } as any).tableInfoDrawerWidth, 900); + assert.equal(normalizeEditorSettings({ tableInfoActiveTab: "columns" } as any).tableInfoActiveTab, "columns"); + assert.equal(normalizeEditorSettings({ tableInfoActiveTab: "invalid" } as any).tableInfoActiveTab, "ddl"); assert.equal(normalizeEditorSettings({ cellDetailDrawerWidth: 456.7 } as any).cellDetailDrawerWidth, 457); assert.equal(normalizeEditorSettings({ cellDetailPanelLayout: "right" } as any).cellDetailPanelLayout, "right"); assert.equal(normalizeEditorSettings({ cellDetailPanelLayout: "invalid" } as any).cellDetailPanelLayout, "bottom");