Repository: gregnb/mui-datatables
Pull request #964 (2019-09-27 19:26:50)

Fix issue by properly checking for basic js objects and preserve displayed index
Closes https://github.com/gregnb/mui-datatables/issues/963.

Also closes https://github.com/gregnb/mui-datatables/issues/973.

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

Coverage decreased (-0.2%) to 76.475% when pulling **0d084ffdfc8883febbbf65bbd5b882dee23dfa72 on bugfix/issue-963-use-displayed-rows-only-with-non-react-objects** into **f0ea9e58d82839c57a4dfd4a342b6649932660a7 on master**.

GuyShaanan: Sorry about nagging - What is the release cycle of this lib / when should we expect this fix to land?

Patch:
diff --git a/README.md b/README.md
index a99b7b7..4ae3c79 100644
--- a/README.md
+++ b/README.md
@@ -135,14 +135,16 @@ The component accepts the following props:
 |**`page`**|number||User provided starting page for pagination
 |**`count`**|number||User provided override for total number of rows
 |**`serverSide`**|boolean|false|Enable remote data source
+|**`serverSideFilterList`**|array|[]|Sets the filter list display when using serverSide: true
 |**`rowsSelected`**|array||User provided selected rows
 |**`rowsExpanded`**|array||User provided expanded rows
-|**`filterType `**|string||Choice of filtering view. `enum('checkbox', 'dropdown', 'multiselect', 'textField')`
-|**`textLabels `**|object||User provided labels to localize text
+|**`filterType`**|string||Choice of filtering view. `enum('checkbox', 'dropdown', 'multiselect', 'textField', 'custom')`
+|**`textLabels`**|object||User provided labels to localize text
 |**`pagination`**|boolean|true|Enable/disable pagination
 |**`selectableRows`**|string|'multiple'|Numbers of rows that can be selected. Options are "multiple", "single", "none".
 |**`selectableRowsOnClick`**|boolean|false|Enable/disable select toggle when row is clicked. When False, only checkbox will trigger this action.
-|**`isRowSelectable`**|function||Enable/disable selection on certain rows with custom function. Returns true if not provided. `function(dataIndex: number, selectedRows: object(lookup: {[dataindex]: boolean}, data: arrayOfObjects: {index: number, dataIndex: number})) => boolean`.
+|**`isRowSelectable`**|function||Enable/disable selection on certain rows with custom function. Returns true if not provided. `function(dataIndex: number, selectedRows: object(lookup: {dataindex: boolean}, data: arrayOfObjects: {index, dataIndex})) => boolean`.
+|**`isRowExpandable`**|function||Enable/disable expansion or collapse on certain expandable rows with custom function. Will be considered true if not provided. `function(dataIndex: number, expandedRows: object(lookup: {dataIndex: number}, data: arrayOfObjects: {index: number, dataIndex: number})) => boolean`.
 |**`selectableRowsHeader`**|boolean|true|Show/hide the select all/deselect all checkbox header for selectable rows
 |**`expandableRows`**|boolean|false|Enable/disable expandable rows
 |**`expandableRowsOnClick`**|boolean|false|Enable/disable expand trigger when row is clicked. When False, only expand icon will trigger this action.
@@ -155,6 +157,7 @@ The component accepts the following props:
 |**`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`
+|**`customFilterDialogFooter `**|function||Add a custom footer to the filter dialog. `customFilterDialogFooter(filterList: array) => 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', 'scrollMaxHeight' (limits height of table), 'scrollFullHeight' (table takes on as much height as needed to display all rows set in rowsPerPage)
@@ -166,6 +169,7 @@ The component accepts the following props:
 |**`sort`**|boolean|true|Enable/disable sort on all columns
 |**`filter`**|boolean|true|Show/hide filter icon from toolbar
 |**`search`**|boolean|true|Show/hide search icon from toolbar
+|**`searchOpen`**|boolean|false|Initially displays search bar  
 |**`searchText`**|string||Initial search text
 |**`searchPlaceholder`**|string||Search text placeholder. [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-search/index.js)
 |**`print`**|boolean|true|Show/hide print	 icon from toolbar
@@ -174,6 +178,7 @@ The component accepts the following props:
 |**`onDownload`**|function||A callback function that triggers when the user downloads the CSV file. In the callback, you can control what is written to the CSV file. `function(buildHead: (columns) => string, buildBody: (data) => string, columns, data) => string`. Return `false` to cancel download of file.
 |**`viewColumns`**|boolean|true|Show/hide viewColumns icon from toolbar
 |**`onRowsSelect`**|function||Callback function that triggers when row(s) are selected. `function(currentRowsSelected: array, allRowsSelected: array) => void`
+|**`onRowsExpand`**|function||Callback function that triggers when row(s) are expanded. `function(currentRowsExpanded: array, allRowsExpanded: array) => void`
 |**`onRowsDelete`**|function||Callback function that triggers when row(s) are deleted. `function(rowsDeleted: object(lookup: {[dataIndex]: boolean}, data: arrayOfObjects: {index: number, dataIndex: number})) => void OR false` (Returning `false` prevents row deletion.)
 |**`onRowClick`**|function||Callback function that triggers when a row is clicked. `function(rowData: string[], rowMeta: { dataIndex: number, rowIndex: number }) => void`
 |**`onCellClick`**|function||Callback function that triggers when a cell is clicked. `function(colData: any, cellMeta: { colIndex: number, rowIndex: number, dataIndex: number }) => void`
@@ -181,8 +186,10 @@ The component accepts the following props:
 |**`onChangeRowsPerPage`**|function||Callback function that triggers when the number of rows per page has changed. `function(numberOfRows: number) => void`
 |**`onSearchChange`**|function||Callback function that triggers when the search text value has changed. `function(searchText: string) => void`
 |**`onSearchOpen`**|function||Callback function that triggers when the searchbox opens. `function() => void`
+|**`onFilterDialogOpen`**|function||Callback function that triggers when the filter dialog opens. `function() => void`
+|**`onFilterDialogClose`**|function||Callback function that triggers when the filter dialog closes. `function() => void`
+|**`onFilterChange`**|function||Callback function that triggers when filters have changed. `function(changedColumn: string, filterList: array, type: enum('checkbox', 'dropdown', 'multiselect', 'textField', 'custom', 'chip', 'reset')) => void`
 |**`onSearchClose`**|function||Callback function that triggers when the searchbox closes. `function() => void`
-|**`onFilterChange`**|function||Callback function that triggers when filters have changed. `function(changedColumn: string, filterList: array) => void`
 |**`onColumnSortChange`**|function||Callback function that triggers when a column has been sorted. `function(changedColumn: string, direction: string) => void`
 |**`onColumnViewChange`**|function||Callback function that triggers when a column view has been changed. `function(changedColumn: string, action: string) => void`
 |**`onTableChange`**|function||Callback function that triggers when table state has changed. `function(action: string, tableState: object) => void`
diff --git a/examples/examples.js b/examples/examples.js
index 59972ed..6746175 100644
--- a/examples/examples.js
+++ b/examples/examples.js
@@ -22,6 +22,7 @@ import OnDownload from "./on-download";
 import OnTableInit from "./on-table-init";
 import ResizableColumns from "./resizable-columns";
 import SelectableRows from "./selectable-rows";
+import ServerSideFilters from "./serverside-filters";
 import ServerSideOptions from "./serverside-options";
 import ServerSidePagination from "./serverside-pagination";
 import Simple from "./simple";
@@ -38,8 +39,8 @@ export default {
     'Column Option Update': ColumnOptionsUpdate,
     'Component': Component,
     'CSV Export': CSVExport,
-    'Custom ActionColumns': CustomActionColumns,
-    'Customize columns': CustomizeColumns,
+    'Custom Action Columns': CustomActionColumns,
+    'Customize Columns': CustomizeColumns,
     'Customize Filter': CustomizeFilter,
     'Customize Footer': CustomizeFooter,
     'Customize Rows': CustomizeRows,
@@ -50,15 +51,16 @@ export default {
     'Customize Toolbar': CustomizeToolbar,
     'Customize Toolbar Select': CustomizeToolbarSelect,
     'Data As Objects': DataAsObjects,
-    'expandableRows': ExpandableRows,
-    'fixedHeader': FixedHeader,
+    'Expandable Rows': ExpandableRows,
+    'Fixed Header': FixedHeader,
     'Hide Columns Print': HideColumnsPrint,
     'OnDownload': OnDownload,
     'OnTableInit': OnTableInit,
-    'resizableColumns': ResizableColumns,
-    'selectableRows': SelectableRows,
-    'serverSide Options': ServerSideOptions,
-    'serverSide Pagination': ServerSidePagination,
+    'Resizable Columns': ResizableColumns,
+    'Selectable Rows': SelectableRows,
+    'ServerSide Filters': ServerSideFilters,
+    'ServerSide Options': ServerSideOptions,
+    'ServerSide Pagination': ServerSidePagination,
     'Simple': Simple,
     'Simple No Toolbar': SimpleNoToolbar,
     'Text Localization': TextLocalization,
diff --git a/examples/expandable-rows/index.js b/examples/expandable-rows/index.js
index ba67d66..4ef1dca 100644
--- a/examples/expandable-rows/index.js
+++ b/examples/expandable-rows/index.js
@@ -82,7 +82,12 @@ class Example extends React.Component {
       responsive: 'scrollMaxHeight',
       expandableRows: true,
       expandableRowsOnClick: true,
-      rowsExpanded: [0, 2, 3],
+      isRowExpandable: (dataIndex, expandedRows) => {
+        // Prevent expand/collapse of any row after the 5th (but allow those already expanded to be collapsed)
+        if (expandedRows.data.length > 4 && expandedRows.data.filter(d => d.dataIndex === dataIndex).length === 0) return false;
+        return true;
+      },
+      rowsExpanded: [0, 1],
       renderExpandableRow: (rowData, rowMeta) => {
         const colSpan = rowData.length + 1;
         return (
@@ -92,7 +97,8 @@ class Example extends React.Component {
             </TableCell>
           </TableRow>
         );
-      }
+      },
+      onRowsExpand: (curExpanded, allExpanded) => console.log(curExpanded, allExpanded)
     };
 
     return (
diff --git a/examples/serverside-filters/index.js b/examples/serverside-filters/index.js
new file mode 100644
index 0000000..6ebfaf7
--- /dev/null
+++ b/examples/serverside-filters/index.js
@@ -0,0 +1,196 @@
+import { Button, CircularProgress } from '@material-ui/core';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import MUIDataTable from '../../src';
+
+class Example extends React.Component {
+  state = {
+    serverSideFilterList: [],
+    filters: [[], [], [], [], []],
+    isLoading: false,
+    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', 'Chicago', 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'],
+    ]
+  };
+
+  // mock async function
+  xhrRequest = (url, filterList) => {
+    return new Promise(resolve => {
+      window.setTimeout(
+        () => {
+          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', 'Chicago', 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 newData = [
+            ['Lane Wilson', 'Commercial Specialist', 'Chicago', 19, '$65,000'],
+            ['Justice Mann', 'Business Consultant', 'Chicago', 24, '$133,000']
+          ];
+
+          if (
+            !filterList[0].length
+            && !filterList[1].length
+            && !filterList[2].length
+            && !filterList[3].length
+            && !filterList[4].length
+          ) {
+            resolve({ data });
+          } else {
+            resolve({ data: newData });
+          }
+        },
+        2000
+      );
+    });
+  }
+
+  handleFilterSubmit = filterList => () => {
+    console.log('Submitting filters: ', filterList);
+
+    this.setState({ isLoading: true, filters: filterList });
+
+    // fake async request
+    this.xhrRequest(`/myApiServer?filters=${filterList}`, filterList).then(res => {
+      this.setState({ isLoading: false, data: res.data, serverSideFilterList: filterList });
+    });
+  };
+
+  render() {
+    const columns = [
+      {
+        name: 'Name',
+        options: {
+          filter: true,
+          filterList: this.state.filters[0],
+        },
+      },
+      {
+        label: 'Title',
+        name: 'Title',
+        options: {
+          filter: true,
+          filterList: this.state.filters[1],
+        },
+      },
+      {
+        name: 'Location',
+        options: {
+          filter: true,
+          filterList: this.state.filters[2],
+        },
+      },
+      {
+        name: 'Age',
+        options: {
+          filter: true,
+          filterList: this.state.filters[3],
+        },
+      },
+      {
+        name: 'Salary',
+        options: {
+          filter: true,
+          filterList: this.state.filters[4],
+        },
+      },
+    ];
+
+    const options = {
+      filter: true,
+      serverSideFilterList: this.state.serverSideFilterList,
+      filterType: 'dropdown',
+      responsive: 'scrollMaxHeight',
+      serverSide: true,
+      onFilterDialogOpen: () => {
+        console.log('filter dialog opened');
+      },
+      onFilterDialogClose: () => {
+        console.log('filter dialog closed');
+      },
+      onFilterChange: (column, filterList, type) => {
+        if (type === 'chip') {
+          console.log('updating filters via chip');
+          this.handleFilterSubmit(filterList)();
+        }
+      },
+      customFilterDialogFooter: filterList => {
+        return (
+          <div style={{ marginTop: '40px' }}>
+            <Button variant="contained" onClick={this.handleFilterSubmit(filterList)}>Apply Filters*</Button>
+            <p><em>*(Simulates selecting "Chicago" from "Location")</em></p>
+          </div>
+        );
+      }
+    };
+
+    return (
+      <React.Fragment>
+        {this.state.isLoading && (
+          <div style={{ position: 'absolute', top: '50%', left: '50%' }}>
+            <CircularProgress />
+          </div>
+        )}
+        <MUIDataTable title={'ACME Employee list'} data={this.state.data} columns={columns} options={options} />
+      </React.Fragment>
+    );
+  }
+}
+
+export default Example;
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index d0fe3d5..75d6526 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -142,14 +142,20 @@ class MUIDataTable extends React.Component {
       customFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customSearchRender: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
       customRowRender: PropTypes.func,
+      customFilterDialogFooter: PropTypes.func,
       onRowClick: PropTypes.func,
+      onRowsExpand: PropTypes.func,
       onRowsSelect: PropTypes.func,
       resizableColumns: PropTypes.bool,
       selectableRows: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['none', 'single', 'multiple'])]),
       selectableRowsOnClick: PropTypes.bool,
       isRowSelectable: PropTypes.func,
+      isRowExpandable: PropTypes.func,
       selectableRowsHeader: PropTypes.bool,
       serverSide: PropTypes.bool,
+      onFilterChange: PropTypes.func,
+      onFilterDialogOpen: PropTypes.func,
+      onFilterDialogClose: PropTypes.func,
       onTableChange: PropTypes.func,
       onTableInit: PropTypes.func,
       caseSensitive: PropTypes.bool,
@@ -166,6 +172,7 @@ class MUIDataTable extends React.Component {
       customSort: PropTypes.func,
       customSearch: PropTypes.func,
       search: PropTypes.bool,
+      searchOpen: PropTypes.bool,
       searchText: PropTypes.string,
       searchPlaceholder: PropTypes.string,
       print: PropTypes.bool,
@@ -262,7 +269,7 @@ class MUIDataTable extends React.Component {
       return;
     });
 
-    this.handleOptionDeprecation(props);
+    this.handleOptionDeprecation();
   }
 
   initializeTable(props) {
@@ -278,6 +285,7 @@ class MUIDataTable extends React.Component {
     filterType: 'dropdown',
     pagination: true,
     textLabels,
+    serverSideFilterList: [],
     expandableRows: false,
     expandableRowsOnClick: false,
     resizableColumns: false,
@@ -304,19 +312,19 @@ class MUIDataTable extends React.Component {
     },
   });
 
-  handleOptionDeprecation = props => {
-    if (typeof props.options.selectableRows === 'boolean') {
+  handleOptionDeprecation = () => {
+    if (typeof this.options.selectableRows === 'boolean') {
       console.error(
         'Using a boolean for selectableRows has been deprecated. Please use string option: multiple | single | none',
       );
-      this.options.selectableRows = props.options.selectableRows ? 'multiple' : 'none';
+      this.options.selectableRows = this.options.selectableRows ? 'multiple' : 'none';
     }
-    if (['scrollMaxHeight', 'scrollFullHeight', 'stacked'].indexOf(props.options.responsive) === -1) {
+    if (['scrollMaxHeight', 'scrollFullHeight', 'stacked'].indexOf(this.options.responsive) === -1) {
       console.error(
         'Invalid option value for responsive. Please use string option: scrollMaxHeight | scrollFullHeight | stacked',
       );
     }
-    if (props.options.responsive === 'scroll') {
+    if (this.options.responsive === 'scroll') {
       console.error('This option has been deprecated. It is being replaced by scrollMaxHeight');
     }
   };
@@ -629,7 +637,7 @@ class MUIDataTable extends React.Component {
     let isFiltered = false;
     let isSearchFound = false;
     let displayRow = [];
-    const data = this.state.data.length ? this.state.data : this.props.data;
+
     for (let index = 0; index < row.length; index++) {
       let columnDisplay = row[index];
       let columnValue = row[index];
@@ -946,7 +954,7 @@ class MUIDataTable extends React.Component {
       () => {
         this.setTableAction('resetFilters');
         if (this.options.onFilterChange) {
-          this.options.onFilterChange(null, this.state.filterList);
+          this.options.onFilterChange(null, this.state.filterList, 'reset');
         }
       },
     );
@@ -962,6 +970,9 @@ class MUIDataTable extends React.Component {
           case 'checkbox':
             filterPos >= 0 ? filterList[index].splice(filterPos, 1) : filterList[index].push(value);
             break;
+          case 'chip':
+            filterPos >= 0 ? filterList[index].splice(filterPos, 1) : filterList[index].push(value);
+            break;
           case 'multiselect':
             filterList[index] = value === '' ? [] : value;
             break;
@@ -987,7 +998,7 @@ class MUIDataTable extends React.Component {
       () => {
         this.setTableAction('filterChange');
         if (this.options.onFilterChange) {
-          this.options.onFilterChange(column, this.state.filterList);
+          this.options.onFilterChange(column, this.state.filterList, type);
         }
       },
     );
@@ -1020,31 +1031,43 @@ class MUIDataTable extends React.Component {
 
   toggleExpandRow = row => {
     const { dataIndex } = row;
-    let expandedRows = [...this.state.expandedRows.data];
-    let rowPos = -1;
-
-    for (let cIndex = 0; cIndex < expandedRows.length; cIndex++) {
-      if (expandedRows[cIndex].dataIndex === dataIndex) {
-        rowPos = cIndex;
+    const { isRowExpandable } = this.options;
+    let { expandedRows } = this.state;
+    const expandedRowsData = [...expandedRows.data];
+    let shouldCollapseExpandedRow = false;
+    let hasRemovedRow = false;
+    let removedRow = [];
+
+    for (var cIndex = 0; cIndex < expandedRowsData.length; cIndex++) {
+      if (expandedRowsData[cIndex].dataIndex === dataIndex) {
+        shouldCollapseExpandedRow = true;
         break;
       }
     }
 
-    if (rowPos >= 0) {
-      expandedRows.splice(rowPos, 1);
+    if (shouldCollapseExpandedRow) {
+      if ((isRowExpandable && isRowExpandable(dataIndex, expandedRows) || !isRowExpandable)) {
+        removedRow = expandedRowsData.splice(cIndex, 1);
+        hasRemovedRow = true;
+      }
     } else {
-      expandedRows.push(row);
+      if (isRowExpandable && isRowExpandable(dataIndex, expandedRows)) expandedRowsData.push(row);
+      else if (!isRowExpandable) expandedRowsData.push(row);
     }
 
     this.setState(
       {
+        curExpandedRows: hasRemovedRow ? removedRow : [row],
         expandedRows: {
-          lookup: buildMap(expandedRows),
-          data: expandedRows,
+          lookup: buildMap(expandedRowsData),
+          data: expandedRowsData,
         },
       },
       () => {
         this.setTableAction('expandRow');
+        if (this.options.onRowsExpand) {
+          this.options.onRowsExpand(this.state.curExpandedRows, this.state.expandedRows.data);
+        }
       },
     );
   };
@@ -1223,6 +1246,7 @@ class MUIDataTable extends React.Component {
       previousSelectedRow,
       expandedRows,
       searchText,
+      serverSideFilterList,
     } = this.state;
 
     const rowCount = this.state.count || displayData.length;
@@ -1282,6 +1306,7 @@ class MUIDataTable extends React.Component {
         )}
         <TableFilterList
           options={this.options}
+          serverSideFilterList={this.props.options.serverSideFilterList || []}
           filterListRenderers={columns.map(c => {
             return c.customFilterListRender ? c.customFilterListRender : f => f;
           })}
@@ -1333,7 +1358,6 @@ class MUIDataTable extends React.Component {
           options={this.options}
           page={page}
           rowCount={rowCount}
-          rowsPerPageOptions={this.options.rowsPerPageOptions}
           rowsPerPage={rowsPerPage}
           changeRowsPerPage={this.changeRowsPerPage}
           changePage={this.changePage}
diff --git a/src/components/Popover.js b/src/components/Popover.js
index a912316..f51d97c 100644
--- a/src/components/Popover.js
+++ b/src/components/Popover.js
@@ -78,7 +78,8 @@ class Popover extends React.Component {
           ref={el => this.popoverEl}
           anchorOrigin={anchorOriginSpecs}
           transformOrigin={transformOriginSpecs}
-          {...providedProps}>
+          {...providedProps}
+        >
           {content}
         </MuiPopover>
         {triggerEl}
diff --git a/src/components/TableBody.js b/src/components/TableBody.js
index efa91e9..a8ed200 100644
--- a/src/components/TableBody.js
+++ b/src/components/TableBody.js
@@ -29,6 +29,8 @@ class TableBody extends React.Component {
     filterList: PropTypes.array,
     /** Callback to execute when row is clicked */
     onRowClick: PropTypes.func,
+    /** Table rows expanded */
+    expandedRows: PropTypes.object,
     /** Table rows selected */
     selectedRows: PropTypes.object,
     /** Callback to trigger table row select */
@@ -100,6 +102,15 @@ class TableBody extends React.Component {
     }
   }
 
+  isRowExpandable(dataIndex) {
+    const { options, expandedRows } = this.props;
+    if (options.isRowExpandable) {
+      return options.isRowExpandable(dataIndex, expandedRows);
+    } else {
+      return true;
+    }
+  }
+
   handleRowSelect = (data, event) => {
     let shiftKey = event && event.nativeEvent ? event.nativeEvent.shiftKey : false;
     let shiftAdjacentRows = [];
@@ -177,7 +188,11 @@ class TableBody extends React.Component {
       this.handleRowSelect(selectRow, event);
     }
     // Check if we should trigger row expand when row is clicked anywhere
-    if (this.props.options.expandableRowsOnClick && this.props.options.expandableRows) {
+    if (
+      this.props.options.expandableRowsOnClick &&
+      this.props.options.expandableRows &&
+      this.isRowExpandable(data.dataIndex, this.props.expandedRows)
+    ) {
       const expandRow = { index: data.rowIndex, dataIndex: data.dataIndex };
       this.props.toggleExpandRow(expandRow);
     }
diff --git a/src/components/TableFilter.js b/src/components/TableFilter.js
index b0ed72e..7975360 100644
--- a/src/components/TableFilter.js
+++ b/src/components/TableFilter.js
@@ -266,7 +266,7 @@ class TableFilter extends React.Component {
   }
 
   render() {
-    const { classes, columns, options, onFilterReset } = this.props;
+    const { classes, columns, options, onFilterReset, customFooter, filterList } = this.props;
     const textLabels = options.textLabels.filter;
     const filterGridColumns = columns.filter(col => col.filter).length === 1 ? 1 : 2;
 
@@ -309,6 +309,7 @@ class TableFilter extends React.Component {
             }
           })}
         </GridList>
+        {customFooter ? customFooter(filterList) : ''}
       </div>
     );
   }
diff --git a/src/components/TableFilterList.js b/src/components/TableFilterList.js
index 6866a23..61d3c0b 100644
--- a/src/components/TableFilterList.js
+++ b/src/components/TableFilterList.js
@@ -35,30 +35,42 @@ class TableFilterList extends React.Component {
   };
 
   render() {
-    const { classes, filterList, filterUpdate, filterListRenderers, columnNames } = this.props;
+    const { classes, filterList, filterUpdate, filterListRenderers, columnNames, serverSideFilterList } = this.props;
+    const { serverSide } = this.props.options;
+
+    const customFilterChip = (item, index) => (
+      <Chip
+        label={filterListRenderers[index](item)}
+        key={index}
+        onDelete={filterUpdate.bind(null, index, [], columnNames[index].name, columnNames[index].filterType)}
+        className={classes.chip}
+      />
+    );
+
+    const filterChip = (index, data, colIndex) => (
+      <Chip
+        label={filterListRenderers[index](data)}
+        key={colIndex}
+        onDelete={filterUpdate.bind(null, index, data, columnNames[index].name, 'chip')}
+        className={classes.chip}
+      />
+    );
 
     return (
       <div className={classes.root}>
-        {filterList.map((item, index) => {
+        {serverSide ? serverSideFilterList.map((item, index) => {
+          if (columnNames[index].filterType === 'custom' && filterListRenderers[index](item)) {
+            return customFilterChip(item, index);
+          }
+
+          return item.map((data, colIndex) => filterChip(index, data, colIndex));
+        }) :
+        filterList.map((item, index) => {
           if (columnNames[index].filterType === 'custom' && filterListRenderers[index](item)) {
-            return (
-              <Chip
-                label={filterListRenderers[index](item)}
-                key={index}
-                onDelete={filterUpdate.bind(null, index, [], columnNames[index].name, columnNames[index].filterType)}
-                className={classes.chip}
-              />
-            );
+            return customFilterChip(item, index);
           }
 
-          return item.map((data, colIndex) => (
-            <Chip
-              label={filterListRenderers[index](data)}
-              key={colIndex}
-              onDelete={filterUpdate.bind(null, index, data, columnNames[index].name, 'checkbox')}
-              className={classes.chip}
-            />
-          ));
+          return item.map((data, colIndex) => filterChip(index, data, colIndex));
         })}
       </div>
     );
diff --git a/src/components/TableHead.js b/src/components/TableHead.js
index fd1a53c..ab2d05d 100644
--- a/src/components/TableHead.js
+++ b/src/components/TableHead.js
@@ -30,7 +30,7 @@ class TableHead extends React.Component {
   };
 
   render() {
-    const { classes, columns, count, options, data, page, setCellRef, selectedRows } = this.props;
+    const { classes, columns, count, options, data, setCellRef, selectedRows } = this.props;
 
     const numSelected = (selectedRows && selectedRows.data.length) || 0;
     const isDeterminate = numSelected > 0 && numSelected < count;
diff --git a/src/components/TableToolbar.js b/src/components/TableToolbar.js
index 1f264de..a7ef8c4 100644
--- a/src/components/TableToolbar.js
+++ b/src/components/TableToolbar.js
@@ -80,7 +80,7 @@ export const defaultToolbarStyles = theme => ({
 class TableToolbar extends React.Component {
   state = {
     iconActive: null,
-    showSearch: Boolean(this.props.searchText || this.props.options.searchText),
+    showSearch: Boolean(this.props.searchText || this.props.options.searchText || this.props.options.searchOpen),
     searchText: this.props.searchText || null,
   };
 
@@ -98,15 +98,19 @@ class TableToolbar extends React.Component {
     if (options.downloadOptions && options.downloadOptions.filterOptions) {
       // check rows first:
       if (options.downloadOptions.filterOptions.useDisplayedRowsOnly) {
-        dataToDownload = displayData.map(row => {
+        dataToDownload = displayData.map((row, index) => {
           let i = -1;
 
+          // Help to preserve sort order in custom render columns
+          row.index = index;
+
           return {
             data: row.data.map(column => {
               i += 1;
 
-              // if we have a custom render, we must grab the actual value from data
-              return typeof column === 'object' ? data[row.dataIndex].data[i] : column;
+              // if we have a custom render, which will appear as a react element, we must grab the actual value from data
+              // TODO: Create a utility function for checking whether or not something is a react object
+              return typeof column === 'object' && column !== null && !Array.isArray(column) ? data[row.index].data[i] : column;
             }),
           };
         });
@@ -126,10 +130,26 @@ class TableToolbar extends React.Component {
   };
 
   setActiveIcon = iconName => {
-    this.setState(() => ({
+    this.setState(prevState => ({
       showSearch: this.isSearchShown(iconName),
       iconActive: iconName,
-    }));
+      prevIconActive: prevState.iconActive
+    }), () => {
+      const { iconActive, prevIconActive } = this.state;
+
+      if (iconActive === 'filter') {
+        this.props.setTableAction('onFilterDialogOpen');
+        if (this.props.options.onFilterDialogOpen) {
+          this.props.options.onFilterDialogOpen();
+        }
+      }
+      if (iconActive === undefined && prevIconActive === 'filter') {
+        this.props.setTableAction('onFilterDialogClose');
+        if (this.props.options.onFilterDialogClose) {
+          this.props.options.onFilterDialogClose();
+        }
+      }
+    });
   };
 
   isSearchShown = iconName => {
@@ -301,6 +321,7 @@ class TableToolbar extends React.Component {
               }
               content={
                 <TableFilter
+                  customFooter={options.customFilterDialogFooter}
                   columns={columns}
                   options={options}
                   filterList={filterList}


Test patch:
diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js
index c6265ec..2a75338 100644
--- a/test/MUIDataTable.test.js
+++ b/test/MUIDataTable.test.js
@@ -606,6 +606,7 @@ describe('<MUIDataTable />', function() {
 
     const mountWrapper = mount(
       <TableFilterList
+        options={{ serverSide: false }}
         filterList={filterList}
         filterListRenderers={filterListRenderers}
         filterUpdate={() => true}
@@ -627,6 +628,7 @@ describe('<MUIDataTable />', function() {
 
     const mountWrapper = mount(
       <TableFilterList
+        options={{ serverSide: false }}
         filterList={filterList}
         filterListRenderers={filterListRenderers}
         filterUpdate={() => true}
@@ -638,6 +640,55 @@ describe('<MUIDataTable />', function() {
     assert.strictEqual(actualResult.prop('label'), 'Name: Joe James');
   });
 
+  it('should render filter Chip(s) when options.serverSide = true and serverSideFilterList is populated', () => {
+    const serverSideFilterList = [['Joe James'], [], [], [], []];
+    const filterListRenderers = [
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+    ];
+    const columnNames = columns.map(column => ({ name: column.name }));
+
+    const mountWrapper = mount(
+      <TableFilterList
+        options={{ serverSide: true }}
+        serverSideFilterList={serverSideFilterList}
+        filterListRenderers={filterListRenderers}
+        filterUpdate={() => true}
+        columnNames={columnNames}
+      />,
+    );
+
+    const actualResult = mountWrapper.find(Chip);
+    assert.strictEqual(actualResult.length, 1);
+  });
+
+  it('should not render filter Chip(s) when options.serverSide = true and serverSideFilterList is not populated', () => {
+    const filterListRenderers = [
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+      defaultRenderCustomFilterList,
+    ];
+    const columnNames = columns.map(column => ({ name: column.name }));
+
+    const mountWrapper = mount(
+      <TableFilterList
+        options={{ serverSide: true }}
+        serverSideFilterList={[]}
+        filterListRenderers={filterListRenderers}
+        filterUpdate={() => true}
+        columnNames={columnNames}
+      />,
+    );
+
+    const actualResult = mountWrapper.find(Chip);
+    assert.strictEqual(actualResult.length, 0);
+  });
+
   it('should remove entry from filterList when calling filterUpdate method with type=dropdown and an empty array', () => {
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />);
     const table = shallowWrapper.dive();
@@ -1109,6 +1160,32 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(JSON.stringify(newDisplayData), expectedResult);
   });
 
+  it('should call onRowsExpand when row is expanded or collapsed', () => {
+    const options = {
+      expandableRows: true,
+      renderExpandableRow: () => <tr><td>foo</td></tr>,
+      expandableRowsOnClick: true,
+      onRowsExpand: spy()
+    };
+    const mountWrapper = mount(<MUIDataTable columns={columns} data={data} options={options} />);
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.strictEqual(options.onRowsExpand.callCount, 1);
+    assert(options.onRowsExpand.calledWith([{ index: 2, dataIndex: 2 }], [{ index: 2, dataIndex: 2 }]));
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.strictEqual(options.onRowsExpand.callCount, 2);
+    assert(options.onRowsExpand.calledWith([{ index: 2, dataIndex: 2 }], []));
+  });
+
   it('should not remove selected data on selectRowDelete when type=cell when onRowsDelete returns false', () => {
     const options = {
       onRowsDelete: () => false,
diff --git a/test/MUIDataTableBody.test.js b/test/MUIDataTableBody.test.js
index 71a0423..c105eec 100644
--- a/test/MUIDataTableBody.test.js
+++ b/test/MUIDataTableBody.test.js
@@ -307,12 +307,47 @@ describe('<TableBody />', function() {
     assert.strictEqual(toggleExpandRow.callCount, 0);
   });
 
+  it('should not gather expanded row data when clicking row with expandableRowsOnClick=true when it is disabled with isRowExpandable via dataIndex.', () => {
+    let expandedRowData;
+    const options = {
+      expandableRows: true,
+      renderExpandableRow: () => <tr><td>foo</td></tr>,
+      expandableRowsOnClick: true,
+      isRowExpandable: dataIndex => dataIndex === 2 ? false : true,
+    };
+    const toggleExpandRow = spy((_, data) => expandedRowData = data);
+
+    const mountWrapper = mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        rowsPerPage={10}
+        selectedRows={[]}
+        expandedRows={[]}
+        toggleExpandRow={toggleExpandRow}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.isUndefined(expandedRowData);
+    assert.strictEqual(toggleExpandRow.callCount, 0);
+  });
+
   it('should not gather selected row data when clicking row with selectableRowsOnClick=true when it is disabled with isRowSelectable via selectedRows.', () => {
     let selectedRowData;
     const options = {
       selectableRows: true,
       selectableRowsOnClick: true,
-      isRowSelectable: (index, selectedRows) => selectedRows.lookup[index] || selectedRows.data.length < 1,
+      isRowSelectable: (dataIndex, selectedRows) => selectedRows.lookup[dataIndex] || selectedRows.data.length < 1,
     };
     const selectRowUpdate = (_, data) => (selectedRowData = data);
     const toggleExpandRow = spy();
@@ -347,12 +382,12 @@ describe('<TableBody />', function() {
     assert.strictEqual(toggleExpandRow.callCount, 0);
   });
 
-  it('should gather selected row data when clicking row with selectableRowsOnClick=true when it is enabled with isRowSelectable via index.', () => {
+  it('should gather selected row data when clicking row with selectableRowsOnClick=true when it is enabled with isRowSelectable via dataIndex.', () => {
     let selectedRowData;
     const options = {
       selectableRows: true,
       selectableRowsOnClick: true,
-      isRowSelectable: (index, selectedRows) => selectedRows.lookup[index] || selectedRows.data.length < 1,
+      isRowSelectable: (dataIndex, selectedRows) => selectedRows.lookup[dataIndex] || selectedRows.data.length < 1,
     };
     const selectRowUpdate = (_, data) => (selectedRowData = data);
     const toggleExpandRow = spy();
@@ -387,6 +422,41 @@ describe('<TableBody />', function() {
     assert.strictEqual(toggleExpandRow.callCount, 0);
   });
 
+  it('should gather expanded row data when clicking row with expandableRowsOnClick=true when it is enabled with isRowExpandable via dataIndex.', () => {
+    let expandedRowData;
+    const options = {
+      expandableRows: true,
+      renderExpandableRow: () => <tr><td>foo</td></tr>,
+      expandableRowsOnClick: true,
+      isRowExpandable: dataIndex => dataIndex === 2 ? true : false,
+    };
+    const toggleExpandRow = spy(data => expandedRowData = data);
+
+    const mountWrapper = mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        rowsPerPage={10}
+        selectedRows={[]}
+        expandedRows={[]}
+        toggleExpandRow={toggleExpandRow}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.isDefined(expandedRowData);
+    assert.strictEqual(toggleExpandRow.callCount, 1);
+  });
+
   it('should gather expanded row data when clicking row with expandableRows=true and expandableRowsOnClick=true.', () => {
     let expandedRowData;
     const options = { selectableRows: true, expandableRows: true, expandableRowsOnClick: true };
diff --git a/test/MUIDataTableFilter.test.js b/test/MUIDataTableFilter.test.js
index 7c2fcb2..c5b3cb6 100644
--- a/test/MUIDataTableFilter.test.js
+++ b/test/MUIDataTableFilter.test.js
@@ -186,6 +186,27 @@ describe('<TableFilter />', function() {
     assert.strictEqual(actualResult.length, 13);
   });
 
+  it('should render a filter dialog with custom footer when customFooter is provided', () => {
+    const CustomFooter = () => <div id="custom-footer">customFooter</div>;
+    const options = { textLabels };
+    const filterList = [[], [], [], []];
+    const onFilterUpdate = spy();
+
+    const shallowWrapper = shallow(
+      <TableFilter
+        customFooter={CustomFooter}
+        columns={columns}
+        onFilterUpdate={onFilterUpdate}
+        filterData={filterData}
+        filterList={filterList}
+        options={options}
+      />,
+    ).dive();
+
+    const actualResult = shallowWrapper.find('#custom-footer');
+    assert.strictEqual(actualResult.length, 1);
+  });
+
   it('should trigger onFilterUpdate prop callback when calling method handleCheckboxChange', () => {
     const options = { filterType: 'checkbox', textLabels };
     const filterList = [[], [], [], []];
diff --git a/test/MUIDataTableToolbar.test.js b/test/MUIDataTableToolbar.test.js
index 32405ed..9e5a76f 100644
--- a/test/MUIDataTableToolbar.test.js
+++ b/test/MUIDataTableToolbar.test.js
@@ -79,6 +79,16 @@ describe('<TableToolbar />', function() {
     assert.strictEqual(actualResult.props().options.searchText, 'searchText');
   });
 
+  it('should render a toolbar with search if option.searchOpen = true', () => {
+    const newOptions = { ...options, searchOpen: true };
+    const mountWrapper = mount(
+      <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
+    );
+    const actualResult = mountWrapper.find(TableSearch);
+    assert.strictEqual(actualResult.length, 1);
+    assert.strictEqual(actualResult.props().options.searchText, undefined);
+  });
+
   it('should render a toolbar with no search icon if option.search = false', () => {
     const newOptions = { ...options, search: false };
     const mountWrapper = mount(
@@ -182,6 +192,36 @@ describe('<TableToolbar />', function() {
     assert.strictEqual(actualResult.length, 0);
   });
 
+  it('should call onFilterDialogOpen when opening filters via toolbar', () => {
+    const onFilterDialogOpen = spy();
+    const newOptions = { ...options, onFilterDialogOpen };
+    const shallowWrapper = shallow(
+      <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
+    ).dive();
+    const instance = shallowWrapper.instance();
+
+    instance.setActiveIcon('filter');
+    shallowWrapper.update();
+
+    assert.strictEqual(onFilterDialogOpen.callCount, 1);
+  });
+
+  it('should call onFilterDialogClose when closing filters dialog', () => {
+    const onFilterDialogClose = spy();
+    const newOptions = { ...options, onFilterDialogClose };
+    const shallowWrapper = shallow(
+      <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
+    ).dive();
+    const instance = shallowWrapper.instance();
+
+    instance.setActiveIcon('filter');
+    shallowWrapper.update();
+    instance.setActiveIcon(undefined);
+    shallowWrapper.update();
+
+    assert.strictEqual(onFilterDialogClose.callCount, 1);
+  });
+
   it('should set icon when calling method setActiveIcon', () => {
     const shallowWrapper = shallow(
       <TableToolbar columns={columns} data={data} options={options} setTableAction={setTableAction} />,