diff --git a/src/dataViewer.ts b/src/dataViewer.ts index 525e9a811..6cead9d68 100644 --- a/src/dataViewer.ts +++ b/src/dataViewer.ts @@ -121,7 +121,24 @@ export function getDataViewerScript(): string { column.cellDataType = 'number'; } else if (column.type === 'booleanColumn') { column.cellDataType = 'boolean'; - column.filterParams = { buttons: ['reset'] }; + column.filterParams = { + buttons: ['reset', 'apply'], + // Keep AG Grid's boolean choices and include R's NA (null). + filterOptions: [ + 'empty', + { + displayKey: 'true', displayName: 'True', + predicate: (_filterValues, cellValue) => cellValue === true, + numberOfInputs: 0 + }, + { + displayKey: 'false', displayName: 'False', + predicate: (_filterValues, cellValue) => cellValue === false, + numberOfInputs: 0 + }, + 'blank' + ] + }; } if (column.field === '0') { column.pinned = 'left'; diff --git a/src/test/suite/dataViewer.test.ts b/src/test/suite/dataViewer.test.ts index 9bae4fa57..f16d39552 100644 --- a/src/test/suite/dataViewer.test.ts +++ b/src/test/suite/dataViewer.test.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import * as vm from 'vm'; -import type { ColDef, ColumnState, GridOptions, GridState } from 'ag-grid-community'; +import type { ColDef, ColumnState, GridOptions, GridState, IFilterOptionDef, ITextFilterParams } from 'ag-grid-community'; import { getDataViewerScript } from '../../dataViewer'; interface SavedViewer { @@ -215,6 +215,43 @@ suite('Data viewer', () => { assert.strictEqual(rows[1]['2'], ''); }); + test('logical filters offer Reset and Apply with the original labels and a built-in Blank option', () => { + const script = viewerScript().script; + const defs: ColDef[] = [{ field: '1', type: 'booleanColumn' }]; + script.prepareViewerColumns(defs); + const params = defs[0].filterParams as ITextFilterParams; + assert.deepStrictEqual(Array.from(params.buttons ?? []), ['reset', 'apply']); + assert.deepStrictEqual(Array.from(params.filterOptions ?? [], option => + typeof option === 'string' ? option : option.displayKey + ), ['empty', 'true', 'false', 'blank']); + assert.deepStrictEqual(Array.from(params.filterOptions ?? [], option => + typeof option === 'string' ? option : option.displayName + ), ['empty', 'True', 'False', 'blank']); + // Preserve AG Grid's "Choose one" and "Blank" labels. + assert.strictEqual(script.getViewerGridOptions(500).localeText?.empty, undefined); + assert.strictEqual(script.getViewerGridOptions(500).localeText?.blank, undefined); + }); + + for (const [key, expected] of [['true', true], ['false', false]] as const) { + test(`logical ${key} filter matches only its value, excluding NA`, () => { + const script = viewerScript().script; + const defs: ColDef[] = [{ field: '1', type: 'booleanColumn' }]; + script.prepareViewerColumns(defs); + const params = defs[0].filterParams as ITextFilterParams; + const option = params.filterOptions?.find((candidate): candidate is IFilterOptionDef => + typeof candidate !== 'string' && candidate.displayKey === key + ); + assert.ok(option?.predicate); + assert.strictEqual(option.numberOfInputs, 0); + const predicate = option.predicate; + const rows = script.prepareViewerRows( + [true, false, null, undefined].map(value => ({ '1': value })), [] + ); + assert.deepStrictEqual(rows.filter(row => Reflect.apply(predicate, undefined, [[], row['1']])) + .map(row => row['1']), [expected]); + }); + } + test('page-size choices are positive, sorted and unique, including restored sizes', () => { for (const pageSize of [0, 20, 50, 75, 500]) { const first = viewerScript(undefined, { pagination: { pageSize: 200 } });