fix(grid): persist table info tab preference

This commit is contained in:
zipg authored and GitHub committed 2026-07-23 19:03:14 +08:00
1 parent 9f42f478bf
commit c6135d6bb2
7 files changed
+139 -9

No files matched your search

+20 -8
View File
@@ -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<TableInfoTab>("ddl");
const activeTableInfoTab = ref<TableInfoTab>(settingsStore.editorSettings.tableInfoActiveTab);
const ddlContent = ref("");
const ddlPreRef = ref<HTMLPreElement | null>(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<ContextMenuItem[]>(() => {
</div>
<!-- Table Info Drawer -->
<div v-if="showTableInfo" class="table-info-drawer relative col-start-2 row-start-1 border-l flex flex-col bg-background min-w-0" :class="[{ 'row-span-2': cellDetailPanelIsBottom }, { 'ddl-drawer-resizing': isResizingDdl }]" :style="ddlDrawerStyle" @contextmenu="onDrawerContextMenu">
<div class="absolute left-0 top-0 bottom-0 z-20 w-1.5 -translate-x-1/2 cursor-col-resize hover:bg-primary/30" @mousedown.prevent="onDdlResizeStart" />
<div class="absolute left-0 top-0 bottom-0 z-20 w-1.5 -translate-x-1/2 cursor-col-resize" @mousedown.prevent="onDdlResizeStart" />
<div class="flex items-center gap-2 px-3 py-1.5 border-b shrink-0 bg-muted/20 h-9">
<TableProperties class="w-3.5 h-3.5 text-muted-foreground" />
<span class="text-xs font-medium flex-1 min-w-0 truncate">{{ tableMeta?.tableName }}</span>
@@ -8635,12 +8647,12 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
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)"
>
<td class="px-3 py-2 text-muted-foreground w-8">{{ index + 1 }}</td>
<td class="px-3 py-2 font-medium">
<td class="cursor-text select-text px-3 py-2 font-medium">
<span class="inline-flex items-center gap-1.5">
<KeyRound v-if="column.is_primary_key" class="h-3 w-3 text-amber-500" />
{{ column.name }}
@@ -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);
});
});
@@ -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<boolean>;
activeTab: Ref<TableInfoTab>;
toggle: (requestedTab?: TableInfoTab) => void;
}
describe("tableInfoTabForDrawerToggle", () => {
it("refreshes a cached grid on open while preserving an already-open drawer", async () => {
const preferredTab = ref<TableInfoTab>("ddl");
const activeGridId = ref("first");
const grids = new Map<string, CachedGridState>();
const CachedGrid = defineComponent({
props: { gridId: { type: String, required: true } },
setup(props) {
const drawerOpen = ref(false);
const activeTab = ref<TableInfoTab>(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");
});
});
@@ -0,0 +1,5 @@
type TableInfoTextSelection = Pick<Selection, "isCollapsed" | "toString">;
export function shouldNavigateFromTableInfoColumnClick(selection?: TableInfoTextSelection | null): boolean {
return !selection || selection.isCollapsed || selection.toString().length === 0;
}
@@ -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;
}
+11 -1
View File
@@ -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<ToolbarItems> | undefined): Toolba
};
}
const TABLE_INFO_TABS = new Set<TableInfoTab>(["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<EditorSettings>, existing?: EditorSettings): EditorSettings {
const sqlSemanticDiagnosticsMode = normalizeSqlSemanticDiagnosticsMode(settings.sqlSemanticDiagnosticsMode, settings.sqlSemanticDiagnosticsEnabled);
const savedExecuteModeDefaultVersion = settings.executeModeDefaultVersion;
@@ -853,6 +861,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, 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);