Repository: gregnb/mui-datatables
Pull request #838 (2019-08-15 10:28:55)

List downloadOptions keys in docs for usability
I believe users would have an easier time using some customization like `downloadOptions` if the allowed properties were listed in the documentation. If I want to customize how the CSV file will be output using `downloadOptions` but I only know about the default properties `filename` and `separator`, I will have no idea about other possible customizations enabled with `filterOptions` (like `useDisplayedColumnsOnly` and `useDisplayedRowsOnly`).

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

Coverage remained the same at 75.347% when pulling **9f4d4fca27b88e857a73e08aaa4c59084ce5f397 on Beamanator:update-download-options-docs** into **8438fe3a06b3bb570910d5c445c33b4501de3d6e on gregnb:master**.


Patch:
diff --git a/README.md b/README.md
index fc29dce..f3a7aa9 100644
--- a/README.md
+++ b/README.md
@@ -142,14 +142,14 @@ The component accepts the following props:
 |**`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) => bool`
+|**`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})) => bool`.
 |**`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.
 |**`renderExpandableRow`**|function||Render expandable row. `function(rowData, rowMeta) => React Component`
 |**`resizableColumns`**|boolean|false|Enable/disable resizable columns
 |**`customToolbar`**|function||Render a custom toolbar
 |**`customToolbarSelect`**|function||Render a custom selected rows toolbar. `function(selectedRows, displayData, setSelectedRows) => void`
-|**`customFooter`**|function||Render a custom table footer. `function(count, page, rowsPerPage, changeRowsPerPage, changePage) => string`&#124;` React Component`
+|**`customFooter`**|function||Render a custom table footer. `function(count, page, rowsPerPage, changeRowsPerPage, changePage, `[`textLabels: object`](https://github.com/gregnb/mui-datatables/blob/master/src/textLabels.js)`) => string`&#124;` React Component` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/customize-footer/index.js)
 |**`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`
@@ -158,7 +158,7 @@ The component accepts the following props:
 |**`caseSensitive `**|boolean|false|Enable/disable case sensitivity for search
 |**`responsive`**|string|'stacked'|Enable/disable responsive table views. Options: 'stacked', 'scroll'
 |**`rowsPerPage`**|number|10|Number of rows allowed per page
-|**`rowsPerPageOptions`**|array|[10,15,20]|Options to provide in pagination for number of rows a user can select
+|**`rowsPerPageOptions`**|array|[10,15,100]|Options to provide in pagination for number of rows a user can select
 |**`rowHover`**|boolean|true|Enable/disable hover style over rows
 |**`fixedHeader`**|boolean|true|Enable/disable fixed header columns
 |**`sortFilterList`**|boolean|true|Enable/disable alphanumeric sorting of filter lists
@@ -168,7 +168,7 @@ The component accepts the following props:
 |**`searchText`**|string||Initial search text
 |**`print`**|boolean|true|Show/hide print	 icon from toolbar
 |**`download`**|boolean|true|Show/hide download icon from toolbar
-|**`downloadOptions`**|object||Options to change the output of the CSV file. Default options: `{filename: 'tableDownload.csv', separator: ','}`
+|**`downloadOptions`**|object|`{filename: 'tableDownload.csv', separator: ','}`|Options to change the output of the CSV file: `filename`: string, `separator`: string, `filterOptions`: object(`useDisplayedColumnsOnly`: boolean, `useDisplayedRowsOnly`: boolean)
 |**`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`
diff --git a/examples/customize-footer/CustomFooter.js b/examples/customize-footer/CustomFooter.js
index b404927..c30edca 100644
--- a/examples/customize-footer/CustomFooter.js
+++ b/examples/customize-footer/CustomFooter.js
@@ -2,6 +2,7 @@ import React from "react";
 import TableFooter from "@material-ui/core/TableFooter";
 import TableRow from "@material-ui/core/TableRow";
 import TableCell from "@material-ui/core/TableCell";
+import MuiTablePagination from "@material-ui/core/TablePagination";
 import { withStyles } from "@material-ui/core/styles";
 
 const defaultFooterStyles = {
@@ -9,20 +10,52 @@ const defaultFooterStyles = {
 
 class CustomFooter extends React.Component {
 
+  handleRowChange = event => {
+    this.props.changeRowsPerPage(event.target.value);
+  };
+
+  handlePageChange = (_, page) => {
+    this.props.changePage(page);
+  };
+
   render() {
-    const { classes } = this.props;
+    const { count, classes, textLabels, rowsPerPage, page } = this.props;
+
+    const footerStyle = {
+      display:'flex', 
+      justifyContent: 'flex-end',
+      padding: '0px 24px 0px 24px'
+    };
 
     return (
-    <TableFooter>
-      <TableRow>
-        <TableCell>
-          some content
-        </TableCell>
-      </TableRow>
-    </TableFooter>
+      <TableFooter>
+        <TableRow>
+          <TableCell style={footerStyle} colSpan={1000}>
+            <button>Custom Option</button>
+          
+            <MuiTablePagination
+              component="div"
+              count={count}
+              rowsPerPage={rowsPerPage}
+              page={page}
+              labelRowsPerPage={textLabels.rowsPerPage}
+              labelDisplayedRows={({ from, to, count }) => `${from}-${to} ${textLabels.displayRows} ${count}`}
+              backIconButtonProps={{
+                'aria-label': textLabels.previous,
+              }}
+              nextIconButtonProps={{
+                'aria-label': textLabels.next,
+              }}
+              rowsPerPageOptions={[10,20,100]}
+              onChangePage={this.handlePageChange}
+              onChangeRowsPerPage={this.handleRowChange}
+            />
+          </TableCell>
+        </TableRow>
+      </TableFooter>
     );
   }
 
 }
 
-export default withStyles(defaultFooterStyles, { name: "CustomFooter" })(CustomFooter);
+export default withStyles(defaultFooterStyles, { name: "CustomFooter" })(CustomFooter);
\ No newline at end of file
diff --git a/examples/customize-footer/index.js b/examples/customize-footer/index.js
index db7e7bf..0ee9de4 100644
--- a/examples/customize-footer/index.js
+++ b/examples/customize-footer/index.js
@@ -44,13 +44,18 @@ class Example extends React.Component {
 
     const options = {
       filter: true,
-      selectableRows: true,
       filterType: 'dropdown',
       responsive: 'stacked',
       rowsPerPage: 10,
-      customFooter: (count, page, rowsPerPage, changeRowsPerPage, changePage) => {
+      customFooter: (count, page, rowsPerPage, changeRowsPerPage, changePage, textLabels) => {
         return (  
-          <CustomFooter changePage={changePage} count={count} />
+          <CustomFooter 
+            count={count} 
+            page={page} 
+            rowsPerPage={rowsPerPage} 
+            changeRowsPerPage={changeRowsPerPage} 
+            changePage={changePage} 
+            textLabels={textLabels} />
         );
       }
     };
@@ -62,4 +67,4 @@ class Example extends React.Component {
   }
 }
 
-ReactDOM.render(<Example />, document.getElementById("app-root"));
+ReactDOM.render(<Example />, document.getElementById("app-root"));
\ No newline at end of file
diff --git a/examples/selectable-rows/index.js b/examples/selectable-rows/index.js
index 4323549..139e803 100644
--- a/examples/selectable-rows/index.js
+++ b/examples/selectable-rows/index.js
@@ -90,7 +90,9 @@ class Example extends React.Component {
       onRowClick: (rowData, rowState) => {
         console.log(rowData, rowState);
       },
-      isRowSelectable: (dataIndex) => {
+      isRowSelectable: (dataIndex, selectedRows) => {
+        //prevents selection of any additional row after the third
+        if (selectedRows.data.length > 2 && selectedRows.data.filter(d => d.dataIndex === dataIndex).length === 0) return false;
         //prevents selection of row with title "Attorney"
         return data[dataIndex][1] != "Attorney";
       }
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index 73b2d5a..11d7dd2 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -295,6 +295,9 @@ class MUIDataTable extends React.Component {
     if (props.options.rowsPerPageOptions) {
       this.options.rowsPerPageOptions = props.options.rowsPerPageOptions;
     }
+    if (['scroll', 'stacked'].indexOf(this.options.responsive) === -1) {
+      console.error('Invalid option value for responsive. Please use string option: stacked | scroll');
+    }
   }
 
   validateOptions(options) {
@@ -1023,7 +1026,7 @@ class MUIDataTable extends React.Component {
       const { isRowSelectable } = this.options;
       this.setState(
         prevState => {
-          const { displayData } = prevState;
+          const { displayData, selectedRows: prevSelectedRows } = prevState;
           const selectedRowsLen = prevState.selectedRows.data.length;
           const isDeselect =
             selectedRowsLen === displayData.length || (selectedRowsLen < displayData.length && selectedRowsLen > 0)
@@ -1031,7 +1034,7 @@ class MUIDataTable extends React.Component {
               : false;
 
           let selectedRows = displayData.reduce((arr, d, i) => {
-            const selected = isRowSelectable ? isRowSelectable(displayData[i].dataIndex) : true;
+            const selected = isRowSelectable ? isRowSelectable(displayData[i].dataIndex, prevSelectedRows) : true;
             selected && arr.push({ index: i, dataIndex: displayData[i].dataIndex });
             return arr;
           }, []);
diff --git a/src/components/TableBody.js b/src/components/TableBody.js
index 8207b4f..dedb641 100644
--- a/src/components/TableBody.js
+++ b/src/components/TableBody.js
@@ -87,11 +87,12 @@ class TableBody extends React.Component {
   }
 
   isRowSelectable(dataIndex) {
-    const { options } = this.props;
+    const { options, selectedRows } = this.props;
     if (options.isRowSelectable) {
-      return options.isRowSelectable(dataIndex);
+      return options.isRowSelectable(dataIndex, selectedRows);
+    } else {
+      return true;
     }
-    return true;
   }
 
   handleRowSelect = data => {
diff --git a/src/components/TableFooter.js b/src/components/TableFooter.js
index c5c33a7..56ff689 100644
--- a/src/components/TableFooter.js
+++ b/src/components/TableFooter.js
@@ -16,7 +16,7 @@ class TableFooter extends React.Component {
     return (
       <MuiTable>
         {options.customFooter
-          ? options.customFooter(rowCount, page, rowsPerPage, changeRowsPerPage, changePage)
+          ? options.customFooter(rowCount, page, rowsPerPage, changeRowsPerPage, changePage, options.textLabels.pagination)
           : options.pagination && (
               <TablePagination
                 count={rowCount}
diff --git a/src/components/TablePagination.js b/src/components/TablePagination.js
index fcc6768..cc20ea1 100644
--- a/src/components/TablePagination.js
+++ b/src/components/TablePagination.js
@@ -44,7 +44,6 @@ class TablePagination extends React.Component {
   };
 
   handlePageChange = (_, page) => {
-    const { options } = this.props;
     this.props.changePage(page);
   };
 
diff --git a/src/components/TableToolbar.js b/src/components/TableToolbar.js
index 41bc119..c25125c 100644
--- a/src/components/TableToolbar.js
+++ b/src/components/TableToolbar.js
@@ -13,10 +13,10 @@ import PrintIcon from '@material-ui/icons/Print';
 import ViewColumnIcon from '@material-ui/icons/ViewColumn';
 import FilterIcon from '@material-ui/icons/FilterList';
 import ReactToPrint from 'react-to-print';
-import styled from '../styled';
+import { withStyles } from '@material-ui/core/styles';
 import { createCSVDownload } from '../utils';
 
-export const defaultToolbarStyles = (theme, props) => ({
+export const defaultToolbarStyles = theme => ({
   root: {},
   left: {
     flex: '1 1 auto',
@@ -43,10 +43,6 @@ export const defaultToolbarStyles = (theme, props) => ({
     marginTop: '10px',
     marginRight: '8px',
   },
-  ...(props.options.responsive ? { ...responsiveToolbarStyles(theme) } : {}),
-});
-
-export const responsiveToolbarStyles = theme => ({
   [theme.breakpoints.down('sm')]: {
     titleRoot: {},
     titleText: {
@@ -315,4 +311,4 @@ class TableToolbar extends React.Component {
   }
 }
 
-export default styled(TableToolbar)(defaultToolbarStyles, { name: 'MUIDataTableToolbar' });
+export default withStyles(defaultToolbarStyles, { name: 'MUIDataTableToolbar' })(TableToolbar);
diff --git a/src/styled.js b/src/styled.js
deleted file mode 100644
index 543db2f..0000000
--- a/src/styled.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import merge from 'lodash.merge';
-import { withStyles } from '@material-ui/core/styles';
-
-/*
- *  Material-UI does not yet support ability to grab props within style()
- *  https://github.com/mui-org/material-ui/issues/7633
- *
- *  This is a workaround provided from the thread
- */
-
-const styles = (theme, props, style) => {
-  return typeof style === 'function' ? style(theme, props) : style;
-};
-
-class StyledComponent extends React.Component {
-  static propTypes = {
-    classes: PropTypes.object.isRequired,
-    className: PropTypes.string,
-  };
-
-  render() {
-    const { classes, className = '', WrappedComponent, ...passThroughProps } = this.props;
-
-    return <WrappedComponent classes={classes} className={className} {...passThroughProps} />;
-  }
-}
-
-const styled = (WrappedComponent, customProps = {}) => {
-  return (style, options = {}) => {
-    const HOCProps = WrappedComponent => {
-      return class _HOCProps extends React.Component {
-        constructor(props) {
-          super(props);
-          this.FinalComponent = withStyles(theme => {
-            const defaultStyles = styles(theme, props, style);
-            const mergedStyles = merge(defaultStyles, props.styles ? props.styles : {});
-            return mergedStyles;
-          }, options)(StyledComponent);
-        }
-
-        render() {
-          const { styles, ...otherProps } = this.props;
-          return <this.FinalComponent {...customProps} {...otherProps} WrappedComponent={WrappedComponent} />;
-        }
-      };
-    };
-    return HOCProps(WrappedComponent);
-  };
-};
-
-export default styled;


Test patch:
diff --git a/test/MUIDataTableBody.test.js b/test/MUIDataTableBody.test.js
index 1fb866e..bb4613f 100644
--- a/test/MUIDataTableBody.test.js
+++ b/test/MUIDataTableBody.test.js
@@ -235,14 +235,14 @@ describe('<TableBody />', function() {
     assert.strictEqual(toggleExpandRow.callCount, 0);
   });
 
-  it('should not gather selected row data when clicking row with selectableRowsOnClick=true when it is disabled with isRowSelectable.', () => {
+  it('should not gather selected row data when clicking row with selectableRowsOnClick=true when it is disabled with isRowSelectable via index.', () => {
     let selectedRowData;
     const options = {
       selectableRows: true,
       selectableRowsOnClick: true,
       isRowSelectable: dataIndex => (dataIndex === 2 ? false : true),
     };
-    const selectRowUpdate = (type, data) => (selectedRowData = data);
+    const selectRowUpdate = (_, data) => (selectedRowData = data);
     const toggleExpandRow = spy();
 
     const mountWrapper = mount(
@@ -271,6 +271,86 @@ describe('<TableBody />', function() {
     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,
+    };
+    const selectRowUpdate = (_, data) => (selectedRowData = data);
+    const toggleExpandRow = spy();
+    const initialSelectedRows = {
+      data: [{ index: 1, dataIndex: 1 }],
+      lookup: { 1: true },
+    };
+
+    const mountWrapper = mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        rowsPerPage={10}
+        selectedRows={initialSelectedRows}
+        selectRowUpdate={selectRowUpdate}
+        expandedRows={[]}
+        toggleExpandRow={toggleExpandRow}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-2')
+      .first()
+      .simulate('click');
+
+    assert.isUndefined(selectedRowData);
+    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.', () => {
+    let selectedRowData;
+    const options = {
+      selectableRows: true,
+      selectableRowsOnClick: true,
+      isRowSelectable: (index, selectedRows) => selectedRows.lookup[index] || selectedRows.data.length < 1,
+    };
+    const selectRowUpdate = (_, data) => (selectedRowData = data);
+    const toggleExpandRow = spy();
+    const initialSelectedRows = {
+      data: [{ index: 1, dataIndex: 1 }],
+      lookup: { 1: true },
+    };
+
+    const mountWrapper = mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        rowsPerPage={10}
+        selectedRows={initialSelectedRows}
+        selectRowUpdate={selectRowUpdate}
+        expandedRows={[]}
+        toggleExpandRow={toggleExpandRow}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    mountWrapper
+      .find('#MUIDataTableBodyRow-1')
+      .first()
+      .simulate('click');
+
+    assert.isDefined(selectedRowData);
+    assert.strictEqual(toggleExpandRow.callCount, 0);
+  });
+
   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 };
@@ -493,4 +573,35 @@ describe('<TableBody />', function() {
 
     expect(html).to.contain('Test_Text');
   });
+
+  it('should pass in selectedRows to isRowSelectable', () => {
+    const selectedIndex = 2;
+    const originalSelectedRows = {
+      data: [{ index: selectedIndex, dataIndex: selectedIndex }],
+      lookup: { [selectedIndex]: true },
+    };
+    const isRowSelectable = spy((_, selectedRows) => {
+      assert.deepEqual(selectedRows, originalSelectedRows);
+      return true;
+    });
+
+    const options = { selectableRows: true, isRowSelectable };
+
+    mount(
+      <TableBody
+        data={displayData}
+        count={displayData.length}
+        columns={columns}
+        page={0}
+        selectedRows={originalSelectedRows}
+        rowsPerPage={10}
+        expandedRows={[]}
+        options={options}
+        searchText={''}
+        filterList={[]}
+      />,
+    );
+
+    assert.equal(isRowSelectable.callCount, displayData.length);
+  });
 });
diff --git a/test/MUIDataTableToolbar.test.js b/test/MUIDataTableToolbar.test.js
index dd00ccf..f44a294 100644
--- a/test/MUIDataTableToolbar.test.js
+++ b/test/MUIDataTableToolbar.test.js
@@ -130,8 +130,6 @@ describe('<TableToolbar />', function() {
     const shallowWrapper = shallow(
       <TableToolbar columns={columns} data={data} options={newOptions} setTableAction={setTableAction} />,
     )
-      .dive()
-      .dive()
       .dive();
     const instance = shallowWrapper.instance();
     instance.setActiveIcon('search');
@@ -144,8 +142,6 @@ describe('<TableToolbar />', function() {
     const shallowWrapper = shallow(
       <TableToolbar columns={columns} data={data} options={options} setTableAction={setTableAction} />,
     )
-      .dive()
-      .dive()
       .dive();
     const instance = shallowWrapper.instance();
 
@@ -167,8 +163,6 @@ describe('<TableToolbar />', function() {
         setTableAction={setTableAction}
       />,
     )
-      .dive()
-      .dive()
       .dive();
     const instance = shallowWrapper.instance();
 
@@ -195,8 +189,6 @@ describe('<TableToolbar />', function() {
     const shallowWrapper = shallow(
       <TableToolbar columns={columns} data={data} options={options} setTableAction={setTableAction} />,
     )
-      .dive()
-      .dive()
       .dive();
     const instance = shallowWrapper.instance();
 
@@ -218,8 +210,6 @@ describe('<TableToolbar />', function() {
       />,
     );
     const instance = shallowWrapper
-      .dive()
-      .dive()
       .dive()
       .instance();
 
@@ -247,8 +237,6 @@ describe('<TableToolbar />', function() {
     );
 
     const instance = shallowWrapper
-      .dive()
-      .dive()
       .dive()
       .instance();