feat(datagrid): 支持列值多选筛选

This commit is contained in:
Kunghim
2026-08-01 19:32:20 +08:00
parent 5640a36e03
commit 42f34a36f4
15 changed files with 187 additions and 52 deletions

View File

@@ -73,6 +73,16 @@ describe('filterRowsByGridConditions', () => {
]).map((row) => row.id)).toEqual([1]);
});
it('filters a structured value selection including null and empty values', () => {
const rowsWithEmpty = [...rows, { id: 4, name: 'Dora', status: '' }];
expect(filterRowsByGridConditions(rowsWithEmpty, [{
column: 'status',
op: 'IN',
valueSelection: { values: ['active'], includeNull: true, includeEmpty: true },
enabled: true,
}]).map((row) => row.id)).toEqual([1, 3, 4]);
});
it('supports null checks and OR logic', () => {
expect(filterRowsByGridConditions(rows, [
{ column: 'status', op: 'IS_NULL', enabled: true },

View File

@@ -124,6 +124,13 @@ const matchSingleCondition = (row: Record<string, any>, condition: FilterConditi
const cell = row?.[column];
const cellText = normalizeCellText(cell);
const valueSelection = condition?.valueSelection;
if (valueSelection) {
return (valueSelection.values || []).some((item) => cellText === String(item))
|| (!!valueSelection.includeNull && isNullishCell(cell))
|| (!!valueSelection.includeEmpty && !isNullishCell(cell) && isEmptyCell(cell));
}
switch (op) {
case 'IS_NULL':
return isNullishCell(cell);

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { buildOrderBySQL, buildPaginatedSelectSQL, quoteQualifiedIdent, reverseOrderBySQL } from './sql';
import { buildOrderBySQL, buildPaginatedSelectSQL, buildWhereSQL, quoteQualifiedIdent, reverseOrderBySQL } from './sql';
describe('buildOrderBySQL', () => {
it('does not add fallback ORDER BY for DuckDB without explicit sort', () => {
@@ -12,6 +12,16 @@ describe('buildOrderBySQL', () => {
});
});
describe('buildWhereSQL', () => {
it('builds one grouped expression for a structured value selection', () => {
expect(buildWhereSQL('mysql', [{
column: 'status',
op: 'IN',
valueSelection: { values: ['active', 'waiting,review'], includeNull: true, includeEmpty: true },
}])).toBe("WHERE ((`status` IN ('active', 'waiting,review') OR `status` IS NULL OR `status` = ''))");
});
});
describe('buildPaginatedSelectSQL', () => {
it('uses SQL Server TOP for the first page to support old compatibility levels', () => {
const sql = buildPaginatedSelectSQL('sqlserver', 'SELECT * FROM [Users]', ' ORDER BY [ID] ASC', 101, 0);

View File

@@ -1,5 +1,11 @@
import { splitQualifiedNameSegments, stripIdentifierQuotes } from './qualifiedName';
export type FilterValueSelection = {
values: string[];
includeNull?: boolean;
includeEmpty?: boolean;
};
export type FilterCondition = {
id?: number;
enabled?: boolean;
@@ -8,6 +14,7 @@ export type FilterCondition = {
op?: string;
value?: string;
value2?: string;
valueSelection?: FilterValueSelection;
};
const normalizeIdentPart = (ident: string) => stripIdentifierQuotes(ident);
@@ -369,6 +376,19 @@ export const buildWhereSQL = (dbType: string, conditions: FilterCondition[]) =>
if (!column) return;
const col = quoteIdentPart(dbType, column);
const valueSelection = cond?.valueSelection;
if (valueSelection) {
const selectedValues = Array.from(new Set((valueSelection.values || []).map((item) => String(item))));
const selectionParts: string[] = [];
if (selectedValues.length > 0) {
selectionParts.push(`${col} IN (${selectedValues.map((item) => `'${escapeLiteral(item)}'`).join(', ')})`);
}
if (valueSelection.includeNull) selectionParts.push(`${col} IS NULL`);
if (valueSelection.includeEmpty) selectionParts.push(`${col} = ''`);
if (selectionParts.length === 0) return;
appendWherePart(selectionParts.length === 1 ? selectionParts[0] : `(${selectionParts.join(' OR ')})`);
return;
}
switch (op) {
case 'IS_NULL':