feat(grid): hide all-null columns
This commit is contained in:
parent
0dbb30f614
commit
4ce010369b
|
|
@ -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<HTMLDivElement>();
|
|||
const headerRef = ref<HTMLDivElement>();
|
||||
const gridScrollbarGutter = ref(0);
|
||||
const hiddenColumnIndexes = ref<Set<number>>(new Set());
|
||||
const nullColumnsHidden = ref(false);
|
||||
const autoHiddenNullColumnIndexes = ref<Set<number>>(new Set());
|
||||
const highlightedColumnIndex = ref<number | null>(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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
|||
<BarChart3 class="h-3.5 w-3.5" />
|
||||
{{ t("chart.title") }}
|
||||
</Button>
|
||||
<Popover v-if="activeOutputView === 'result' && activeTab.result">
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="ml-auto h-6 w-7 shrink-0 text-foreground hover:bg-accent"
|
||||
:class="{ 'bg-accent text-foreground': dataGridRef?.nullColumnsHidden }"
|
||||
:title="t('grid.viewOptions')"
|
||||
:aria-label="t('grid.viewOptions')"
|
||||
>
|
||||
<Wrench class="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
class="w-max min-w-44 max-w-[calc(100vw-2rem)] gap-0 overflow-hidden rounded-xl border bg-popover p-0 text-popover-foreground shadow-xl"
|
||||
@click.stop
|
||||
@keydown.stop
|
||||
>
|
||||
<div class="border-b bg-muted/40 px-3 py-2">
|
||||
<div class="text-xs font-semibold">{{ t("grid.viewOptions") }}</div>
|
||||
</div>
|
||||
<label
|
||||
class="flex cursor-pointer items-center gap-2 px-3 py-2 text-xs hover:bg-accent"
|
||||
:class="{ 'cursor-not-allowed opacity-60': !dataGridRef?.canToggleAllNullColumns }"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-3.5 w-3.5 shrink-0 accent-primary"
|
||||
:checked="!!dataGridRef?.nullColumnsHidden"
|
||||
:disabled="!dataGridRef?.canToggleAllNullColumns"
|
||||
@change="dataGridRef?.toggleAllNullColumns()"
|
||||
/>
|
||||
<span class="min-w-0 flex items-center gap-1 font-medium">
|
||||
{{ t("grid.hideNullColumns") }}
|
||||
<span
|
||||
v-if="(dataGridRef?.allNullColumnCount ?? 0) > 0"
|
||||
class="text-muted-foreground tabular-nums"
|
||||
>
|
||||
({{ dataGridRef?.allNullColumnCount }})
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button
|
||||
v-if="activeOutputView === 'result' && activeTab.result"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="ml-auto h-6 shrink-0 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||
class="h-6 shrink-0 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||
:disabled="activeTab.isExecuting"
|
||||
@click="refreshData"
|
||||
>
|
||||
|
|
@ -660,6 +710,48 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
|
|||
>
|
||||
<TableProperties class="h-3.5 w-3.5" /> {{ t("grid.tableInfo") }}
|
||||
</Button>
|
||||
<Popover v-if="activeTab.result?.columns.length">
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-7 shrink-0 text-foreground hover:bg-accent"
|
||||
:class="{ 'bg-accent text-foreground': dataGridRef?.nullColumnsHidden }"
|
||||
:title="t('grid.viewOptions')"
|
||||
:aria-label="t('grid.viewOptions')"
|
||||
>
|
||||
<Wrench class="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
class="w-max min-w-44 max-w-[calc(100vw-2rem)] gap-0 overflow-hidden rounded-xl border bg-popover p-0 text-popover-foreground shadow-xl"
|
||||
@click.stop
|
||||
@keydown.stop
|
||||
>
|
||||
<div class="border-b bg-muted/40 px-3 py-2">
|
||||
<div class="text-xs font-semibold">{{ t("grid.viewOptions") }}</div>
|
||||
</div>
|
||||
<label
|
||||
class="flex cursor-pointer items-center gap-2 px-3 py-2 text-xs hover:bg-accent"
|
||||
:class="{ 'cursor-not-allowed opacity-60': !dataGridRef?.canToggleAllNullColumns }"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-3.5 w-3.5 shrink-0 accent-primary"
|
||||
:checked="!!dataGridRef?.nullColumnsHidden"
|
||||
:disabled="!dataGridRef?.canToggleAllNullColumns"
|
||||
@change="dataGridRef?.toggleAllNullColumns()"
|
||||
/>
|
||||
<span class="min-w-0 flex items-center gap-1 font-medium">
|
||||
{{ t("grid.hideNullColumns") }}
|
||||
<span v-if="(dataGridRef?.allNullColumnCount ?? 0) > 0" class="text-muted-foreground tabular-nums">
|
||||
({{ dataGridRef?.allNullColumnCount }})
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<DataGrid
|
||||
v-if="activeTab.result"
|
||||
|
|
|
|||
|
|
@ -500,6 +500,9 @@ export default {
|
|||
searchColumns: "Search columns...",
|
||||
invertColumnVisibility: "Invert",
|
||||
showAllColumns: "Show all",
|
||||
viewOptions: "View options",
|
||||
hideNullColumns: "Hide NULL",
|
||||
hideNullColumnsHint: "Toggle columns whose values are all NULL in the current result.",
|
||||
moreValues: "{count} more values, keep typing to narrow results",
|
||||
filterByValue: "Filter by This Value",
|
||||
filterExcludeValue: "Exclude This Value",
|
||||
|
|
|
|||
|
|
@ -468,6 +468,9 @@ export default {
|
|||
searchColumns: "Buscar columnas...",
|
||||
invertColumnVisibility: "Invertir",
|
||||
showAllColumns: "Mostrar todo",
|
||||
viewOptions: "Opciones de vista",
|
||||
hideNullColumns: "Ocultar NULL",
|
||||
hideNullColumnsHint: "Alterna columnas cuyos valores son todos NULL en el resultado actual.",
|
||||
moreValues: "{count} valores más, sigue escribiendo para acotar los resultados",
|
||||
filterByValue: "Filtrar por este valor",
|
||||
filterExcludeValue: "Excluir este valor",
|
||||
|
|
|
|||
|
|
@ -495,6 +495,9 @@ export default {
|
|||
searchColumns: "搜索字段...",
|
||||
invertColumnVisibility: "反选",
|
||||
showAllColumns: "显示全部",
|
||||
viewOptions: "视图选项",
|
||||
hideNullColumns: "隐藏 NULL 列",
|
||||
hideNullColumnsHint: "切换隐藏当前结果中整列均为 NULL 的字段。",
|
||||
moreValues: "还有 {count} 个值,输入关键词继续缩小范围",
|
||||
filterByValue: "筛选此值",
|
||||
filterExcludeValue: "排除此值",
|
||||
|
|
|
|||
|
|
@ -495,6 +495,9 @@ export default {
|
|||
searchColumns: "搜尋欄位……",
|
||||
invertColumnVisibility: "反選",
|
||||
showAllColumns: "顯示全部",
|
||||
viewOptions: "檢視選項",
|
||||
hideNullColumns: "隱藏 NULL 欄",
|
||||
hideNullColumnsHint: "切換隱藏目前結果中整欄皆為 NULL 的欄位。",
|
||||
moreValues: "還有 {count} 個值,輸入關鍵字繼續縮小範圍",
|
||||
filterByValue: "篩選此值",
|
||||
filterExcludeValue: "排除此值",
|
||||
|
|
|
|||
|
|
@ -44,3 +44,48 @@ export function invertedHiddenColumnIndexes(
|
|||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function allNullColumnIndexes(
|
||||
rows: ReadonlyArray<ReadonlyArray<unknown>>,
|
||||
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<number>;
|
||||
allNullIndexes: ReadonlySet<number>;
|
||||
}): { hiddenIndexes: Set<number>; autoHiddenIndexes: Set<number> } {
|
||||
const next = new Set(options.hiddenIndexes);
|
||||
const autoHidden = new Set<number>();
|
||||
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<number>,
|
||||
autoHiddenIndexes: ReadonlySet<number>,
|
||||
): Set<number> {
|
||||
const next = new Set(hiddenIndexes);
|
||||
for (const index of autoHiddenIndexes) {
|
||||
next.delete(index);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue