diff --git a/packages/grid/_modules_/grid/components/panel/filterPanel/GridFilterForm.tsx b/packages/grid/_modules_/grid/components/panel/filterPanel/GridFilterForm.tsx index 4d1642a5ed5e..466737c41bb0 100644 --- a/packages/grid/_modules_/grid/components/panel/filterPanel/GridFilterForm.tsx +++ b/packages/grid/_modules_/grid/components/panel/filterPanel/GridFilterForm.tsx @@ -218,7 +218,7 @@ export function GridFilterForm(props: GridFilterFormProps) { - {currentColumn && currentOperator && ( + {currentOperator?.InputComponent && ( GridFilterModelState export const activeGridFilterItemsSelector = createSelector< GridState, GridFilterModelState, + GridColumnLookup, GridFilterItem[] ->( - filterGridStateSelector, - (filterModel) => - filterModel.items?.filter((item) => item.value != null && item.value?.toString() !== ''), // allows to count false or 0 +>(filterGridStateSelector, gridColumnLookupSelector, (filterModel, columnLookup) => + filterModel.items?.filter((item) => { + if (!item.columnField) { + return false; + } + const column = columnLookup[item.columnField]; + if (!column?.filterOperators || column?.filterOperators?.length === 0) { + return false; + } + const filterOperator = column.filterOperators.find( + (operator) => operator.value === item.operatorValue, + ); + if (!filterOperator) { + return false; + } + return !filterOperator.InputComponent || (item.value != null && item.value?.toString() !== ''); + }), ); export const filterGridItemsCounterSelector = createSelector( diff --git a/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts b/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts index b4e5fc4bfa22..a9925e1d431f 100644 --- a/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts +++ b/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts @@ -60,7 +60,7 @@ export const useGridFilter = ( const applyFilter = React.useCallback( (filterItem: GridFilterItem, linkOperator: GridLinkOperator = GridLinkOperator.And) => { - if (!filterItem.columnField || !filterItem.operatorValue || !filterItem.value) { + if (!filterItem.columnField || !filterItem.operatorValue) { return; } @@ -93,6 +93,9 @@ export const useGridFilter = ( } const applyFilterOnRow = filterOperator.getApplyFilterFn(newFilterItem, column)!; + if (typeof applyFilterOnRow !== 'function') { + return; + } setGridState((state) => { const visibleRowsLookup = { ...state.visibleRows.visibleRowsLookup }; diff --git a/packages/grid/_modules_/grid/locales/bgBG.ts b/packages/grid/_modules_/grid/locales/bgBG.ts index f9dce7426541..cdcde42c387a 100644 --- a/packages/grid/_modules_/grid/locales/bgBG.ts +++ b/packages/grid/_modules_/grid/locales/bgBG.ts @@ -54,6 +54,8 @@ const bgBGGrid: Partial = { filterOperatorOnOrBefore: 'е на или преди', filterPanelInputLabel: 'Стойност', filterPanelInputPlaceholder: 'Стойност на филтъра', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Column menu text columnMenuLabel: 'Меню', diff --git a/packages/grid/_modules_/grid/locales/csCZ.ts b/packages/grid/_modules_/grid/locales/csCZ.ts index cb85dbd5c9dd..390fdabebc2d 100644 --- a/packages/grid/_modules_/grid/locales/csCZ.ts +++ b/packages/grid/_modules_/grid/locales/csCZ.ts @@ -67,6 +67,8 @@ const csCZKGrid: Partial = { filterOperatorOnOrAfter: 'je na nebo po', filterOperatorBefore: 'je před', filterOperatorOnOrBefore: 'je na nebo dříve', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Filter values text filterValueAny: 'jakýkoliv', diff --git a/packages/grid/_modules_/grid/locales/deDE.ts b/packages/grid/_modules_/grid/locales/deDE.ts index b7b717aef22e..d12a1431851f 100644 --- a/packages/grid/_modules_/grid/locales/deDE.ts +++ b/packages/grid/_modules_/grid/locales/deDE.ts @@ -60,6 +60,8 @@ const deDEGrid: Partial = { filterOperatorBefore: 'ist vor', filterOperatorOnOrBefore: 'ist an oder vor', filterOperatorAfter: 'ist nach', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Column menu text columnMenuLabel: 'Menü', diff --git a/packages/grid/_modules_/grid/locales/elGR.ts b/packages/grid/_modules_/grid/locales/elGR.ts index 4f3dda815d24..1b0a71d3c791 100644 --- a/packages/grid/_modules_/grid/locales/elGR.ts +++ b/packages/grid/_modules_/grid/locales/elGR.ts @@ -59,6 +59,8 @@ const elGRGrid: Partial = { filterOperatorOnOrAfter: 'είναι ίσο ή μετά', filterOperatorBefore: 'είναι πριν', filterOperatorOnOrBefore: 'είναι ίσο ή πριν', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Column menu text columnMenuLabel: 'Μενού', diff --git a/packages/grid/_modules_/grid/locales/esES.ts b/packages/grid/_modules_/grid/locales/esES.ts index cd6f739f9948..3c5ef78296a2 100644 --- a/packages/grid/_modules_/grid/locales/esES.ts +++ b/packages/grid/_modules_/grid/locales/esES.ts @@ -60,6 +60,8 @@ const esESGrid: Partial = { filterOperatorOnOrAfter: 'es en o posterior', filterOperatorBefore: 'es anterior', filterOperatorOnOrBefore: 'es en o anterior', + filterOperatorIsEmpty: 'está vacío', + filterOperatorIsNotEmpty: 'no esta vacío', // Column menu text columnMenuLabel: 'Menú', diff --git a/packages/grid/_modules_/grid/locales/frFR.ts b/packages/grid/_modules_/grid/locales/frFR.ts index f51a8e5d54d0..79c23d6e6282 100644 --- a/packages/grid/_modules_/grid/locales/frFR.ts +++ b/packages/grid/_modules_/grid/locales/frFR.ts @@ -60,6 +60,8 @@ const frFRGrid: Partial = { filterOperatorAfter: 'postérieur', filterOperatorOnOrBefore: 'égal ou antérieur', filterOperatorBefore: 'antérieur', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Column menu text columnMenuLabel: 'Menu', diff --git a/packages/grid/_modules_/grid/locales/itIT.ts b/packages/grid/_modules_/grid/locales/itIT.ts index c0db9eebacb4..8ed83cfbe834 100644 --- a/packages/grid/_modules_/grid/locales/itIT.ts +++ b/packages/grid/_modules_/grid/locales/itIT.ts @@ -60,6 +60,8 @@ const itITGrid: Partial = { filterOperatorAfter: 'dopo il', filterOperatorOnOrBefore: 'fino al', filterOperatorBefore: 'prima del', + filterOperatorIsEmpty: 'è vuoto', + filterOperatorIsNotEmpty: 'non è vuoto', // Column menu text columnMenuLabel: 'Menu', diff --git a/packages/grid/_modules_/grid/locales/jaJP.ts b/packages/grid/_modules_/grid/locales/jaJP.ts index c85f0b93b51d..880adbe1fbe1 100644 --- a/packages/grid/_modules_/grid/locales/jaJP.ts +++ b/packages/grid/_modules_/grid/locales/jaJP.ts @@ -59,6 +59,8 @@ const jaJPGrid: Partial = { filterOperatorOnOrAfter: '...以降', filterOperatorBefore: '...より前', filterOperatorOnOrBefore: '...以前', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Column menu text columnMenuLabel: 'メニュー', diff --git a/packages/grid/_modules_/grid/locales/nlNL.ts b/packages/grid/_modules_/grid/locales/nlNL.ts index 971f2a2c91bc..ca333ba16dbf 100644 --- a/packages/grid/_modules_/grid/locales/nlNL.ts +++ b/packages/grid/_modules_/grid/locales/nlNL.ts @@ -60,6 +60,8 @@ const nlNLGrid: Partial = { filterOperatorAfter: 'is voor', filterOperatorOnOrBefore: 'is gelijk of er na', filterOperatorBefore: 'is na', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Column menu text columnMenuLabel: 'Menu', diff --git a/packages/grid/_modules_/grid/locales/plPL.ts b/packages/grid/_modules_/grid/locales/plPL.ts index 6013af2530c7..e46042578290 100644 --- a/packages/grid/_modules_/grid/locales/plPL.ts +++ b/packages/grid/_modules_/grid/locales/plPL.ts @@ -59,6 +59,8 @@ export const plPLGrid: Partial = { filterOperatorOnOrAfter: 'większe lub równe', filterOperatorBefore: 'mniejsze niż', filterOperatorOnOrBefore: 'mniejsze lub równe', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Column menu text columnMenuLabel: 'Menu', diff --git a/packages/grid/_modules_/grid/locales/ptBR.ts b/packages/grid/_modules_/grid/locales/ptBR.ts index e0275c381905..bcd52669c8b6 100644 --- a/packages/grid/_modules_/grid/locales/ptBR.ts +++ b/packages/grid/_modules_/grid/locales/ptBR.ts @@ -60,6 +60,8 @@ const ptBRGrid: Partial = { filterOperatorBefore: 'antes de', filterOperatorOnOrBefore: 'em ou antes de', filterOperatorAfter: 'após', + filterOperatorIsEmpty: 'está vazio', + filterOperatorIsNotEmpty: 'não está vazio', // Column menu text columnMenuLabel: 'Menu', diff --git a/packages/grid/_modules_/grid/locales/ruRU.ts b/packages/grid/_modules_/grid/locales/ruRU.ts index 14b43d9785e1..79fbbbcaad05 100644 --- a/packages/grid/_modules_/grid/locales/ruRU.ts +++ b/packages/grid/_modules_/grid/locales/ruRU.ts @@ -69,6 +69,8 @@ export const ruRUGrid: Partial = { filterOperatorOnOrAfter: 'больше или равно', filterOperatorBefore: 'меньше чем', filterOperatorOnOrBefore: 'меньше или равно', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Filter values text filterValueAny: 'любой', diff --git a/packages/grid/_modules_/grid/locales/skSK.ts b/packages/grid/_modules_/grid/locales/skSK.ts index ddcad6b81928..d891da0eeb0a 100644 --- a/packages/grid/_modules_/grid/locales/skSK.ts +++ b/packages/grid/_modules_/grid/locales/skSK.ts @@ -67,6 +67,8 @@ export const skSKGrid: Partial = { filterOperatorOnOrAfter: 'je na alebo po', filterOperatorBefore: 'je pred', filterOperatorOnOrBefore: 'je na alebo skôr', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Filter values text filterValueAny: 'akýkoľvek', diff --git a/packages/grid/_modules_/grid/locales/trTR.ts b/packages/grid/_modules_/grid/locales/trTR.ts index 5c1043787979..5ef107aed1eb 100644 --- a/packages/grid/_modules_/grid/locales/trTR.ts +++ b/packages/grid/_modules_/grid/locales/trTR.ts @@ -59,6 +59,8 @@ const trTRGrid: Partial = { filterOperatorOnOrAfter: 'büyük eşit', filterOperatorBefore: 'küçük', filterOperatorOnOrBefore: 'küçük eşit', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Column menu text columnMenuLabel: 'Menü', diff --git a/packages/grid/_modules_/grid/locales/ukUA.ts b/packages/grid/_modules_/grid/locales/ukUA.ts index a6be374e890f..15573af4de17 100644 --- a/packages/grid/_modules_/grid/locales/ukUA.ts +++ b/packages/grid/_modules_/grid/locales/ukUA.ts @@ -60,6 +60,8 @@ export const ukUAGrid: Partial = { filterOperatorOnOrAfter: 'більше або дорівнює', filterOperatorBefore: 'менше ніж', filterOperatorOnOrBefore: 'менше або дорівнює', + // filterOperatorIsEmpty: 'is empty', + // filterOperatorIsNotEmpty: 'is not empty', // Filter values text filterValueAny: 'будь-який', diff --git a/packages/grid/_modules_/grid/models/colDef/gridBooleanOperators.ts b/packages/grid/_modules_/grid/models/colDef/gridBooleanOperators.ts index b9d354df5959..da3f3fde5296 100644 --- a/packages/grid/_modules_/grid/models/colDef/gridBooleanOperators.ts +++ b/packages/grid/_modules_/grid/models/colDef/gridBooleanOperators.ts @@ -6,7 +6,7 @@ export const getGridBooleanOperators: () => GridFilterOperator[] = () => [ { value: 'is', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } diff --git a/packages/grid/_modules_/grid/models/colDef/gridDateOperators.ts b/packages/grid/_modules_/grid/models/colDef/gridDateOperators.ts index 08c69d923611..3edf497758d5 100644 --- a/packages/grid/_modules_/grid/models/colDef/gridDateOperators.ts +++ b/packages/grid/_modules_/grid/models/colDef/gridDateOperators.ts @@ -11,7 +11,7 @@ function buildApplyFilterFn( showTime?: boolean, keepHours?: boolean, ) { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } diff --git a/packages/grid/_modules_/grid/models/colDef/gridNumericOperators.ts b/packages/grid/_modules_/grid/models/colDef/gridNumericOperators.ts index 57b5fcc9cec5..cb026cdbc4bc 100644 --- a/packages/grid/_modules_/grid/models/colDef/gridNumericOperators.ts +++ b/packages/grid/_modules_/grid/models/colDef/gridNumericOperators.ts @@ -7,7 +7,7 @@ export const getGridNumericColumnOperators: () => GridFilterOperator[] = () => [ label: '=', value: '=', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } @@ -22,7 +22,7 @@ export const getGridNumericColumnOperators: () => GridFilterOperator[] = () => [ label: '!=', value: '!=', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } @@ -37,7 +37,7 @@ export const getGridNumericColumnOperators: () => GridFilterOperator[] = () => [ label: '>', value: '>', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } @@ -52,7 +52,7 @@ export const getGridNumericColumnOperators: () => GridFilterOperator[] = () => [ label: '>=', value: '>=', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } @@ -67,7 +67,7 @@ export const getGridNumericColumnOperators: () => GridFilterOperator[] = () => [ label: '<', value: '<', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } @@ -82,7 +82,7 @@ export const getGridNumericColumnOperators: () => GridFilterOperator[] = () => [ label: '<=', value: '<=', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } diff --git a/packages/grid/_modules_/grid/models/colDef/gridStringOperators.ts b/packages/grid/_modules_/grid/models/colDef/gridStringOperators.ts index f8456caf4d64..b8fe4e3d2a4a 100644 --- a/packages/grid/_modules_/grid/models/colDef/gridStringOperators.ts +++ b/packages/grid/_modules_/grid/models/colDef/gridStringOperators.ts @@ -10,7 +10,7 @@ export const getGridStringOperators: () => GridFilterOperator[] = () => [ { value: 'contains', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } @@ -24,7 +24,7 @@ export const getGridStringOperators: () => GridFilterOperator[] = () => [ { value: 'equals', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } return ({ value }): boolean => { @@ -40,7 +40,7 @@ export const getGridStringOperators: () => GridFilterOperator[] = () => [ { value: 'startsWith', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } @@ -54,7 +54,7 @@ export const getGridStringOperators: () => GridFilterOperator[] = () => [ { value: 'endsWith', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (!filterItem.value) { return null; } @@ -65,4 +65,20 @@ export const getGridStringOperators: () => GridFilterOperator[] = () => [ }, InputComponent: GridFilterInputValue, }, + { + value: 'isEmpty', + getApplyFilterFn: () => { + return ({ value }): boolean => { + return value === '' || value == null; + }; + }, + }, + { + value: 'isNotEmpty', + getApplyFilterFn: () => { + return ({ value }): boolean => { + return value !== '' && value != null; + }; + }, + }, ]; diff --git a/packages/grid/_modules_/grid/models/gridFilterOperator.ts b/packages/grid/_modules_/grid/models/gridFilterOperator.ts index fb7932d06b30..7db6ff425460 100644 --- a/packages/grid/_modules_/grid/models/gridFilterOperator.ts +++ b/packages/grid/_modules_/grid/models/gridFilterOperator.ts @@ -10,6 +10,6 @@ export interface GridFilterOperator { filterItem: GridFilterItem, column: any, ) => null | ((params: GridCellParams) => boolean); - InputComponent: React.JSXElementConstructor; + InputComponent?: React.JSXElementConstructor; InputComponentProps?: Record; } diff --git a/packages/grid/data-grid/src/tests/filtering.DataGrid.test.tsx b/packages/grid/data-grid/src/tests/filtering.DataGrid.test.tsx index 7989a6c3f749..5950b6b8da58 100644 --- a/packages/grid/data-grid/src/tests/filtering.DataGrid.test.tsx +++ b/packages/grid/data-grid/src/tests/filtering.DataGrid.test.tsx @@ -1,5 +1,8 @@ import * as React from 'react'; -import { createClientRenderStrictMode } from 'test/utils'; +import { + createClientRenderStrictMode, // @ts-expect-error need to migrate helpers to TypeScript + screen, +} from 'test/utils'; import { expect } from 'chai'; import { DataGrid, GridToolbar } from '@material-ui/data-grid'; import { getColumnValues } from 'test/utils/helperFn'; @@ -153,6 +156,64 @@ describe(' - Filter', () => { expect(getColumnValues()).to.deep.equal(['Nike']); }); + it('should allow operator isEmpty', () => { + const rows = [ + { + id: 0, + brand: 'Nike', + country: 'United States', + }, + { + id: 1, + brand: 'Adidas', + country: null, + }, + { + id: 2, + brand: 'Puma', + country: '', + }, + ]; + render( + , + ); + expect(getColumnValues()).to.deep.equal(['Adidas', 'Puma']); + }); + + it('should allow operator isNotEmpty', () => { + const rows = [ + { + id: 0, + brand: 'Nike', + country: 'United States', + }, + { + id: 1, + brand: 'Adidas', + country: null, + }, + { + id: 2, + brand: 'Puma', + country: '', + }, + ]; + render( + , + ); + expect(getColumnValues()).to.deep.equal(['Nike']); + }); + [ { operatorValue: 'contains', value: 'a', expected: ['Asics'] }, { operatorValue: 'startsWith', value: 'r', expected: ['RedBull'] }, @@ -636,4 +697,33 @@ describe(' - Filter', () => { ); expect(getColumnValues()).to.deep.equal(['0.5']); }); + + it('should include value-less operators when displaying the number of active filters', () => { + const rows = [ + { + id: 0, + brand: 'Nike', + country: 'United States', + }, + { + id: 1, + brand: 'Adidas', + country: null, + }, + { + id: 2, + brand: 'Puma', + country: '', + }, + ]; + render( + , + ); + expect(screen.queryByTitle('1 active filter')).not.to.equal(null); + }); }); diff --git a/packages/storybook/src/stories/grid-filter.stories.tsx b/packages/storybook/src/stories/grid-filter.stories.tsx index 7f9a07b3f0a4..3de86305ac82 100644 --- a/packages/storybook/src/stories/grid-filter.stories.tsx +++ b/packages/storybook/src/stories/grid-filter.stories.tsx @@ -17,7 +17,7 @@ import { useGridApiRef, XGrid, } from '@material-ui/x-grid'; -import { useDemoData } from '@material-ui/x-grid-data-generator'; +import { useDemoData, randomArrayItem } from '@material-ui/x-grid-data-generator'; import { action } from '@storybook/addon-actions'; import * as React from 'react'; import { randomInt } from '../data/random-generator'; @@ -218,6 +218,38 @@ export function CommodityNoToolbar() { ); } +export function CommodityWithEmptyCells() { + const { data } = useDemoData({ + dataSet: 'Commodity', + rowLength: 100, + maxColumns: 6, + }); + + const rows = React.useMemo( + () => + data.rows.map((row) => { + return Object.entries(row).reduce((acc, [key, value]) => { + acc[key] = key === 'id' ? value : randomArrayItem([value, null, '']); + return acc; + }, {}); + }), + [data.rows], + ); + + return ( +
+ +
+ ); +} + export function ServerFilterViaProps() { const demoServer = useDemoData({ dataSet: 'Commodity', rowLength: 100 }); const [rows, setRows] = React.useState(demoServer.data.rows);