From 92f052fa6cae1f14e48b98aef662a5864c9d5cb6 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 8 Sep 2026 21:12:04 +0800 Subject: [PATCH] refactor(grid): extract local filter value logic --- apps/desktop/src/components/grid/DataGrid.vue | 56 +++------------ .../dataGridLocalColumnFilterState.spec.ts | 28 +++++++- .../dataGridLocalColumnFilterState.ts | 72 +++++++++++++++++++ 3 files changed, 110 insertions(+), 46 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index bdcb4d4181..19d70c39db 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -377,7 +377,7 @@ import { reserveDataGridHeaderLine } from "@/lib/dataGrid/dataGridHeaderLayout"; import { buildColumnIndexMap, columnIndexColorClass, columnIndexMetadataRequestCurrent, columnIndexNameKey, columnIndexTableIdentity, type ColumnIndexKind } from "@/lib/dataGrid/dataGridColumnIndexIcon"; import { supportsTableStructureEditing } from "@/lib/database/databaseCapabilities"; import { rememberDataGridConditionHistory } from "@/lib/dataGrid/dataGridConditionHistory"; -import { restoreDataGridLocalColumnFilters, serializeDataGridLocalColumnFilters } from "@/lib/dataGrid/dataGridLocalColumnFilterState"; +import { buildDataGridLocalFilterOptions, dataGridLocalFilterKey, dataGridLocalFilterLabel, restoreDataGridLocalColumnFilters, rowMatchesDataGridLocalColumnFilters, serializeDataGridLocalColumnFilters, type DataGridLocalFilterOption } from "@/lib/dataGrid/dataGridLocalColumnFilterState"; import { effectiveDatabaseTypeForConnection, gaussdbCountQueryDopHint } from "@/lib/database/jdbcDialect"; import { mongoCollectionSupportsIndexes, supportsMongoIndexMutations } from "@/lib/mongo/mongoCapabilities"; import { refreshLoadedMongoIndexes } from "@/lib/mongo/mongoIndexMetadata"; @@ -1061,12 +1061,7 @@ const conditionHistoryScope = computed(() => ({ tableName: props.tableMeta?.tableName, })); type LocalFilterMode = "local" | "server"; -type LocalFilterOption = { - key: string; - label: string; - count: number | null; - value: CellValue; -}; +type LocalFilterOption = DataGridLocalFilterOption; type LocalColumnFilterDraft = { columnIndex: number; @@ -1300,14 +1295,11 @@ const savedCustomFormatters = computed(() => { }); function localFilterKey(value: CellValue): string { - if (value === null) return "__dbx_null__"; - if (typeof value === "boolean") return `bool:${value}`; - if (typeof value === "number") return `num:${value}`; - return `str:${String(value)}`; + return dataGridLocalFilterKey(value); } function localFilterLabel(value: CellValue, columnIndex: number): string { - return value === null ? "NULL" : formatCellCached(value, columnIndex); + return dataGridLocalFilterLabel(value, columnIndex, formatCellCached); } function localFilterActive(colIdx: number): boolean { @@ -1345,9 +1337,7 @@ const localFilterSummaries = computed(() => ); function rowMatchesLocalColumnFilters(data: CellValue[]): boolean { - const activeEntries = Object.entries(localColumnFilters.value).filter(([, selected]) => selected.size > 0); - if (activeEntries.length === 0) return true; - return activeEntries.every(([columnIndex, selected]) => selected.has(localFilterKey(data[Number(columnIndex)] ?? null))); + return rowMatchesDataGridLocalColumnFilters(data, localColumnFilters.value); } const localFilteredRows = computed(() => { @@ -1366,36 +1356,12 @@ const localFilteredRows = computed(() => { }); function buildLocalFilterOptions(columnIndex: number): LocalFilterOption[] { - const byKey = new Map(); - const addValue = (value: CellValue) => { - const key = localFilterKey(value); - const current = byKey.get(key); - if (current) { - current.count = (current.count ?? 0) + 1; - } else { - byKey.set(key, { - key, - label: localFilterLabel(value, columnIndex), - count: 1, - value, - }); - } - }; - - for (const [sourceIndex, row] of props.result.rows.entries()) { - addValue(rowDataWithChanges(row, sourceIndex)[columnIndex] ?? null); - } - for (const row of newRows.value) { - addValue(row[columnIndex] ?? null); - } - - return [...byKey.values()].sort((a, b) => { - if (a.value === null && b.value !== null) return -1; - if (a.value !== null && b.value === null) return 1; - return a.label.localeCompare(b.label, undefined, { - numeric: true, - sensitivity: "base", - }); + return buildDataGridLocalFilterOptions({ + rows: props.result.rows, + newRows: newRows.value, + columnIndex, + getRowData: rowDataWithChanges, + formatValue: formatCellCached, }); } diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridLocalColumnFilterState.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridLocalColumnFilterState.spec.ts index 0af87929d1..12454e5c0c 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/dataGridLocalColumnFilterState.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridLocalColumnFilterState.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { restoreDataGridLocalColumnFilters, serializeDataGridLocalColumnFilters } from "@/lib/dataGrid/dataGridLocalColumnFilterState"; +import { buildDataGridLocalFilterOptions, dataGridLocalFilterKey, restoreDataGridLocalColumnFilters, rowMatchesDataGridLocalColumnFilters, serializeDataGridLocalColumnFilters } from "@/lib/dataGrid/dataGridLocalColumnFilterState"; describe("data grid local column filter state", () => { it("round-trips selected values so a remounted grid restores its filters", () => { @@ -27,4 +27,30 @@ describe("data grid local column filter state", () => { expect(restored).toEqual({}); }); + + it("builds stable keys and filters rows across multiple columns", () => { + expect(dataGridLocalFilterKey(null)).toBe("__dbx_null__"); + expect(dataGridLocalFilterKey(true)).toBe("bool:true"); + expect(dataGridLocalFilterKey(42)).toBe("num:42"); + expect(dataGridLocalFilterKey("active")).toBe("str:active"); + + expect(rowMatchesDataGridLocalColumnFilters(["active", 42], { 0: new Set(["str:active"]), 1: new Set(["num:42"]) })).toBe(true); + expect(rowMatchesDataGridLocalColumnFilters(["active", 7], { 0: new Set(["str:active"]), 1: new Set(["num:42"]) })).toBe(false); + }); + + it("deduplicates local filter options and sorts null first", () => { + const options = buildDataGridLocalFilterOptions({ + rows: [{ values: ["beta"] }, { values: [null] }, { values: ["beta"] }], + newRows: [["alpha"]], + columnIndex: 0, + getRowData: (row) => row.values, + formatValue: (value) => String(value), + }); + + expect(options).toEqual([ + { key: "__dbx_null__", label: "NULL", count: 1, value: null }, + { key: "str:alpha", label: "alpha", count: 1, value: "alpha" }, + { key: "str:beta", label: "beta", count: 2, value: "beta" }, + ]); + }); }); diff --git a/apps/desktop/src/lib/dataGrid/dataGridLocalColumnFilterState.ts b/apps/desktop/src/lib/dataGrid/dataGridLocalColumnFilterState.ts index 50f077f67d..d6e6df7c3c 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridLocalColumnFilterState.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridLocalColumnFilterState.ts @@ -1,5 +1,77 @@ +import type { CellValue } from "./cellValue"; + export type SerializedDataGridLocalColumnFilters = Record; +export type DataGridLocalFilterOption = { + key: string; + label: string; + count: number | null; + value: CellValue; +}; + +export function dataGridLocalFilterKey(value: CellValue): string { + if (value === null) return "__dbx_null__"; + if (typeof value === "boolean") return `bool:${value}`; + if (typeof value === "number") return `num:${value}`; + return `str:${String(value)}`; +} + +export function dataGridLocalFilterLabel(value: CellValue, columnIndex: number, formatValue: (value: CellValue, columnIndex: number) => string): string { + return value === null ? "NULL" : formatValue(value, columnIndex); +} + +export function rowMatchesDataGridLocalColumnFilters(data: readonly CellValue[], filters: Readonly>>): boolean { + const activeEntries = Object.entries(filters).filter(([, selected]) => selected.size > 0); + if (activeEntries.length === 0) return true; + return activeEntries.every(([columnIndex, selected]) => selected.has(dataGridLocalFilterKey(data[Number(columnIndex)] ?? null))); +} + +export function buildDataGridLocalFilterOptions({ + rows, + newRows, + columnIndex, + getRowData, + formatValue, +}: { + rows: readonly TRow[]; + newRows: readonly (readonly CellValue[])[]; + columnIndex: number; + getRowData: (row: TRow, sourceIndex: number) => readonly CellValue[]; + formatValue: (value: CellValue, columnIndex: number) => string; +}): DataGridLocalFilterOption[] { + const byKey = new Map(); + const addValue = (value: CellValue) => { + const key = dataGridLocalFilterKey(value); + const current = byKey.get(key); + if (current) { + current.count = (current.count ?? 0) + 1; + } else { + byKey.set(key, { + key, + label: dataGridLocalFilterLabel(value, columnIndex, formatValue), + count: 1, + value, + }); + } + }; + + for (const [sourceIndex, row] of rows.entries()) { + addValue(getRowData(row, sourceIndex)[columnIndex] ?? null); + } + for (const row of newRows) { + addValue(row[columnIndex] ?? null); + } + + return [...byKey.values()].sort((a, b) => { + if (a.value === null && b.value !== null) return -1; + if (a.value !== null && b.value === null) return 1; + return a.label.localeCompare(b.label, undefined, { + numeric: true, + sensitivity: "base", + }); + }); +} + export function restoreDataGridLocalColumnFilters(serialized: SerializedDataGridLocalColumnFilters | undefined, columnCount: number): Record> { if (!serialized || typeof serialized !== "object") return {};