From 4f9f2a406c015a851ca630cc5226b1bf70cbbc6f Mon Sep 17 00:00:00 2001 From: Gabriel Liwerant Date: Thu, 21 Nov 2019 18:33:09 -0500 Subject: [PATCH] Feature/issue 937 array and custom update filter support (#1067) * Add ability for custom filter list render to use arrays * Update example to demonstrate new feature * Rename arguments for clarity * Add prop type check and deprecation notice * Allow default filter update when not specified in options * Fix deprecation warnings in examples * Improve deprecation verbage for responsive option * Prettify files * Update serverside filter list render to include new feature * Update documentation * Update tests with deprecation notices and future API * Add test for custom filter update function call * Pretiffy test file --- README.md | 3 +- examples/column-filters/index.js | 10 +-- examples/column-options-update/index.js | 8 +-- examples/customize-filter/index.js | 53 +++++++++++--- src/MUIDataTable.js | 34 +++++++-- src/components/TableFilterList.js | 95 +++++++++++++++++++++---- test/MUIDataTable.test.js | 67 +++++++++++++++-- 7 files changed, 224 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index d3d7cbf90..cd54678c6 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,8 @@ const columns = [ |**`viewColumns`**|boolean|true|Allow user to toggle column visibility through 'View Column' list |**`filterList`**|array||Filter value list [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/column-filters/index.js) |**`filterOptions`**|{names, logic, display}||With filter options, it's possible to use custom names for the filter fields [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/column-filters/index.js), custom filter logic [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-filter/index.js), and custom rendering [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-filter/index.js) -|**`customFilterListRender`**|function||Function that returns a string used as the chip label. `function(value) => string` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/column-filters/index.js) +|**`customFilterListRender` DEPRECATED **|function||Function that returns a string or array of strings used as the chip label(s). `function(value) => string | arrayOfStrings` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/column-filters/index.js) +|**`customFilterListOptions` **|{render: function, update: function}|| `render` returns a string or array of strings used as the chip label(s). `function(value) => string | arrayOfStrings`, `update` returns a `filterList (see above)` allowing for custom filter updates when removing the filter chip [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/column-filters/index.js) |**`filter`**|boolean|true|Display column in filter list |**`filterType `**|string|'dropdown'|Choice of filtering view. Takes priority over global filterType option.`enum('checkbox', 'dropdown', 'multiselect', 'textField', 'custom')` Use 'custom' if you are supplying your own rendering via `filterOptions`. |**`sort`**|boolean|true|Enable/disable sorting on column diff --git a/examples/column-filters/index.js b/examples/column-filters/index.js index c1fd4f9f7..2a871525a 100644 --- a/examples/column-filters/index.js +++ b/examples/column-filters/index.js @@ -12,7 +12,7 @@ class Example extends React.Component { options: { filter: true, filterList: ['Franky Miles'], - customFilterListRender: v => `Name: ${v}`, + customFilterListOptions: { render: v => `Name: ${v}` }, filterOptions: { names: ['a', 'b', 'c', 'Business Analyst'] }, @@ -23,7 +23,7 @@ class Example extends React.Component { options: { filter: true, filterList: ['Business Analyst'], - customFilterListRender: v => `Title: ${v}`, + customFilterListOptions: { render: v => `Title: ${v}` }, filterType: 'textField' // set filterType's at the column level } }, @@ -37,14 +37,14 @@ class Example extends React.Component { name: "Age", options: { filter: true, - customFilterListRender: v => `Age: ${v}`, + customFilterListOptions: { render: v => `Age: ${v}` }, } }, { name: "Salary", options: { filter: true, - customFilterListRender: v => `Salary: ${v}`, + customFilterListOptions: { render: v => `Salary: ${v}` }, sort: false } } @@ -87,7 +87,7 @@ class Example extends React.Component { onFilterChange: (changedColumn, filterList) => { console.log(changedColumn, filterList); }, - selectableRows: true, + selectableRows: 'multiple', filterType: 'dropdown', responsive: 'stacked', rowsPerPage: 10, diff --git a/examples/column-options-update/index.js b/examples/column-options-update/index.js index 9930923b3..f71e58cde 100644 --- a/examples/column-options-update/index.js +++ b/examples/column-options-update/index.js @@ -73,7 +73,7 @@ class Example extends React.Component { filter: true, display: this.state.display[0], filterList: filterList[0].length ? filterList[0] : null, - customFilterListRender: v => `Name: ${v}`, + customFilterListOptions: { render: v => `Name: ${v}` }, filterOptions: { names: filterOptions }, @@ -85,7 +85,7 @@ class Example extends React.Component { display: this.state.display[1], filter: true, filterList: filterList[1].length ? filterList[1] : null, - customFilterListRender: v => `Title: ${v}`, + customFilterListOptions: { render: v => `Title: ${v}` }, filterType: 'textField' // set filterType's at the column level } }, @@ -103,7 +103,7 @@ class Example extends React.Component { display: this.state.display[3], filter: true, filterList: filterList[3].length ? filterList[3] : null, - customFilterListRender: v => `Age: ${v}`, + customFilterListOptions: { render: v => `Age: ${v}` }, } }, { @@ -112,7 +112,7 @@ class Example extends React.Component { display: this.state.display[4], filter: true, filterList: filterList[4].length ? filterList[4] : null, - customFilterListRender: v => `Salary: ${v}`, + customFilterListOptions: { render: v => `Salary: ${v}` }, sort: false } } diff --git a/examples/customize-filter/index.js b/examples/customize-filter/index.js index a9ecf30ae..575155a6d 100644 --- a/examples/customize-filter/index.js +++ b/examples/customize-filter/index.js @@ -1,8 +1,12 @@ -import { FormGroup, FormLabel, TextField } from '@material-ui/core'; +import { FormGroup, FormLabel, TextField, Checkbox, FormControlLabel, Grid } from '@material-ui/core'; import React from 'react'; import MUIDataTable from '../../src'; class Example extends React.Component { + state = { + ageFilterChecked: false + }; + render() { const columns = [ { @@ -32,15 +36,32 @@ class Example extends React.Component { filter: true, filterType: 'custom', filterList: [25, 50], - customFilterListRender: v => { - if (v[0] && v[1]) { - return `Min Age: ${v[0]}, Max Age: ${v[1]}`; - } else if (v[0]) { - return `Min Age: ${v[0]}`; - } else if (v[1]) { - return `Max Age: ${v[1]}`; - } - return false; + customFilterListOptions: { + render: v => { + if (v[0] && v[1] && this.state.ageFilterChecked) { + return [`Min Age: ${v[0]}`, `Max Age: ${v[1]}`]; + } else if (v[0] && v[1] && !this.state.ageFilterChecked) { + return `Min Age: ${v[0]}, Max Age: ${v[1]}`; + } else if (v[0]) { + return `Min Age: ${v[0]}`; + } else if (v[1]) { + return `Max Age: ${v[1]}`; + } + return false; + }, + update: (filterList, filterPos, index) => { + console.log('customFilterListOnDelete: ', filterList, filterPos, index); + + if (filterPos === 0) { + filterList[index].splice(filterPos, 1, ''); + } else if (filterPos === 1) { + filterList[index].splice(filterPos, 1); + } else if (filterPos === -1) { + filterList[index] = []; + } + + return filterList; + }, }, filterOptions: { names: [], @@ -76,6 +97,16 @@ class Example extends React.Component { }} style={{ width: '45%' }} /> + this.setState({ ageFilterChecked: event.target.checked })} + /> + } + label="Separate Values" + style={{ marginLeft: '0px' }} + /> ), @@ -139,7 +170,7 @@ class Example extends React.Component { const options = { filter: true, - filterType: 'dropdown', + filterType: 'multiselect', responsive: 'scrollMaxHeight', }; diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js index bcd7c2986..5451042f4 100644 --- a/src/MUIDataTable.js +++ b/src/MUIDataTable.js @@ -121,6 +121,12 @@ class MUIDataTable extends React.Component { filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField', 'custom']), customHeadRender: PropTypes.func, customBodyRender: PropTypes.func, + customFilterListOptions: PropTypes.oneOfType([ + PropTypes.shape({ + render: PropTypes.func, + update: PropTypes.func, + }), + ]), customFilterListRender: PropTypes.func, }), }), @@ -327,8 +333,16 @@ class MUIDataTable extends React.Component { ); } if (this.options.responsive === 'scroll') { - console.error('This option has been deprecated. It is being replaced by scrollMaxHeight'); + console.error('The "scroll" responsive option has been deprecated. It is being replaced by "scrollMaxHeight"'); } + + this.props.columns.map(c => { + if (c.options && c.options.customFilterListRender) { + console.error( + 'The customFilterListRender option has been deprecated. It is being replaced by customFilterListOptions.render (Specify customFilterListOptions: { render: Function } in column options.)', + ); + } + }); }; /* @@ -997,10 +1011,10 @@ class MUIDataTable extends React.Component { ); }; - filterUpdate = (index, value, column, type) => { + filterUpdate = (index, value, column, type, customUpdate) => { this.setState( prevState => { - const filterList = prevState.filterList.slice(0); + let filterList = prevState.filterList.slice(0); const filterPos = filterList[index].indexOf(value); switch (type) { @@ -1017,7 +1031,8 @@ class MUIDataTable extends React.Component { filterList[index] = value; break; case 'custom': - filterList[index] = value; + if (customUpdate) filterList = customUpdate(filterList, filterPos, index); + else filterList[index] = value; break; default: filterList[index] = filterPos >= 0 || value === '' ? [] : [value]; @@ -1359,7 +1374,16 @@ class MUIDataTable extends React.Component { options={this.options} serverSideFilterList={this.props.options.serverSideFilterList || []} filterListRenderers={columns.map(c => { - return c.customFilterListRender ? c.customFilterListRender : f => f; + if (c.customFilterListOptions && c.customFilterListOptions.render) return c.customFilterListOptions.render; + // DEPRECATED: This option is being replaced with customFilterListOptions.render + if (c.customFilterListRender) return c.customFilterListRender; + + return f => f; + })} + customFilterListUpdate={columns.map(c => { + return c.customFilterListOptions && c.customFilterListOptions.update + ? c.customFilterListOptions.update + : null; })} filterList={filterList} filterUpdate={this.filterUpdate} diff --git a/src/components/TableFilterList.js b/src/components/TableFilterList.js index f55f5aeec..bafe8a5ab 100644 --- a/src/components/TableFilterList.js +++ b/src/components/TableFilterList.js @@ -35,17 +35,58 @@ class TableFilterList extends React.Component { }; render() { - const { classes, filterList, filterUpdate, filterListRenderers, columnNames, serverSideFilterList } = this.props; + const { + classes, + filterList, + filterUpdate, + filterListRenderers, + columnNames, + serverSideFilterList, + customFilterListUpdate, + } = this.props; const { serverSide } = this.props.options; - const customFilterChip = (item, index) => ( - - ); + const customFilterChipMultiValue = (customFilterItem, index, customFilterItemIndex, item, orig) => { + let label = ''; + const type = customFilterListUpdate[index] ? 'custom' : 'chip'; + + if (Array.isArray(orig)) label = filterListRenderers[customFilterItemIndex](customFilterItem); + else label = filterListRenderers[index](item); + + return ( + + ); + }; + + const customFilterChipSingleValue = (index, item) => { + return ( + + ); + }; const filterChip = (index, data, colIndex) => ( {serverSide ? serverSideFilterList.map((item, index) => { - if (columnNames[index].filterType === 'custom' && filterListRenderers[index](item)) { - return customFilterChip(item, index); + const filterListRenderersValue = filterListRenderers[index](item); + + if (columnNames[index].filterType === 'custom' && filterListRenderersValue) { + if (Array.isArray(filterListRenderersValue)) { + return filterListRenderersValue.map((customFilterItem, customFilterItemIndex) => + customFilterChipMultiValue( + customFilterItem, + index, + customFilterItemIndex, + item, + filterListRenderersValue, + ), + ); + } else { + return customFilterChipSingleValue(index, item); + } } return item.map((data, colIndex) => filterChip(index, data, colIndex)); }) : filterList.map((item, index) => { - if (columnNames[index].filterType === 'custom' && filterListRenderers[index](item)) { - return customFilterChip(item, index); + const customFilterListRenderersValue = filterListRenderers[index](item); + + if (columnNames[index].filterType === 'custom' && customFilterListRenderersValue) { + if (Array.isArray(customFilterListRenderersValue)) { + return customFilterListRenderersValue.map((customFilterItem, customFilterItemIndex) => + customFilterChipMultiValue( + customFilterItem, + index, + customFilterItemIndex, + item, + customFilterListRenderersValue, + ), + ); + } else { + return customFilterChipSingleValue(index, item); + } } return item.map((data, colIndex) => filterChip(index, data, colIndex)); diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js index bfee2f9ff..1d225169a 100644 --- a/test/MUIDataTable.test.js +++ b/test/MUIDataTable.test.js @@ -29,7 +29,14 @@ describe('', function() { before(() => { columns = [ - { name: 'Name', options: { customBodyRender: renderName, customFilterListRender: renderCustomFilterList } }, + { + name: 'Name', + options: { + customBodyRender: renderName, + customFilterListRender: renderCustomFilterList, // DEPRECATED + customFilterListOptions: { render: renderCustomFilterList }, + }, + }, 'Company', { name: 'City', label: 'City Label', options: { customBodyRender: renderCities, filterType: 'textField' } }, { @@ -102,7 +109,8 @@ describe('', function() { searchable: true, sortDirection: 'none', viewColumns: true, - customFilterListRender: renderCustomFilterList, + customFilterListRender: renderCustomFilterList, // DEPRECATED + customFilterListOptions: { render: renderCustomFilterList }, customBodyRender: renderName, }, { @@ -179,7 +187,8 @@ describe('', function() { filter: false, display: 'excluded', customBodyRender: renderName, - customFilterListRender: renderCustomFilterList, + customFilterListRender: renderCustomFilterList, // DEPRECATED + customFilterListOptions: { render: renderCustomFilterList }, }, }, 'Company', @@ -207,7 +216,8 @@ describe('', function() { searchable: true, sortDirection: 'none', viewColumns: true, - customFilterListRender: renderCustomFilterList, + customFilterListRender: renderCustomFilterList, // DEPRECATED + customFilterListOptions: { render: renderCustomFilterList }, customBodyRender: renderName, }, { @@ -282,7 +292,14 @@ describe('', function() { it('should correctly build internal table data and displayData structure when using nested data', () => { const columns = [ - { name: 'Name', options: { customBodyRender: renderName, customFilterListRender: renderCustomFilterList } }, + { + name: 'Name', + options: { + customBodyRender: renderName, + customFilterListRender: renderCustomFilterList, // DEPRECATED + customFilterListOptions: { render: renderCustomFilterList }, + }, + }, 'Company', { name: 'Location.City', label: 'City Label' }, { name: 'Location.State' }, @@ -630,7 +647,7 @@ describe('', function() { assert.strictEqual(actualResult.length, 1); }); - it('should create Chip with custom label when filterList and customFilterListRender are populated', () => { + it('DEPRECATED: should create Chip with custom label when filterList and customFilterListRender are populated', () => { const filterList = [['Joe James'], [], [], [], []]; const filterListRenderers = columns.map(c => { return c.options && c.options.customFilterListRender @@ -653,6 +670,41 @@ describe('', function() { assert.strictEqual(actualResult.prop('label'), 'Name: Joe James'); }); + it('should create Chip with custom label when filterList and customFilterListOptions are populated', () => { + const filterList = [['Joe James'], [], [], [], []]; + const filterListRenderers = columns.map(c => { + return c.options && c.options.customFilterListOptions && c.options.customFilterListOptions.render + ? c.options.customFilterListOptions.render + : defaultRenderCustomFilterList; + }); + const columnNames = columns.map(column => ({ name: column.name })); + + const mountWrapper = mount( + true} + columnNames={columnNames} + />, + ); + const actualResult = mountWrapper.find(Chip); + assert.strictEqual(actualResult.length, 1); + assert.strictEqual(actualResult.prop('label'), 'Name: Joe James'); + }); + + it('should call custom filter update function when it is passed into custom filter update', () => { + const customFilterListUpdate = spy(() => [[], [], [], [], []]); + const shallowWrapper = shallow(); + const table = shallowWrapper.dive(); + const instance = table.instance(); + + instance.filterUpdate(0, ['Joe James'], 'Name', 'custom', customFilterListUpdate); + table.update(); + + assert.deepEqual(customFilterListUpdate.callCount, 1); + }); + it('should render filter Chip(s) when options.serverSide = true and serverSideFilterList is populated', () => { const serverSideFilterList = [['Joe James'], [], [], [], []]; const filterListRenderers = [ @@ -960,7 +1012,8 @@ describe('', function() { sortDirection: 'none', customBodyRender: renderName, viewColumns: true, - customFilterListRender: renderCustomFilterList, + customFilterListRender: renderCustomFilterList, // DEPRECATED + customFilterListOptions: { render: renderCustomFilterList }, }, { name: 'Company',