diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 84c5454ab3..7cb1cf0c83 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -228,6 +228,8 @@ const connectionStore = useConnectionStore(); const queryStore = useQueryStore(); const settingsStore = useSettingsStore(); const tableFontSize = computed(() => settingsStore.editorSettings.tableFontSize); +const multiRowTranspose = computed(() => settingsStore.editorSettings.dataGridMultiRowTranspose); +const hideNullColumns = computed(() => settingsStore.editorSettings.dataGridHideNullColumns); const { isDark, themePalette } = useTheme(); const { toast } = useToast(); const { highlight } = useSqlHighlighter(); @@ -343,7 +345,6 @@ if (isDebugLoggingEnabled()) { const transposeRowIndex = ref(null); const showTranspose = ref(false); -const multiRowTranspose = ref(false); const preserveTransposeOnNextResult = ref(false); watch( @@ -1676,6 +1677,8 @@ const { columnOrderKeys, layoutScopeKey: columnLayoutScopeKey, tableScopeKey: tableColumnOrderScopeKey, + hideNullColumns, + onHideNullColumnsChange: (value) => settingsStore.updateEditorSettings({ dataGridHideNullColumns: value }), onRefreshMetrics: refreshGridScrollerMetrics, }); const goToColumnItems = computed(() => @@ -6116,7 +6119,8 @@ function scrollTransposeRecordIntoView(rowIndex: number) { } function setMultiRowTranspose(value: boolean) { - multiRowTranspose.value = value; + if (multiRowTranspose.value === value) return; + settingsStore.updateEditorSettings({ dataGridMultiRowTranspose: value }); if (!showTranspose.value) return; nextTick(updateTransposeViewport); if (value && transposeRowIndex.value !== null) { diff --git a/apps/desktop/src/composables/__tests__/useDataGridColumnLayout.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridColumnLayout.spec.ts index fca3e9a34b..dbdeca36e3 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridColumnLayout.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridColumnLayout.spec.ts @@ -1,6 +1,6 @@ // @vitest-environment happy-dom -import { effectScope, ref } from "vue"; +import { effectScope, nextTick, ref } from "vue"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { dataGridColumnOffsets, dataGridHorizontalColumnWindow, useDataGridColumnLayout, useDataGridColumnLayoutState } from "@/composables/useDataGridColumnLayout"; @@ -51,6 +51,55 @@ describe("useDataGridColumnLayout", () => { scope.stop(); }); + it("reapplies a persisted null-column preference without losing manual visibility state", async () => { + const scope = effectScope(); + const hideNullColumns = ref(true); + const allNullColumnIndexes = ref([2]); + const state = scope.run(() => + useDataGridColumnLayoutState({ + columns: ref(["id", "name", "empty"]), + sourceColumns: ref(undefined), + commentByColumn: ref(new Map()), + displayableColumnIndexes: ref([0, 1, 2]), + allNullColumnIndexes, + columnOrderKeys: ref(["id\0\0", "name\0\0", "empty\0\0"]), + layoutScopeKey: ref("persisted-null-layout"), + tableScopeKey: ref(""), + hideNullColumns, + onHideNullColumnsChange: (value) => { + hideNullColumns.value = value; + }, + }), + )!; + + expect(state.nullColumnsHidden.value).toBe(true); + expect(state.visibleColumnIndexes.value).toEqual([0, 1]); + + state.toggleColumnVisibility(1); + hideNullColumns.value = false; + await nextTick(); + expect(state.visibleColumnIndexes.value).toEqual([0, 2]); + + hideNullColumns.value = true; + await nextTick(); + expect(state.visibleColumnIndexes.value).toEqual([0]); + + allNullColumnIndexes.value = []; + await nextTick(); + expect(state.nullColumnsHidden.value).toBe(true); + expect(state.visibleColumnIndexes.value).toEqual([0, 2]); + + allNullColumnIndexes.value = [2]; + await nextTick(); + state.resetColumnVisibility(); + expect(state.visibleColumnIndexes.value).toEqual([0, 1]); + + state.toggleAllNullColumns(); + expect(hideNullColumns.value).toBe(false); + expect(state.visibleColumnIndexes.value).toEqual([0, 1, 2]); + scope.stop(); + }); + it("keeps a new resize active when the previous resize completion frame is pending", () => { const frames = new Map(); let nextFrame = 1; diff --git a/apps/desktop/src/composables/useDataGridColumnLayout.ts b/apps/desktop/src/composables/useDataGridColumnLayout.ts index 3dbcdc4a7e..4a7a38d7eb 100644 --- a/apps/desktop/src/composables/useDataGridColumnLayout.ts +++ b/apps/desktop/src/composables/useDataGridColumnLayout.ts @@ -73,10 +73,13 @@ export function useDataGridColumnLayoutState(options: { columnOrderKeys: MaybeRefOrGetter; layoutScopeKey: MaybeRefOrGetter; tableScopeKey: MaybeRefOrGetter; + hideNullColumns?: MaybeRefOrGetter; + onHideNullColumnsChange?: (value: boolean) => void; onRefreshMetrics?: () => void; }) { const hiddenColumnIndexes = ref>(new Set()); - const nullColumnsHidden = ref(false); + const localNullColumnsHidden = ref(false); + const nullColumnsHidden = computed(() => (options.hideNullColumns === undefined ? localNullColumnsHidden.value : toValue(options.hideNullColumns))); const autoHiddenNullColumnIndexes = ref>(new Set()); const persistedColumnOrderKeys = ref([]); const orderedDisplayableColumnIndexes = computed(() => orderedColumnIndexes({ availableIndexes: toValue(options.displayableColumnIndexes), columnKeys: toValue(options.columnOrderKeys), orderedKeys: persistedColumnOrderKeys.value })); @@ -137,16 +140,25 @@ export function useDataGridColumnLayoutState(options: { persistedColumnOrderKeys.value = []; if (options.onRefreshMetrics) nextTick(options.onRefreshMetrics); } - function showAllNullColumns() { + function setNullColumnsHidden(value: boolean) { + if (options.hideNullColumns === undefined) localNullColumnsHidden.value = value; + else options.onHideNullColumnsChange?.(value); + } + function applyNullColumnVisibility(hidden: boolean) { hiddenColumnIndexes.value = removeAutoHiddenColumnIndexes(hiddenColumnIndexes.value, autoHiddenNullColumnIndexes.value); autoHiddenNullColumnIndexes.value = new Set(); - nullColumnsHidden.value = false; - } - function hideAllNullColumns() { + if (!hidden) return; const next = hiddenColumnIndexesWithAllNullColumns({ availableIndexes: [...toValue(options.displayableColumnIndexes)], hiddenIndexes: hiddenColumnIndexes.value, allNullIndexes: new Set(toValue(options.allNullColumnIndexes)) }); hiddenColumnIndexes.value = next.hiddenIndexes; autoHiddenNullColumnIndexes.value = next.autoHiddenIndexes; - nullColumnsHidden.value = next.autoHiddenIndexes.size > 0; + } + function showAllNullColumns() { + setNullColumnsHidden(false); + applyNullColumnVisibility(false); + } + function hideAllNullColumns() { + setNullColumnsHidden(true); + applyNullColumnVisibility(true); } function toggleAllNullColumns() { if (nullColumnsHidden.value) showAllNullColumns(); @@ -163,18 +175,10 @@ export function useDataGridColumnLayoutState(options: { function resetColumnVisibility() { hiddenColumnIndexes.value = new Set(); autoHiddenNullColumnIndexes.value = new Set(); - nullColumnsHidden.value = false; + applyNullColumnVisibility(nullColumnsHidden.value); } - watch( - () => [...toValue(options.allNullColumnIndexes)], - () => { - if (!nullColumnsHidden.value) return; - hiddenColumnIndexes.value = removeAutoHiddenColumnIndexes(hiddenColumnIndexes.value, autoHiddenNullColumnIndexes.value); - autoHiddenNullColumnIndexes.value = new Set(); - hideAllNullColumns(); - }, - ); + watch([() => nullColumnsHidden.value, () => [...toValue(options.allNullColumnIndexes)], () => [...toValue(options.displayableColumnIndexes)]], ([hidden]) => applyNullColumnVisibility(hidden as boolean), { immediate: true }); watch([() => toValue(options.layoutScopeKey), () => toValue(options.tableScopeKey)], loadColumnOrder, { immediate: true }); return { diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts index eacb6bb778..a31257841d 100644 --- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts +++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts @@ -79,6 +79,20 @@ describe("normalizeEditorSettings", () => { expect(normalizeEditorSettings({ dataGridSearchMode: "invalid" as any }).dataGridSearchMode).toBe("filter"); }); + it("defaults persistent data grid view options off and preserves enabled values", () => { + const defaults = normalizeEditorSettings({}); + expect(defaults.dataGridMultiRowTranspose).toBe(false); + expect(defaults.dataGridHideNullColumns).toBe(false); + + const enabled = normalizeEditorSettings({ dataGridMultiRowTranspose: true, dataGridHideNullColumns: true }); + expect(enabled.dataGridMultiRowTranspose).toBe(true); + expect(enabled.dataGridHideNullColumns).toBe(true); + + const invalid = normalizeEditorSettings({ dataGridMultiRowTranspose: "true" as any, dataGridHideNullColumns: 1 as any }); + expect(invalid.dataGridMultiRowTranspose).toBe(false); + expect(invalid.dataGridHideNullColumns).toBe(false); + }); + it("shows cell detail metadata by default and preserves collapsed state", () => { expect(normalizeEditorSettings({}).cellDetailMetadataCollapsed).toBe(false); expect(normalizeEditorSettings({ cellDetailMetadataCollapsed: true }).cellDetailMetadataCollapsed).toBe(true); diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 0c938f82b9..75c4dad99e 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -388,6 +388,8 @@ export interface EditorSettings { dataGridQuickEntry: boolean; dataGridRenderMode: DataGridRenderMode; dataGridSearchMode: DataGridSearchMode; + dataGridMultiRowTranspose: boolean; + dataGridHideNullColumns: boolean; tableFontSize: number; structureEditorDensity: StructureEditorDensity; tableInfoDrawerWidth: number; @@ -530,6 +532,8 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { dataGridQuickEntry: false, dataGridRenderMode: "canvas", dataGridSearchMode: "filter", + dataGridMultiRowTranspose: false, + dataGridHideNullColumns: false, tableFontSize: TABLE_FONT_SIZE_DEFAULT, structureEditorDensity: "compact", tableInfoDrawerWidth: 320, @@ -776,6 +780,8 @@ export function normalizeEditorSettings(settings: Partial, exist dataGridQuickEntry: settings.dataGridQuickEntry ?? DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry, dataGridRenderMode: normalizeDataGridRenderMode(settings.dataGridRenderMode), dataGridSearchMode: normalizeDataGridSearchMode(settings.dataGridSearchMode), + dataGridMultiRowTranspose: settings.dataGridMultiRowTranspose === true, + dataGridHideNullColumns: settings.dataGridHideNullColumns === true, tableFontSize: normalizeTableFontSize(settings.tableFontSize), structureEditorDensity: normalizeStructureEditorDensity(settings.structureEditorDensity), tableInfoDrawerWidth: normalizeDrawerWidth(settings.tableInfoDrawerWidth, 240, DEFAULT_EDITOR_SETTINGS.tableInfoDrawerWidth), @@ -1096,6 +1102,8 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.dataGridQuickEntry !== undefined) editorSettings.value.dataGridQuickEntry = partial.dataGridQuickEntry; if (partial.dataGridRenderMode !== undefined) editorSettings.value.dataGridRenderMode = normalizeDataGridRenderMode(partial.dataGridRenderMode); if (partial.dataGridSearchMode !== undefined) editorSettings.value.dataGridSearchMode = normalizeDataGridSearchMode(partial.dataGridSearchMode); + if (partial.dataGridMultiRowTranspose !== undefined) editorSettings.value.dataGridMultiRowTranspose = partial.dataGridMultiRowTranspose === true; + if (partial.dataGridHideNullColumns !== undefined) editorSettings.value.dataGridHideNullColumns = partial.dataGridHideNullColumns === true; if (partial.tableFontSize !== undefined) editorSettings.value.tableFontSize = normalizeTableFontSize(partial.tableFontSize); if (partial.structureEditorDensity !== undefined) editorSettings.value.structureEditorDensity = normalizeStructureEditorDensity(partial.structureEditorDensity); if (partial.tableInfoDrawerWidth !== undefined) editorSettings.value.tableInfoDrawerWidth = normalizeDrawerWidth(partial.tableInfoDrawerWidth, 240, DEFAULT_EDITOR_SETTINGS.tableInfoDrawerWidth);