🐛 fix: 修复 Issue #726 (Closes #726)

This commit is contained in:
mango
2026-07-27 16:54:46 +08:00
parent 2d1034e77e
commit 83f54d62af
18 changed files with 586 additions and 66 deletions

View File

@@ -1,5 +1,44 @@
import { describe, expect, it } from 'vitest';
import { filterRowsByGridConditions } from './dataGridClientFilter';
import { countGridColumnValues, filterRowsByGridConditions } from './dataGridClientFilter';
describe('countGridColumnValues', () => {
it('counts nullish, empty, scalar, and stable complex values separately', () => {
const rows = [
{ value: null },
{ value: undefined },
{ value: '' },
{ value: 1 },
{ value: '1' },
{ value: true },
{ value: { b: 2, a: 1 } },
{ value: { a: 1, b: 2 } },
{ value: ['x', 1] },
];
expect(countGridColumnValues(rows, 'value')).toEqual([
{ key: 'nullish', display: '', kind: 'nullish', count: 2 },
{ key: 'object:{"a":1,"b":2}', display: '{"a":1,"b":2}', kind: 'value', count: 2 },
{ key: 'empty', display: '', kind: 'empty', count: 1 },
{ key: 'object:["x",1]', display: '["x",1]', kind: 'value', count: 1 },
{ key: 'number:1', display: '1', kind: 'value', count: 1 },
{ key: 'string:1', display: '1', kind: 'value', count: 1 },
{ key: 'boolean:true', display: 'true', kind: 'value', count: 1 },
]);
});
it('sorts by count, display text, and stable key', () => {
expect(countGridColumnValues([
{ value: '10' },
{ value: '2' },
{ value: 2 },
{ value: '10' },
], 'value').map(({ key, count }) => ({ key, count }))).toEqual([
{ key: 'string:10', count: 2 },
{ key: 'number:2', count: 1 },
{ key: 'string:2', count: 1 },
]);
});
});
describe('filterRowsByGridConditions', () => {
const rows = [
@@ -23,6 +62,17 @@ describe('filterRowsByGridConditions', () => {
]).map((row) => row.id)).toEqual([2]);
});
it('uses the stable complex display text for equality filtering', () => {
const rows = [
{ id: 1, value: { b: 2, a: 1 } },
{ id: 2, value: { a: 1, b: 3 } },
];
expect(filterRowsByGridConditions(rows, [
{ column: 'value', op: '=', value: '{"a":1,"b":2}', enabled: true },
]).map((row) => row.id)).toEqual([1]);
});
it('supports null checks and OR logic', () => {
expect(filterRowsByGridConditions(rows, [
{ column: 'status', op: 'IS_NULL', enabled: true },

View File

@@ -1,17 +1,94 @@
import type { FilterCondition } from './sql';
import { parseListValues } from './sql';
export type DataGridColumnValueCount = {
key: string;
display: string;
kind: 'nullish' | 'empty' | 'value';
count: number;
};
const stableSerializeComplexValue = (value: unknown, seen = new WeakSet<object>()): string => {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (typeof value === 'string') return JSON.stringify(value);
if (typeof value === 'number') {
if (Number.isNaN(value)) return 'NaN';
if (value === Number.POSITIVE_INFINITY) return 'Infinity';
if (value === Number.NEGATIVE_INFINITY) return '-Infinity';
if (Object.is(value, -0)) return '-0';
return String(value);
}
if (typeof value === 'boolean') return String(value);
if (typeof value === 'bigint') return `${value}n`;
if (typeof value === 'symbol' || typeof value === 'function') return String(value);
if (seen.has(value)) return '"[Circular]"';
seen.add(value);
try {
if (Array.isArray(value)) {
return `[${value.map((item) => stableSerializeComplexValue(item, seen)).join(',')}]`;
}
if (value instanceof Date) {
return JSON.stringify(Number.isNaN(value.getTime()) ? String(value) : value.toISOString());
}
const entries = Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableSerializeComplexValue((value as Record<string, unknown>)[key], seen)}`);
return `{${entries.join(',')}}`;
} finally {
seen.delete(value);
}
};
const normalizeColumnValueCount = (value: unknown): Omit<DataGridColumnValueCount, 'count'> => {
if (value === null || value === undefined) {
return { key: 'nullish', display: '', kind: 'nullish' };
}
if (value === '') {
return { key: 'empty', display: '', kind: 'empty' };
}
const valueType = typeof value;
const display = valueType === 'object'
? stableSerializeComplexValue(value)
: String(value);
return {
key: `${valueType}:${display}`,
display,
kind: 'value',
};
};
export const countGridColumnValues = <T extends Record<string, any>>(
rows: T[],
columnName: string,
): DataGridColumnValueCount[] => {
const counts = new Map<string, DataGridColumnValueCount>();
rows.forEach((row) => {
const normalized = normalizeColumnValueCount(row?.[columnName]);
const existing = counts.get(normalized.key);
if (existing) {
existing.count += 1;
} else {
counts.set(normalized.key, { ...normalized, count: 1 });
}
});
return Array.from(counts.values()).sort((left, right) => (
right.count - left.count
|| left.display.localeCompare(right.display, undefined, { numeric: true, sensitivity: 'base' })
|| left.key.localeCompare(right.key)
));
};
const normalizeCellText = (value: unknown): string => {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value;
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
return String(value);
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
return stableSerializeComplexValue(value);
};
const isNullishCell = (value: unknown): boolean => value === null || value === undefined;

View File

@@ -37,6 +37,7 @@ export type DetachedQueryResultSnapshot = {
readOnly: boolean;
showRowNumberColumn?: boolean;
truncated?: boolean;
pinned?: boolean;
};
export type DetachedQueryResultWindow = DetachedWindowBounds & {