diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index fbeea038a..18fcb8007 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -170,9 +170,12 @@ import { resultPageSizeMenuOptions, } from "@/lib/paginationPageSize"; import { + allNullColumnIndexes, filterColumnVisibilityOptions, + hiddenColumnIndexesWithAllNullColumns, invertedHiddenColumnIndexes, nextHiddenColumnIndexes, + removeAutoHiddenColumnIndexes, visibleColumnIndexesForFilter, } from "@/lib/dataGridColumnVisibility"; @@ -1436,6 +1439,8 @@ const gridRef = ref(); const headerRef = ref(); const gridScrollbarGutter = ref(0); const hiddenColumnIndexes = ref>(new Set()); +const nullColumnsHidden = ref(false); +const autoHiddenNullColumnIndexes = ref>(new Set()); const highlightedColumnIndex = ref(null); let highlightedColumnTimer = 0; const displayableColumnIndexes = computed(() => @@ -1455,6 +1460,13 @@ const visibleSourceColumns = computed(() => { const visibleColumnCount = computed(() => visibleColumnIndexes.value.length); const displayableColumnCount = computed(() => displayableColumnIndexes.value.length); const hiddenColumnCount = computed(() => displayableColumnCount.value - visibleColumnCount.value); +const allNullColumnIndexesForResult = computed(() => + allNullColumnIndexes(props.result.rows, displayableColumnIndexes.value), +); +const allNullColumnCount = computed(() => allNullColumnIndexesForResult.value.length); +const canToggleAllNullColumns = computed( + () => nullColumnsHidden.value || (allNullColumnCount.value > 0 && displayableColumnCount.value > 1), +); function filteredColumnVisibilityOptions(query: string) { const displayable = new Set(displayableColumnIndexes.value); return filterColumnVisibilityOptions(props.result.columns, query).filter((option) => displayable.has(option.index)); @@ -1475,6 +1487,40 @@ function showAllColumns() { function invertColumnVisibility() { hiddenColumnIndexes.value = invertedHiddenColumnIndexes(displayableColumnIndexes.value, hiddenColumnIndexes.value); } +function showAllNullColumns() { + hiddenColumnIndexes.value = removeAutoHiddenColumnIndexes( + hiddenColumnIndexes.value, + autoHiddenNullColumnIndexes.value, + ); + autoHiddenNullColumnIndexes.value = new Set(); + nullColumnsHidden.value = false; +} +function hideAllNullColumns() { + const next = hiddenColumnIndexesWithAllNullColumns({ + availableIndexes: displayableColumnIndexes.value, + hiddenIndexes: hiddenColumnIndexes.value, + allNullIndexes: new Set(allNullColumnIndexesForResult.value), + }); + hiddenColumnIndexes.value = next.hiddenIndexes; + autoHiddenNullColumnIndexes.value = next.autoHiddenIndexes; + nullColumnsHidden.value = next.autoHiddenIndexes.size > 0; +} +function toggleAllNullColumns() { + if (nullColumnsHidden.value) { + showAllNullColumns(); + } else { + hideAllNullColumns(); + } +} +watch(allNullColumnIndexesForResult, () => { + if (!nullColumnsHidden.value) return; + hiddenColumnIndexes.value = removeAutoHiddenColumnIndexes( + hiddenColumnIndexes.value, + autoHiddenNullColumnIndexes.value, + ); + autoHiddenNullColumnIndexes.value = new Set(); + hideAllNullColumns(); +}); const firstVisibleColumnIndex = computed(() => visibleColumnIndexes.value[0] ?? 0); function actualColumnIndex(visibleColumnIndex: number): number { return visibleColumnIndexes.value[visibleColumnIndex] ?? visibleColumnIndex; @@ -1690,6 +1736,8 @@ watch( () => { localColumnFilters.value = {}; hiddenColumnIndexes.value = new Set(); + nullColumnsHidden.value = false; + autoHiddenNullColumnIndexes.value = new Set(); closeLocalFilter(); }, ); @@ -4896,6 +4944,10 @@ defineExpose({ toggleColumnVisibility, showAllColumns, invertColumnVisibility, + nullColumnsHidden, + allNullColumnCount, + canToggleAllNullColumns, + toggleAllNullColumns, openCellDetailSearch, }); diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 80a92eefe..cf5ec5ea2 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -15,6 +15,7 @@ import { ChevronDown, ChevronUp, RefreshCcw, + Wrench, } from "lucide-vue-next"; import { Splitpanes, Pane } from "splitpanes"; import "splitpanes/dist/splitpanes.css"; @@ -55,6 +56,10 @@ type DataGridHandle = { toggleColumnVisibility: (columnIndex: number) => void; showAllColumns: () => void; invertColumnVisibility: () => void; + nullColumnsHidden: boolean; + allNullColumnCount: number; + canToggleAllNullColumns: boolean; + toggleAllNullColumns: () => void; showDdl: boolean; toggleDdl: () => void; }; @@ -412,11 +417,56 @@ defineExpose({ focusSearch, refreshData, handleModRTarget }); {{ t("chart.title") }} + + + + + +
+
{{ t("grid.viewOptions") }}
+
+ +
+
+ + + + + +
+
{{ t("grid.viewOptions") }}
+
+ +
+
>, + availableIndexes: number[], +): number[] { + if (rows.length === 0) return []; + return availableIndexes.filter((index) => rows.every((row) => row[index] === null)); +} + +export function hiddenColumnIndexesWithAllNullColumns(options: { + availableIndexes: number[]; + hiddenIndexes: ReadonlySet; + allNullIndexes: ReadonlySet; +}): { hiddenIndexes: Set; autoHiddenIndexes: Set } { + const next = new Set(options.hiddenIndexes); + const autoHidden = new Set(); + const available = new Set(options.availableIndexes); + + for (const index of options.allNullIndexes) { + if (!available.has(index) || next.has(index)) continue; + next.add(index); + autoHidden.add(index); + } + + const hasVisibleColumn = options.availableIndexes.some((index) => !next.has(index)); + if (!hasVisibleColumn && options.availableIndexes.length > 0) { + const restoredIndex = + options.availableIndexes.find((index) => autoHidden.has(index)) ?? options.availableIndexes[0]; + next.delete(restoredIndex); + autoHidden.delete(restoredIndex); + } + + return { hiddenIndexes: next, autoHiddenIndexes: autoHidden }; +} + +export function removeAutoHiddenColumnIndexes( + hiddenIndexes: ReadonlySet, + autoHiddenIndexes: ReadonlySet, +): Set { + const next = new Set(hiddenIndexes); + for (const index of autoHiddenIndexes) { + next.delete(index); + } + return next; +} diff --git a/packages/app-tests/dataGridColumnVisibility.test.ts b/packages/app-tests/dataGridColumnVisibility.test.ts index 64b346f88..f28aa0475 100644 --- a/packages/app-tests/dataGridColumnVisibility.test.ts +++ b/packages/app-tests/dataGridColumnVisibility.test.ts @@ -1,9 +1,12 @@ import { strict as assert } from "node:assert"; import test from "node:test"; import { + allNullColumnIndexes, filterColumnVisibilityOptions, + hiddenColumnIndexesWithAllNullColumns, invertedHiddenColumnIndexes, nextHiddenColumnIndexes, + removeAutoHiddenColumnIndexes, visibleColumnIndexesForFilter, } from "../../apps/desktop/src/lib/dataGridColumnVisibility.ts"; @@ -50,3 +53,42 @@ test("keeps one column visible when inverting all visible columns", () => { assert.deepEqual([...hidden].sort(), [1, 2]); }); + +test("finds columns where every row is NULL", () => { + const indexes = allNullColumnIndexes( + [ + [1, null, null, "x"], + [2, null, null, null], + ], + [0, 1, 2, 3], + ); + + assert.deepEqual(indexes, [1, 2]); +}); + +test("does not treat empty results as all-null columns", () => { + assert.deepEqual(allNullColumnIndexes([], [0, 1, 2]), []); +}); + +test("hides all-null columns without restoring manually hidden columns", () => { + const result = hiddenColumnIndexesWithAllNullColumns({ + availableIndexes: [0, 1, 2, 3], + hiddenIndexes: new Set([3]), + allNullIndexes: new Set([1, 2, 3]), + }); + + assert.deepEqual([...result.hiddenIndexes].sort(), [1, 2, 3]); + assert.deepEqual([...result.autoHiddenIndexes].sort(), [1, 2]); + assert.deepEqual([...removeAutoHiddenColumnIndexes(result.hiddenIndexes, result.autoHiddenIndexes)].sort(), [3]); +}); + +test("keeps one all-null column visible when every column is NULL", () => { + const result = hiddenColumnIndexesWithAllNullColumns({ + availableIndexes: [0, 1, 2], + hiddenIndexes: new Set(), + allNullIndexes: new Set([0, 1, 2]), + }); + + assert.deepEqual([...result.hiddenIndexes].sort(), [1, 2]); + assert.deepEqual([...result.autoHiddenIndexes].sort(), [1, 2]); +});