fix(grid): persist table info tab preference
This commit is contained in:
7 files changed
+139
-9
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user