Repository: gregnb/mui-datatables
Pull request #683 (2019-06-11 04:22:38)

Feature/issue 430 pr 578 custom filter logic
Continued from https://github.com/gregnb/mui-datatables/pull/578, finishing up some remaining requested changes.

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

Coverage increased (+0.1%) to 77.355% when pulling **46bee9ecdf68fe860106dcc3d895b12dec71f1c7 on feature/issue-430-pr-578-custom-filter-logic** into **2b508722db834843007970c9c8c9c1d9d930cad5 on master**.


Patch:
diff --git a/README.md b/README.md
index c5a216a..b7e9c9c 100644
--- a/README.md
+++ b/README.md
@@ -216,7 +216,7 @@ const columns = [
 |**`empty`**|boolean|false|This denotes whether the column has data or not (for use with intentionally empty 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`**|array||Filter options [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/column-filters/index.js)
+|**`filterOptions`**|{names, logic}||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) and/or custom filter logic [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)
 |**`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')`.
diff --git a/examples/column-filters/index.js b/examples/column-filters/index.js
index 02c9d5b..f300690 100644
--- a/examples/column-filters/index.js
+++ b/examples/column-filters/index.js
@@ -13,7 +13,9 @@ class Example extends React.Component {
           filter: true,
           filterList: ['Franky Miles'],
           customFilterListRender: v => `Name: ${v}`,
-          filterOptions: ['a', 'b', 'c', 'Business Analyst']
+          filterOptions: {
+            names: ['a', 'b', 'c', 'Business Analyst']
+          },
         }
       },
       {
diff --git a/examples/customize-filter/index.js b/examples/customize-filter/index.js
new file mode 100644
index 0000000..d13f05d
--- /dev/null
+++ b/examples/customize-filter/index.js
@@ -0,0 +1,117 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import MUIDataTable from "../../src";
+
+class Example extends React.Component {
+
+  render() {
+
+    const columns = [
+      {
+        name: "Name",
+        options: {
+          filter: true,
+          display: 'excluded',
+        }
+      },
+      {
+        label: "Modified Title Label",
+        name: "Title",
+        options: {
+          filter: true,
+        }
+      },
+      {
+        name: "Location",
+        options: {
+          print: false,
+          filter: false,
+        }
+      },
+      {
+        name: "Age",
+        options: {
+          filter: true,
+          filterOptions: {
+            names: ["young", "adult", "middle-age", "senior", "elderly"],
+            logic(age, filters) {
+              const show = (filters.indexOf("young") >= 0 && age <= 35) ||
+                (filters.indexOf("adult") >= 0 && age > 35 && age <= 45) ||
+                (filters.indexOf("middle-age") >= 0 && age > 45 && age <= 65) ||
+                (filters.indexOf("senior") >= 0 && age > 65 && age <= 75) ||
+                (filters.indexOf("elderly") >= 0 && age > 75);
+              const filtered = !show;
+              return filtered;
+            }
+          },
+          print: false,
+        }
+      },
+      {
+        name: "Salary",
+        options: {
+          filter: true,
+          filterType: "checkbox",
+          filterOptions: {
+            names: ["Lower wages", "Average wages", "Higher wages"],
+            logic(salary, filterVal) {
+              salary = salary.replace(/[^\d]/g, "");
+              const show = (filterVal.indexOf("Lower wages") >= 0 && salary < 100000) ||
+                (filterVal.indexOf("Average wages") >= 0 && salary >= 100000 && salary < 200000) ||
+                (filterVal.indexOf("Higher wages") >= 0 && salary >= 200000);
+              return !show;
+            }
+          },
+          sort: false
+        }
+      }
+    ];
+
+
+    const data = [
+      ["Gabby George", "Business Analyst", "Minneapolis", 30, "$100,000"],
+      ["Aiden Lloyd", "Business Consultant", "Dallas", 55, "$200,000"],
+      ["Jaden Collins", "Attorney", "Santa Ana", 27, "$500,000"],
+      ["Franky Rees", "Business Analyst", "St. Petersburg", 22, "$50,000"],
+      ["Aaren Rose", "Business Consultant", "Toledo", 28, "$75,000"],
+      ["Blake Duncan", "Business Management Analyst", "San Diego", 65, "$94,000"],
+      ["Frankie Parry", "Agency Legal Counsel", "Jacksonville", 71, "$210,000"],
+      ["Lane Wilson", "Commercial Specialist", "Omaha", 19, "$65,000"],
+      ["Robin Duncan", "Business Analyst", "Los Angeles", 20, "$77,000"],
+      ["Mel Brooks", "Business Consultant", "Oklahoma City", 37, "$135,000"],
+      ["Harper White", "Attorney", "Pittsburgh", 52, "$420,000"],
+      ["Kris Humphrey", "Agency Legal Counsel", "Laredo", 30, "$150,000"],
+      ["Frankie Long", "Industrial Analyst", "Austin", 31, "$170,000"],
+      ["Brynn Robbins", "Business Analyst", "Norfolk", 22, "$90,000"],
+      ["Justice Mann", "Business Consultant", "Chicago", 24, "$133,000"],
+      ["Addison Navarro", "Business Management Analyst", "New York", 50, "$295,000"],
+      ["Jesse Welch", "Agency Legal Counsel", "Seattle", 28, "$200,000"],
+      ["Eli Mejia", "Commercial Specialist", "Long Beach", 65, "$400,000"],
+      ["Gene Leblanc", "Industrial Analyst", "Hartford", 34, "$110,000"],
+      ["Danny Leon", "Computer Scientist", "Newark", 60, "$220,000"],
+      ["Lane Lee", "Corporate Counselor", "Cincinnati", 52, "$180,000"],
+      ["Jesse Hall", "Business Analyst", "Baltimore", 44, "$99,000"],
+      ["Danni Hudson", "Agency Legal Counsel", "Tampa", 37, "$90,000"],
+      ["Terry Macdonald", "Commercial Specialist", "Miami", 39, "$140,000"],
+      ["Justice Mccarthy", "Attorney", "Tucson", 26, "$330,000"],
+      ["Silver Carey", "Computer Scientist", "Memphis", 47, "$250,000"],
+      ["Franky Miles", "Industrial Analyst", "Buffalo", 49, "$190,000"],
+      ["Glen Nixon", "Corporate Counselor", "Arlington", 44, "$80,000"],
+      ["Gabby Strickland", "Business Process Consultant", "Scottsdale", 26, "$45,000"],
+      ["Mason Ray", "Computer Scientist", "San Francisco", 39, "$142,000"]
+    ];
+
+    const options = {
+      filter: true,
+      filterType: 'dropdown',
+      responsive: 'scroll',
+    };
+
+    return (
+      <MUIDataTable title={"ACME Employee list - customizeFilter"} data={data} columns={columns} options={options} />
+    );
+
+  }
+}
+
+ReactDOM.render(<Example />, document.getElementById("app-root"));
\ No newline at end of file
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index dfb01dd..db6a775 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -91,7 +91,10 @@ class MUIDataTable extends React.Component {
             download: PropTypes.bool,
             viewColumns: PropTypes.bool,
             filterList: PropTypes.array,
-            filterOptions: PropTypes.array,
+            filterOptions: PropTypes.oneOfType([PropTypes.array, PropTypes.shape({
+              names: PropTypes.array,
+              logic: PropTypes.func,
+            })]),
             filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField']),
             customHeadRender: PropTypes.func,
             customBodyRender: PropTypes.func,
@@ -183,8 +186,8 @@ class MUIDataTable extends React.Component {
     this.tableRef = false;
     this.tableContent = React.createRef();
     this.headCellRefs = {};
-    this.setHeadResizeable = () => {};
-    this.updateDividers = () => {};
+    this.setHeadResizeable = () => { };
+    this.updateDividers = () => { };
   }
 
   componentWillMount() {
@@ -382,17 +385,17 @@ class MUIDataTable extends React.Component {
   transformData = (columns, data) => {
     return Array.isArray(data[0])
       ? data.map(row => {
-          let i = -1;
+        let i = -1;
 
-          return columns.map(col => {
-            if (!col.empty) i++;
-            return col.empty ? undefined : row[i];
-          });
-        })
+        return columns.map(col => {
+          if (!col.empty) i++;
+          return col.empty ? undefined : row[i];
+        });
+      })
       : data.map(row => columns.map(col => row[col.name]));
   };
 
-  setTableData(props, status, callback = () => {}) {
+  setTableData(props, status, callback = () => { }) {
     const { options } = props;
 
     let tableData = [];
@@ -437,7 +440,15 @@ class MUIDataTable extends React.Component {
       }
 
       if (column.filterOptions) {
-        filterData[colIndex] = cloneDeep(column.filterOptions);
+        if (Array.isArray(column.filterOptions)) {
+          filterData[colIndex] = cloneDeep(column.filterOptions);
+          console.error(
+            'Deprecated: filterOptions must now be an object. see https://github.com/gregnb/mui-datatables/tree/master/examples/customize-filter example',
+          );
+        }
+        else if (Array.isArray(column.filterOptions.names)) {
+          filterData[colIndex] = cloneDeep(column.filterOptions.names);
+        }
       }
 
       if (column.filterList) {
@@ -530,8 +541,8 @@ class MUIDataTable extends React.Component {
           typeof funcResult === 'string' || !funcResult
             ? funcResult
             : funcResult.props && funcResult.props.value
-            ? funcResult.props.value
-            : columnValue;
+              ? funcResult.props.value
+              : columnValue;
       }
 
       displayRow.push(columnDisplay);
@@ -542,7 +553,9 @@ class MUIDataTable extends React.Component {
       const caseSensitive = this.options.caseSensitive;
       const filterType = column.filterType || this.options.filterType;
       if (filterVal.length) {
-        if (filterType === 'textField' && !this.hasSearchText(columnVal, filterVal, caseSensitive)) {
+        if (column.filterOptions && column.filterOptions.logic) {
+          if (column.filterOptions.logic(columnValue, filterVal)) isFiltered = true;
+        } else if (filterType === 'textField' && !this.hasSearchText(columnVal, filterVal, caseSensitive)) {
           isFiltered = true;
         } else if (
           filterType !== 'textField' &&
diff --git a/webpack.config.js b/webpack.config.js
index 7cb007b..b98ca64 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -3,7 +3,7 @@ const webpack = require('webpack');
 
 module.exports = {
   entry: {
-    app: "./examples/serverside-pagination/index.js"
+    app: "./examples/customize-filter/index.js"
   },
   stats: "verbose",
   context: __dirname,


Test patch:
diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js
index 512c9e2..e963760 100644
--- a/test/MUIDataTable.test.js
+++ b/test/MUIDataTable.test.js
@@ -11,7 +11,7 @@ import Chip from '@material-ui/core/Chip';
 import Cities from '../examples/component/cities';
 import { getCollatorComparator } from '../src/utils';
 
-describe('<MUIDataTable />', function() {
+describe('<MUIDataTable />', function () {
   let data;
   let displayData;
   let columns;
@@ -371,7 +371,6 @@ describe('<MUIDataTable />', function() {
     const instance = table.instance();
     instance.filterUpdate(0, 'Joe James', 'Name', 'checkbox');
     table.update();
-
     const state = table.state();
     assert.deepEqual(state.filterList, [['Joe James'], [], [], [], []]);
   });
@@ -777,7 +776,7 @@ describe('<MUIDataTable />', function() {
   it('should remove selected data on selectRowDelete when type=cell', () => {
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />).dive();
     const instance = shallowWrapper.instance();
-    const onRowsDelete = () => {};
+    const onRowsDelete = () => { };
     const selectedRows = { data: [1] };
 
     instance.selectRowUpdate('cell', { index: 0, dataIndex: 0 });
@@ -936,6 +935,88 @@ describe('<MUIDataTable />', function() {
     assert.equal(state.displayData.length, data.length);
   });
 
+  describe('should displayData consider filterOptions with logic', () => {
+
+    it('with one column', () => {
+
+      const customFilterColumns = columns.map(c => {
+        if (c.name === "Name") return {
+          ...c,
+          options: {
+            ...c.options,
+            filterOptions: {
+              names: ["B", "J"],
+              logic(name, filters) {
+                const firstLetter = name[0];
+                return firstLetter !== filters[0];
+              }
+            },
+          }
+        };
+        return c;
+      });
+  
+      const shallowWrapper = shallow(<MUIDataTable columns={customFilterColumns} data={data} />);
+      const table = shallowWrapper.dive();
+      const instance = table.instance();
+      instance.filterUpdate(0, 'J', 'checkbox');
+      table.update();
+  
+      const state = table.state();
+      assert.deepEqual(state.filterList, [['J'], [], [], [], []]);
+  
+      const expectedResult = JSON.stringify([
+        { data: ['James, Joe', 'Test Corp', renderCities('Yonkers', { rowIndex: 0 }), 'NY', undefined], dataIndex: 0 },
+      ]);
+      assert.deepEqual(JSON.stringify(state.displayData), expectedResult);
+  
+    });
+  
+    it('operating as AND', () => {
+  
+      const customFilterColumns = columns.map(c => {
+        if (c.name === "Name") return {
+          ...c,
+          options: {
+            ...c.options,
+            filterOptions: {
+              names: ["B", "J", 'H'],
+              logic(name, filters) {
+                const firstLetter = name[0];
+                return firstLetter !== filters[0];
+              }
+            },
+          }
+        };
+        if (c.name === "State") return {
+          ...c,
+          options: {
+            ...c.options,
+            filterOptions: {
+              names: ["NY", "FL", "TX"],
+            },
+          }
+        };
+        return c;
+      });
+  
+      const shallowWrapper = shallow(<MUIDataTable columns={customFilterColumns} data={data} />);
+      const table = shallowWrapper.dive();
+      const instance = table.instance();
+      instance.filterUpdate(0, 'H', 'checkbox');
+      instance.filterUpdate(3, 'TX', 'checkbox');
+      table.update();
+  
+      const state = table.state();
+      assert.deepEqual(state.filterList, [['H'], [], [], ["TX"], []]);
+  
+      const expectedResult = JSON.stringify([
+        { data: ['Houston, James', 'Test Corp', renderCities('Dallas', { rowIndex: 3 }), 'TX', undefined], dataIndex: 3 },
+      ]);
+      assert.deepEqual(JSON.stringify(state.displayData), expectedResult);
+    });    
+  })
+
   describe('should correctly handle array data', () => {
     const columns = [
       {