From f1538f0cc386c147a5919e540773b1deecff3d35 Mon Sep 17 00:00:00 2001 From: Matheus Wichman Date: Fri, 23 Jul 2021 16:08:28 -0300 Subject: [PATCH] [DataGrid] Fix support for singleSelect with numeric values (#2112) --- docs/pages/api-docs/data-grid/grid-col-def.md | 2 +- .../cell/GridEditSingleSelectCell.tsx | 10 +- .../filterPanel/GridFilterInputValue.tsx | 22 +- .../grid/models/colDef/gridColDef.ts | 2 +- .../colDef/gridSingleSelectOperators.ts | 19 +- .../src/tests/filtering.DataGrid.test.tsx | 205 ++++++++++++++---- .../x-grid/src/tests/editRows.XGrid.test.tsx | 134 ++++++------ .../src/stories/grid-columns.stories.tsx | 30 ++- 8 files changed, 296 insertions(+), 128 deletions(-) diff --git a/docs/pages/api-docs/data-grid/grid-col-def.md b/docs/pages/api-docs/data-grid/grid-col-def.md index bfbac0249d00..962e8f7ba67d 100644 --- a/docs/pages/api-docs/data-grid/grid-col-def.md +++ b/docs/pages/api-docs/data-grid/grid-col-def.md @@ -40,6 +40,6 @@ import { GridColDef } from '@material-ui/data-grid'; | type? | string | 'string'
| Type allows to merge this object with a default definition [GridColDef](/api/data-grid/grid-col-def/). | | valueFormatter? | (params: GridValueFormatterParams) => GridCellValue | | Function that allows to apply a formatter before rendering its value. | | valueGetter? | (params: GridValueGetterParams) => GridCellValue | | Function that allows to get a specific data instead of field to render in the cell. | -| valueOptions? | (string \| { label: string; value: any })[] | | To be used in combination with `type: 'singleSelect'`. This is an array of the possible cell values and labels. | +| valueOptions? | (string \| number \| { label: string; value: any })[] | | To be used in combination with `type: 'singleSelect'`. This is an array of the possible cell values and labels. | | valueParser? | (value: GridCellValue, params?: GridCellParams) => GridCellValue | | Function that takes the user-entered value and converts it to a value used internally. | | width? | number | 100
| Set the width of the column. | diff --git a/packages/grid/_modules_/grid/components/cell/GridEditSingleSelectCell.tsx b/packages/grid/_modules_/grid/components/cell/GridEditSingleSelectCell.tsx index 5d79e5bae0f4..6891d827b443 100644 --- a/packages/grid/_modules_/grid/components/cell/GridEditSingleSelectCell.tsx +++ b/packages/grid/_modules_/grid/components/cell/GridEditSingleSelectCell.tsx @@ -5,14 +5,14 @@ import { GridCellParams } from '../../models/params/gridCellParams'; import { isEscapeKey } from '../../utils/keyboardUtils'; const renderSingleSelectOptions = (option) => - typeof option === 'string' ? ( - - {option} - - ) : ( + typeof option === 'object' ? ( {option.label} + ) : ( + + {option} + ); export function GridEditSingleSelectCell(props: GridCellParams & SelectProps) { diff --git a/packages/grid/_modules_/grid/components/panel/filterPanel/GridFilterInputValue.tsx b/packages/grid/_modules_/grid/components/panel/filterPanel/GridFilterInputValue.tsx index 879edff0fff6..be4930f42ce3 100644 --- a/packages/grid/_modules_/grid/components/panel/filterPanel/GridFilterInputValue.tsx +++ b/packages/grid/_modules_/grid/components/panel/filterPanel/GridFilterInputValue.tsx @@ -8,14 +8,14 @@ import { GridColDef } from '../../../models/colDef/gridColDef'; const renderSingleSelectOptions = ({ valueOptions }: GridColDef) => ['', ...valueOptions!].map((option) => - typeof option === 'string' ? ( - - ) : ( + typeof option === 'object' ? ( + ) : ( + ), ); @@ -44,8 +44,16 @@ export function GridFilterInputValue(props: GridTypeFilterInputValueProps & Text const onFilterChange = React.useCallback( (event) => { + let value = event.target.value; + // NativeSelect casts the value to a string. + if (type === 'singleSelect') { + const column = apiRef.current.getColumn(item.columnField); + value = column.valueOptions + .map((option) => (typeof option === 'object' ? option.value : option)) + .find((optionValue) => String(optionValue) === value); + } + clearTimeout(filterTimeout.current); - const value = event.target.value; setFilterValueState(value); setIsApplying(true); filterTimeout.current = setTimeout(() => { @@ -53,7 +61,7 @@ export function GridFilterInputValue(props: GridTypeFilterInputValueProps & Text setIsApplying(false); }, SUBMIT_FILTER_STROKE_TIME); }, - [applyValue, item], + [apiRef, applyValue, item, type], ); React.useEffect(() => { diff --git a/packages/grid/_modules_/grid/models/colDef/gridColDef.ts b/packages/grid/_modules_/grid/models/colDef/gridColDef.ts index 31de2676a247..500401a558ea 100644 --- a/packages/grid/_modules_/grid/models/colDef/gridColDef.ts +++ b/packages/grid/_modules_/grid/models/colDef/gridColDef.ts @@ -79,7 +79,7 @@ export interface GridColDef { /** * To be used in combination with `type: 'singleSelect'`. This is an array of the possible cell values and labels. */ - valueOptions?: Array; + valueOptions?: Array; /** * Allows to align the column values in cells. */ diff --git a/packages/grid/_modules_/grid/models/colDef/gridSingleSelectOperators.ts b/packages/grid/_modules_/grid/models/colDef/gridSingleSelectOperators.ts index 8f88ef7cb380..5b05d25eea51 100644 --- a/packages/grid/_modules_/grid/models/colDef/gridSingleSelectOperators.ts +++ b/packages/grid/_modules_/grid/models/colDef/gridSingleSelectOperators.ts @@ -6,13 +6,14 @@ export const getGridSingleSelectOperators: () => GridFilterOperator[] = () => [ { value: 'is', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (filterItem.value == null || filterItem.value === '') { return null; } return ({ value }): boolean => { - return typeof value === 'string' - ? filterItem.value === value - : filterItem.value === (value as { value: any; label: string }).value; + if (typeof value === 'object') { + return filterItem.value === (value as { value: any; label: string }).value; + } + return filterItem.value === value; }; }, InputComponent: GridFilterInputValue, @@ -21,14 +22,14 @@ export const getGridSingleSelectOperators: () => GridFilterOperator[] = () => [ { value: 'not', getApplyFilterFn: (filterItem: GridFilterItem) => { - if (!filterItem.columnField || !filterItem.value || !filterItem.operatorValue) { + if (filterItem.value == null || filterItem.value === '') { return null; } - return ({ value }): boolean => { - return typeof value === 'string' - ? filterItem.value !== value - : filterItem.value !== (value as { value: any; label: string }).value; + if (typeof value === 'object') { + return filterItem.value !== (value as { value: any; label: string }).value; + } + return filterItem.value !== value; }; }, InputComponent: GridFilterInputValue, 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 4cef0308b8c1..ad014ce074d2 100644 --- a/packages/grid/data-grid/src/tests/filtering.DataGrid.test.tsx +++ b/packages/grid/data-grid/src/tests/filtering.DataGrid.test.tsx @@ -2,9 +2,17 @@ import * as React from 'react'; import { createClientRenderStrictMode, // @ts-expect-error need to migrate helpers to TypeScript screen, + // @ts-expect-error need to migrate helpers to TypeScript + fireEvent, } from 'test/utils'; import { expect } from 'chai'; -import { DataGrid, GridToolbar } from '@material-ui/data-grid'; +import { useFakeTimers } from 'sinon'; +import { + DataGrid, + GridToolbar, + GridPreferencePanelsValue, + GridLinkOperator, +} from '@material-ui/data-grid'; import { getColumnValues } from 'test/utils/helperFn'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); @@ -21,18 +29,21 @@ describe(' - Filter', () => { brand: 'Nike', isPublished: false, country: 'United States', + status: 0, }, { id: 1, brand: 'Adidas', isPublished: true, country: 'Germany', + status: 0, }, { id: 2, brand: 'Puma', isPublished: true, country: 'Germany', + status: 2, }, ], columns: [ @@ -43,6 +54,15 @@ describe(' - Filter', () => { type: 'singleSelect', valueOptions: ['United States', 'Germany', 'France'], }, + { + field: 'status', + type: 'singleSelect', + valueOptions: [ + { value: 0, label: 'Payment Pending' }, + { value: 1, label: 'Shipped' }, + { value: 2, label: 'Delivered' }, + ], + }, ], }; @@ -52,8 +72,9 @@ describe(' - Filter', () => { operatorValue?: string; value?: any; field?: string; + state?: any; }) => { - const { operatorValue, value, rows, columns, field = 'brand' } = props; + const { operatorValue, value, rows, columns, field = 'brand', ...other } = props; return (
- Filter', () => { ], }} disableColumnFilter={false} + {...other} />
); @@ -638,49 +660,158 @@ describe(' - Filter', () => { }); }); - describe('select operators', () => { - it('should allow operator is', () => { - const { setProps } = render(); - setProps({ - field: 'country', - operatorValue: 'is', - value: 'United States', + describe('singleSelect operators', () => { + let clock; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('simple options', () => { + it('should allow operator is', () => { + const { setProps } = render(); + setProps({ + field: 'country', + operatorValue: 'is', + value: 'United States', + }); + expect(getColumnValues()).to.deep.equal(['Nike']); + setProps({ + field: 'country', + operatorValue: 'is', + value: 'Germany', + }); + expect(getColumnValues()).to.deep.equal(['Adidas', 'Puma']); + setProps({ + field: 'country', + operatorValue: 'is', + value: '', + }); + expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); }); - expect(getColumnValues()).to.deep.equal(['Nike']); - setProps({ - field: 'country', - operatorValue: 'is', - value: 'Germany', + + it('should allow operator not', () => { + const { setProps } = render(); + setProps({ + field: 'country', + operatorValue: 'not', + value: 'United States', + }); + expect(getColumnValues()).to.deep.equal(['Adidas', 'Puma']); + setProps({ + field: 'country', + operatorValue: 'not', + value: 'Germany', + }); + expect(getColumnValues()).to.deep.equal(['Nike']); + setProps({ + field: 'country', + operatorValue: 'not', + value: '', + }); + expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); }); - expect(getColumnValues()).to.deep.equal(['Adidas', 'Puma']); - setProps({ - field: 'country', - operatorValue: 'is', - value: '', + + it('should work with numeric values', () => { + render( + , + ); + expect(getColumnValues()).to.deep.equal(['Hair Dryer', 'Dishwasher', 'Microwave']); + fireEvent.change(screen.getByLabelText('Value'), { target: { value: '220' } }); + clock.tick(600); // Wait for the debounce + expect(getColumnValues()).to.deep.equal(['Hair Dryer', 'Microwave']); }); - expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); }); - it('should allow operator not', () => { - const { setProps } = render(); - setProps({ - field: 'country', - operatorValue: 'not', - value: 'United States', + describe('complex options', () => { + it('should allow operator is', () => { + const { setProps } = render(); + setProps({ + field: 'status', + operatorValue: 'is', + value: 0, + }); + expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas']); + setProps({ + field: 'status', + operatorValue: 'is', + value: 2, + }); + expect(getColumnValues()).to.deep.equal(['Puma']); + setProps({ + field: 'status', + operatorValue: 'is', + value: '', + }); + expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); }); - expect(getColumnValues()).to.deep.equal(['Adidas', 'Puma']); - setProps({ - field: 'country', - operatorValue: 'not', - value: 'Germany', + + it('should allow operator not', () => { + const { setProps } = render(); + setProps({ + field: 'status', + operatorValue: 'not', + value: 0, + }); + expect(getColumnValues()).to.deep.equal(['Puma']); + setProps({ + field: 'status', + operatorValue: 'not', + value: 2, + }); + expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas']); + setProps({ + field: 'status', + operatorValue: 'not', + value: '', + }); + expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); }); - expect(getColumnValues()).to.deep.equal(['Nike']); - setProps({ - field: 'country', - operatorValue: 'not', - value: '', + + it('should work with numeric values', () => { + render( + , + ); + expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); + fireEvent.change(screen.getByLabelText('Value'), { target: { value: '2' } }); + clock.tick(600); + expect(getColumnValues()).to.deep.equal(['Puma']); }); - expect(getColumnValues()).to.deep.equal(['Nike', 'Adidas', 'Puma']); }); }); diff --git a/packages/grid/x-grid/src/tests/editRows.XGrid.test.tsx b/packages/grid/x-grid/src/tests/editRows.XGrid.test.tsx index 2b441ff3ac0e..a6deb86040f3 100644 --- a/packages/grid/x-grid/src/tests/editRows.XGrid.test.tsx +++ b/packages/grid/x-grid/src/tests/editRows.XGrid.test.tsx @@ -475,79 +475,81 @@ describe(' - Edit Rows', () => { }); }); - it('should change cell value correctly when column type is singleSelect and valueOptions is array of strings', () => { - render( -
- -
, - ); + describe('column type: singleSelect', () => { + it('should change cell value correctly when the valueOptions is array of strings', () => { + render( +
+ +
, + ); - const cell = getCell(0, 0); - fireEvent.doubleClick(cell); - fireEvent.click(screen.queryAllByRole('option')[1]); + const cell = getCell(0, 0); + fireEvent.doubleClick(cell); + fireEvent.click(screen.queryAllByRole('option')[1]); - expect(cell).not.to.have.class('MuiDataGrid-cell--editing'); - expect(cell).to.have.text('Adidas'); - }); + expect(cell).not.to.have.class('MuiDataGrid-cell--editing'); + expect(cell).to.have.text('Adidas'); + }); - it('should change cell value correctly when column type is singleSelect and valueOptions is array of objects', () => { - const countries = [ - { - value: 'fr', - label: 'France', - }, - { - value: 'it', - label: 'Italy', - }, - ]; + it('should change cell value correctly when the valueOptions is array of objects', () => { + const countries = [ + { + value: 'fr', + label: 'France', + }, + { + value: 'it', + label: 'Italy', + }, + ]; - render( -
- { - const result = countries.find((country) => country.value === params.value); - return result!.label; + render( +
+ { + const result = countries.find((country) => country.value === params.value); + return result!.label; + }, + editable: true, }, - editable: true, - }, - ]} - rows={[ - { - id: 0, - country: 'fr', - }, - ]} - /> -
, - ); + ]} + rows={[ + { + id: 0, + country: 'fr', + }, + ]} + /> +
, + ); - const cell = getCell(0, 0); - fireEvent.doubleClick(cell); - fireEvent.click(screen.queryAllByRole('option')[1]); + const cell = getCell(0, 0); + fireEvent.doubleClick(cell); + fireEvent.click(screen.queryAllByRole('option')[1]); - expect(cell).not.to.have.class('MuiDataGrid-cell--editing'); - expect(cell).to.have.text('Italy'); + expect(cell).not.to.have.class('MuiDataGrid-cell--editing'); + expect(cell).to.have.text('Italy'); + }); }); it('should keep the right type', () => { diff --git a/packages/storybook/src/stories/grid-columns.stories.tsx b/packages/storybook/src/stories/grid-columns.stories.tsx index 7b88f6ee9d4b..de95747ad1e6 100644 --- a/packages/storybook/src/stories/grid-columns.stories.tsx +++ b/packages/storybook/src/stories/grid-columns.stories.tsx @@ -337,10 +337,18 @@ export const SingleSelectColumnType = () => { }, ]; + const fruits = [ + { value: 1, label: 'Apple' }, + { value: 2, label: 'Orange' }, + { value: 3, label: 'Banana' }, + ]; + + const ratings = [1, 2, 3, 4, 5]; + const data = { rows: [ - { id: 0, country: 'bg' }, - { id: 1, country: 'nl' }, + { id: 0, country: 'bg', fruit: 1, rating: 5 }, + { id: 1, country: 'nl', fruit: 2, rating: 4 }, ], columns: [ { @@ -354,6 +362,24 @@ export const SingleSelectColumnType = () => { editable: true, width: 200, }, + { + field: 'fruit', + type: 'singleSelect', + valueOptions: fruits, + valueFormatter: (params) => { + const result = fruits.find((fruit) => fruit.value === params.value); + return result!.label; + }, + editable: true, + width: 200, + }, + { + field: 'rating', + type: 'singleSelect', + valueOptions: ratings, + editable: true, + width: 200, + }, ], };