Repository: gregnb/mui-datatables
Pull request #814 (2019-08-05 21:26:07)

Enhancement/pr 659 sort multiple
Continuation of https://github.com/gregnb/mui-datatables/pull/659.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/24997668/badge)](https://coveralls.io/builds/24997668)

Coverage decreased (-0.3%) to 74.651% when pulling **ca2ca0c927038d151670f902b3e1e9d161556bf9 on enhancement/pr-659-sort-multiple** into **6740f71c14fcbda2e20ef0a358aca3e81d19e1b3 on master**.


Patch:
diff --git a/README.md b/README.md
index 4bc6098..fc29dce 100644
--- a/README.md
+++ b/README.md
@@ -153,6 +153,7 @@ The component accepts the following props:
 |**`customRowRender `**|function||Override default row rendering with custom function. `customRowRender(data, dataIndex, rowIndex) => React Component`
 |**`customSort`**|function||Override default sorting with custom function. `function(data: array, colIndex: number, order: string) => array`
 |**`customSearch `**|function||Override default search with custom function. `customSearch(searchQuery: string, currentRow: array, columns: array) => boolean`
+|**`customSearchRender `**|function||Render a custom table search. `customSearchRender(searchText: string, handleSearch, hideSearch, options) => React Component`
 |**`elevation`**|number|4|Shadow depth applied to Paper component
 |**`caseSensitive `**|boolean|false|Enable/disable case sensitivity for search
 |**`responsive`**|string|'stacked'|Enable/disable responsive table views. Options: 'stacked', 'scroll'
diff --git a/examples/customize-search-render/CustomSearchRender.js b/examples/customize-search-render/CustomSearchRender.js
new file mode 100644
index 0000000..0fa6589
--- /dev/null
+++ b/examples/customize-search-render/CustomSearchRender.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import Grow from '@material-ui/core/Grow';
+import TextField from '@material-ui/core/TextField';
+import IconButton from '@material-ui/core/IconButton';
+import ClearIcon from '@material-ui/icons/Clear';
+import { withStyles } from '@material-ui/core/styles';
+
+const defaultSearchStyles = theme => ({
+  main: {
+    display: 'flex',
+    flex: '1 0 auto',
+  },
+  searchText: {
+    flex: '0.8 0',
+  },
+  clearIcon: {
+    '&:hover': {
+      color: theme.palette.error.main,
+    },
+  },
+});
+
+class CustomSearchRender extends React.Component {
+  handleTextChange = event => {
+    const { onSearchChange } = this.props.options;
+
+    if (onSearchChange) {
+      onSearchChange(event.target.value);
+    }
+
+    this.props.onSearch(event.target.value);
+  };
+
+  componentDidMount() {
+    document.addEventListener('keydown', this.onKeyDown, false);
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener('keydown', this.onKeyDown, false);
+  }
+
+  onKeyDown = event => {
+    if (event.keyCode === 27) {
+      this.props.onHide();
+    }
+  };
+
+  render() {
+    const { classes, options, onHide, searchText } = this.props;
+
+    return (
+      <Grow appear in={true} timeout={300}>
+        <div className={classes.main} ref={el => (this.rootRef = el)}>
+          <TextField
+            placeholder={'Custom TableSearch without search icon'}
+            className={classes.searchText}
+            InputProps={{
+              'aria-label': options.textLabels.toolbar.search,
+            }}
+            value={searchText || ''}
+            onChange={this.handleTextChange}
+            fullWidth={true}
+            inputRef={el => (this.searchField = el)}
+          />
+          <IconButton className={classes.clearIcon} onClick={onHide}>
+            <ClearIcon />
+          </IconButton>
+        </div>
+      </Grow>
+    );
+  }
+}
+
+export default withStyles(defaultSearchStyles, { name: 'CustomSearchRender' })(CustomSearchRender);
diff --git a/examples/customize-search-render/index.js b/examples/customize-search-render/index.js
new file mode 100644
index 0000000..db816c1
--- /dev/null
+++ b/examples/customize-search-render/index.js
@@ -0,0 +1,70 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import MUIDataTable from '../../src/';
+import CustomSearchRender from './CustomSearchRender';
+
+class Example extends React.Component {
+
+  render () {
+
+    const columns = ['Name', 'Title', 'Location', 'Age', 'Salary'];
+
+    let data = [
+      ['Gabby George', 'Business Analyst', 'Minneapolis', 30, 100000],
+      ['Aiden Lloyd', 'Business Consultant', 'Dallas', 55, 200000],
+      ['Jaden Collins', 'Attorney', 'Santa Ana', 27, 500000],
+      ['Franky Rees', 'Business Analyst', 'St. Petersburg', 22, 50000],
+      ['Aaren Rose', 'Business Consultant', 'Toledo', 28, 75000],
+      ['Blake Duncan', 'Business Management Analyst', 'San Diego', 65, 94000],
+      ['Frankie Parry', 'Agency Legal Counsel', 'Jacksonville', 71, 210000],
+      ['Lane Wilson', 'Commercial Specialist', 'Omaha', 19, 65000],
+      ['Robin Duncan', 'Business Analyst', 'Los Angeles', 20, 77000],
+      ['Mel Brooks', 'Business Consultant', 'Oklahoma City', 37, 135000],
+      ['Harper White', 'Attorney', 'Pittsburgh', 52, 420000],
+      ['Kris Humphrey', 'Agency Legal Counsel', 'Laredo', 30, 150000],
+      ['Frankie Long', 'Industrial Analyst', 'Austin', 31, 170000],
+      ['Brynn Robbins', 'Business Analyst', 'Norfolk', 22, 90000],
+      ['Justice Mann', 'Business Consultant', 'Chicago', 24, 133000],
+      ['Addison Navarro', 'Business Management Analyst', 'New York', 50, 295000],
+      ['Jesse Welch', 'Agency Legal Counsel', 'Seattle', 28, 200000],
+      ['Eli Mejia', 'Commercial Specialist', 'Long Beach', 65, 400000],
+      ['Gene Leblanc', 'Industrial Analyst', 'Hartford', 34, 110000],
+      ['Danny Leon', 'Computer Scientist', 'Newark', 60, 220000],
+      ['Lane Lee', 'Corporate Counselor', 'Cincinnati', 52, 180000],
+      ['Jesse Hall', 'Business Analyst', 'Baltimore', 44, 99000],
+      ['Danni Hudson', 'Agency Legal Counsel', 'Tampa', 37, 90000],
+      ['Terry Macdonald', 'Commercial Specialist', 'Miami', 39, 140000],
+      ['Justice Mccarthy', 'Attorney', 'Tucson', 26, 330000],
+      ['Silver Carey', 'Computer Scientist', 'Memphis', 47, 250000],
+      ['Franky Miles', 'Industrial Analyst', 'Buffalo', 49, 190000],
+      ['Glen Nixon', 'Corporate Counselor', 'Arlington', 44, 80000],
+      ['Gabby Strickland', 'Business Process Consultant', 'Scottsdale', 26, 45000],
+      ['Mason Ray', 'Computer Scientist', 'San Francisco', 39, 142000]
+    ];
+
+    const options = {
+      filter: true,
+      selectableRows: true,
+      filterType: 'dropdown',
+      responsive: 'stacked',
+      rowsPerPage: 10,
+      customSearchRender: (searchText, handleSearch, hideSearch, options) => {
+        return (
+          <CustomSearchRender
+            searchText={searchText}
+            onSearch={handleSearch}
+            onHide={hideSearch}
+            options={options}
+          />
+        );
+      }
+    };
+
+    return (
+      <MUIDataTable title={'ACME Employee list'} data={data} columns={columns} options={options} />
+    );
+
+  }
+}
+
+ReactDOM.render(<Example />, document.getElementById('app-root'));
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index 1a6b4f7..f20d031 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -126,6 +126,7 @@ class MUIDataTable extends React.Component {
       customToolbar: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customToolbarSelect: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
+      customSearchRender: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customRowRender: PropTypes.func,
       onRowClick: PropTypes.func,
       resizableColumns: PropTypes.bool,
@@ -213,8 +214,8 @@ class MUIDataTable extends React.Component {
   componentDidMount() {
     this.setHeadResizeable(this.headCellRefs, this.tableRef);
 
-    // When we have a search, we must reset page to view it
-    if (this.props.options.searchText) this.setState({ page: 0 });
+    // When we have a search, we must reset page to view it unless on serverSide since paging is handled by the user.
+    if (this.props.options.searchText && !this.props.options.serverSide) this.setState({ page: 0 });
   }
 
   componentDidUpdate(prevProps) {
@@ -225,8 +226,8 @@ class MUIDataTable extends React.Component {
       this.updateOptions(this.props);
     }
 
-    if (this.props.options.searchText !== prevProps.options.searchText) {
-      // When we have a search, we must reset page to view it
+    if (this.props.options.searchText !== prevProps.options.searchText && !this.props.options.serverSide) {
+      // When we have a search, we must reset page to view it unless on serverSide since paging is handled by the user.
       this.setState({ page: 0 });
     }
 
@@ -363,6 +364,7 @@ class MUIDataTable extends React.Component {
     let columnData = [];
     let filterData = [];
     let filterList = [];
+    let sortDirectionSet = false;
 
     newColumns.forEach((column, colIndex) => {
       let columnOptions = {
@@ -378,8 +380,19 @@ class MUIDataTable extends React.Component {
       };
 
       if (typeof column === 'object') {
-        if (column.options && column.options.display !== undefined) {
-          column.options.display = column.options.display.toString();
+        if (column.options) {
+          if (column.options.display !== undefined) {
+            column.options.display = column.options.display.toString();
+          }
+
+          if (column.options.sortDirection !== undefined) {
+            if (sortDirectionSet) {
+              console.error('sortDirection is set for more than one column. Only the first column will be considered.');
+              column.options.sortDirection = null;
+            } else {
+              sortDirectionSet = true;
+            }
+          }
         }
 
         columnOptions = {
diff --git a/src/components/TableFilter.js b/src/components/TableFilter.js
index 0ea4c2f..99e3697 100644
--- a/src/components/TableFilter.js
+++ b/src/components/TableFilter.js
@@ -1,4 +1,5 @@
 import { Grid, GridList, GridListTile, TextField } from '@material-ui/core';
+
 import Button from '@material-ui/core/Button';
 import Checkbox from '@material-ui/core/Checkbox';
 import FormControl from '@material-ui/core/FormControl';
@@ -8,12 +9,12 @@ import Input from '@material-ui/core/Input';
 import InputLabel from '@material-ui/core/InputLabel';
 import ListItemText from '@material-ui/core/ListItemText';
 import MenuItem from '@material-ui/core/MenuItem';
+import PropTypes from 'prop-types';
+import React from 'react';
 import Select from '@material-ui/core/Select';
-import { withStyles } from '@material-ui/core/styles';
 import Typography from '@material-ui/core/Typography';
 import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React from 'react';
+import { withStyles } from '@material-ui/core/styles';
 
 export const defaultFilterStyles = theme => ({
   root: {
@@ -79,29 +80,8 @@ export const defaultFilterStyles = theme => ({
     },
   },
   checked: {},
-  /* selects */
-  selectRoot: {
-    display: 'flex',
-    marginTop: '16px',
-    flexDirection: 'row',
-    flexWrap: 'wrap',
-    width: '100%',
-    height: '80%',
-    justifyContent: 'space-between',
-  },
-  selectFormControl: {
-    flex: '1 1 calc(50% - 24px)',
-  },
-  /* textField */
-  textFieldRoot: {
-    display: 'flex',
+  gridListTile: {
     marginTop: '16px',
-    flexDirection: 'row',
-    flexWrap: 'wrap',
-    width: '100%',
-  },
-  textFieldFormControl: {
-    flex: '1 1 calc(50% - 24px)',
   },
 });
 
@@ -190,26 +170,25 @@ class TableFilter extends React.Component {
     const textLabels = options.textLabels.filter;
 
     return (
-      <GridListTile key={index} cols={1}>
-        <div className={classes.selectRoot}>
-          <FormControl className={classes.selectFormControl} key={index}>
-            <InputLabel htmlFor={column.name}>{column.label}</InputLabel>
-            <Select
-              value={filterList[index].toString() || textLabels.all}
-              name={column.name}
-              onChange={event => this.handleDropdownChange(event, index, column.name)}
-              input={<Input name={column.name} id={column.name} />}>
-              <MenuItem value={textLabels.all} key={0}>
-                {textLabels.all}
+      <GridListTile key={index} cols={1} classes={{ tile: classes.gridListTile }}>
+        <FormControl key={index} fullWidth>
+          <InputLabel htmlFor={column.name}>{column.label}</InputLabel>
+          <Select
+            fullWidth
+            value={filterList[index].toString() || textLabels.all}
+            name={column.name}
+            onChange={event => this.handleDropdownChange(event, index, column.name)}
+            input={<Input name={column.name} id={column.name} />}>
+            <MenuItem value={textLabels.all} key={0}>
+              {textLabels.all}
+            </MenuItem>
+            {filterData[index].map((filterValue, filterIndex) => (
+              <MenuItem value={filterValue} key={filterIndex + 1}>
+                {filterValue != null ? filterValue.toString() : ''}
               </MenuItem>
-              {filterData[index].map((filterValue, filterIndex) => (
-                <MenuItem value={filterValue} key={filterIndex + 1}>
-                  {filterValue != null ? filterValue.toString() : ''}
-                </MenuItem>
-              ))}
-            </Select>
-          </FormControl>
-        </div>
+            ))}
+          </Select>
+        </FormControl>
       </GridListTile>
     );
   }
@@ -218,16 +197,15 @@ class TableFilter extends React.Component {
     const { classes, filterList } = this.props;
 
     return (
-      <GridListTile key={index} cols={1}>
-        <div className={classes.textFieldRoot}>
-          <FormControl className={classes.textFieldFormControl} key={index}>
-            <TextField
-              label={column.label}
-              value={filterList[index].toString() || ''}
-              onChange={event => this.handleTextFieldChange(event, index, column.name)}
-            />
-          </FormControl>
-        </div>
+      <GridListTile key={index} cols={1} classes={{ tile: classes.gridListTile }}>
+        <FormControl key={index} fullWidth>
+          <TextField
+            fullWidth
+            label={column.label}
+            value={filterList[index].toString() || ''}
+            onChange={event => this.handleTextFieldChange(event, index, column.name)}
+          />
+        </FormControl>
       </GridListTile>
     );
   }
@@ -236,34 +214,33 @@ class TableFilter extends React.Component {
     const { classes, filterData, filterList } = this.props;
 
     return (
-      <GridListTile key={index} cols={1}>
-        <div className={classes.selectRoot}>
-          <FormControl className={classes.selectFormControl} key={index}>
-            <InputLabel htmlFor={column.name}>{column.label}</InputLabel>
-            <Select
-              multiple
-              value={filterList[index] || []}
-              renderValue={selected => selected.join(', ')}
-              name={column.name}
-              onChange={event => this.handleMultiselectChange(index, event.target.value, column.name)}
-              input={<Input name={column.name} id={column.name} />}>
-              {filterData[index].map((filterValue, filterIndex) => (
-                <MenuItem value={filterValue} key={filterIndex + 1}>
-                  <Checkbox
-                    checked={filterList[index].indexOf(filterValue) >= 0 ? true : false}
-                    value={filterValue != null ? filterValue.toString() : ''}
-                    className={classes.checkboxIcon}
-                    classes={{
-                      root: classes.checkbox,
-                      checked: classes.checked,
-                    }}
-                  />
-                  <ListItemText primary={filterValue} />
-                </MenuItem>
-              ))}
-            </Select>
-          </FormControl>
-        </div>
+      <GridListTile key={index} cols={1} classes={{ tile: classes.gridListTile }}>
+        <FormControl key={index} fullWidth>
+          <InputLabel htmlFor={column.name}>{column.label}</InputLabel>
+          <Select
+            multiple
+            fullWidth
+            value={filterList[index] || []}
+            renderValue={selected => selected.join(', ')}
+            name={column.name}
+            onChange={event => this.handleMultiselectChange(index, event.target.value, column.name)}
+            input={<Input name={column.name} id={column.name} />}>
+            {filterData[index].map((filterValue, filterIndex) => (
+              <MenuItem value={filterValue} key={filterIndex + 1}>
+                <Checkbox
+                  checked={filterList[index].indexOf(filterValue) >= 0 ? true : false}
+                  value={filterValue != null ? filterValue.toString() : ''}
+                  className={classes.checkboxIcon}
+                  classes={{
+                    root: classes.checkbox,
+                    checked: classes.checked,
+                  }}
+                />
+                <ListItemText primary={filterValue} />
+              </MenuItem>
+            ))}
+          </Select>
+        </FormControl>
       </GridListTile>
     );
   }
@@ -280,10 +257,10 @@ class TableFilter extends React.Component {
     }
 
     return (
-      <GridListTile key={index} cols={1}>
-        <div className={classes.textFieldRoot}>
-          <FormControl key={index}>{display(filterList, this.handleCustomChange, index, column)}</FormControl>
-        </div>
+      <GridListTile key={index} cols={1} classes={{ tile: classes.gridListTile }}>
+        <FormControl key={index} fullWidth>
+          {display(filterList, this.handleCustomChange, index, column)}
+        </FormControl>
       </GridListTile>
     );
   }
diff --git a/src/components/TableToolbar.js b/src/components/TableToolbar.js
index 70d9bc4..41bc119 100644
--- a/src/components/TableToolbar.js
+++ b/src/components/TableToolbar.js
@@ -204,12 +204,16 @@ class TableToolbar extends React.Component {
       <Toolbar className={classes.root} role={'toolbar'} aria-label={'Table Toolbar'}>
         <div className={classes.left}>
           {showSearch === true ? (
-            <TableSearch
-              searchText={searchText}
-              onSearch={this.handleSearch}
-              onHide={this.hideSearch}
-              options={options}
-            />
+            options.customSearchRender ? (
+              options.customSearchRender(searchText, this.handleSearch, this.hideSearch, options)
+            ) : (
+              <TableSearch
+                searchText={searchText}
+                onSearch={this.handleSearch}
+                onHide={this.hideSearch}
+                options={options}
+              />
+            )
           ) : typeof title !== 'string' ? (
             title
           ) : (


Test patch:
diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js
index 10d9f9f..cbb7f00 100644
--- a/test/MUIDataTable.test.js
+++ b/test/MUIDataTable.test.js
@@ -2,6 +2,7 @@ import React from 'react';
 import { spy } from 'sinon';
 import { mount, shallow } from 'enzyme';
 import { assert, expect } from 'chai';
+import cloneDeep from 'lodash.clonedeep';
 import MUIDataTable from '../src/MUIDataTable';
 import TableFilterList from '../src/components/TableFilterList';
 import TablePagination from '../src/components/TablePagination';
@@ -429,6 +430,21 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(state.filterList, [['Joe James'], [], [], [], []]);
   });
 
+  it('should apply columns prop change for filterList', () => {
+    const mountShallowWrapper = mount(shallow(<MUIDataTable columns={columns} data={data} />).get(0));
+    const instance = mountShallowWrapper.instance();
+    instance.initializeTable(mountShallowWrapper.props());
+    // now use updated columns props
+    const newColumns = cloneDeep(columns);
+    newColumns[0].options.filterList = ['Joe James'];
+    mountShallowWrapper.setProps({ columns: newColumns });
+    mountShallowWrapper.update();
+    instance.setTableData(mountShallowWrapper.props(), 1 /* instance.TABLE_LOAD.INITIAL */);
+
+    const updatedState = mountShallowWrapper.state();
+    assert.deepEqual(updatedState.filterList, [['Joe James'], [], [], [], []]);
+  });
+
   it('should create Chip when filterList is populated', () => {
     const filterList = [['Joe James'], [], [], [], []];
     const filterListRenderers = [
diff --git a/test/MUIDataTableToolbar.test.js b/test/MUIDataTableToolbar.test.js
index dd9fcce..dd00ccf 100644
--- a/test/MUIDataTableToolbar.test.js
+++ b/test/MUIDataTableToolbar.test.js
@@ -124,6 +124,22 @@ describe('<TableToolbar />', function() {
     assert.strictEqual(actualResult.length, 0);
   });
 
+  it('should render a toolbar with custom search when option.customSearchRender is provided', () => {
+    const CustomSearchRender = () => <h1>customSearchRender</h1>;
+    const newOptions = { ...options, customSearchRender: CustomSearchRender };
+    const shallowWrapper = shallow(
+      <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
+    )
+      .dive()
+      .dive()
+      .dive();
+    const instance = shallowWrapper.instance();
+    instance.setActiveIcon('search');
+    shallowWrapper.update();
+    const actualResult = shallowWrapper.find('h1');
+    assert.strictEqual(actualResult.length, 1);
+  });
+
   it('should render a toolbar with a search clicking search icon', () => {
     const shallowWrapper = shallow(
       <TableToolbar columns={columns} data={data} options={options} setTableAction={setTableAction} />,