Repository: gregnb/mui-datatables
Pull request #802 (2019-08-01 06:05:31)

Enhancement/pr 667 updated
Continuation of https://github.com/gregnb/mui-datatables/pull/667.

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

Coverage remained the same at 75.972% when pulling **b4e8372836e2c96383ed6449363b48ec57d7e5e0 on enhancement/pr-667-updated** into **6e973d22c8d3b67c983ae57deb6cd8edc11b037d on master**.


Patch:
diff --git a/README.md b/README.md
index 4a3dea7..4bc6098 100644
--- a/README.md
+++ b/README.md
@@ -136,6 +136,7 @@ The component accepts the following props:
 |**`count`**|number||User provided override for total number of rows
 |**`serverSide`**|boolean|false|Enable remote data source
 |**`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
 |**`pagination`**|boolean|true|Enable/disable pagination
diff --git a/examples/csv-export/index.js b/examples/csv-export/index.js
index 6970dca..40d9b04 100644
--- a/examples/csv-export/index.js
+++ b/examples/csv-export/index.js
@@ -12,8 +12,18 @@ class Example extends React.Component {
     const columns = [
       'Name',
       'Title',
-      'Location',
-      'Age',
+      {
+        name: 'Location',
+        options: {
+          display: 'false'
+        }
+      },
+      {
+        name: 'Age',
+        options: {
+          customBodyRender: value => <div><span>{value}</span></div>
+        }
+      },
       {
         name: 'Salary',
         options: {
@@ -62,10 +72,20 @@ class Example extends React.Component {
       responsive: 'stacked',
       rowsPerPage: 10,
       downloadOptions: {
-        filename: 'excel-format.csv',
-        separator: ';',
+          filename: 'excel-format.csv',
+          separator: ';',
+          filterOptions: {
+            useDisplayedColumnsOnly: true,
+            useDisplayedRowsOnly: true,
+          }
+      },
+      onDownload: (buildHead, buildBody, columns, data) => {
+        if (this.state.downloadFile) {
+          return `${buildHead(columns)}${buildBody(data)}`.trim();
+        }
+
+        return false;
       },
-      onDownload: () => this.state.downloadFile,
       onRowsSelect: (rowsSelected, allRows) => {
         console.log(rowsSelected, allRows);
       },
diff --git a/examples/customize-filter/index.js b/examples/customize-filter/index.js
index db58014..a829180 100644
--- a/examples/customize-filter/index.js
+++ b/examples/customize-filter/index.js
@@ -32,25 +32,26 @@ class Example extends React.Component {
         options: {
           filter: true,
           filterType: 'custom',
+          filterList: [25, 50],
           customFilterListRender: v => {
-            if (v['min'] && v['max']) {
-              return `Min Age: ${v['min']}, Max Age: ${v['max']}`;
-            } else if (v['min']) {
-              return `Min Age: ${v['min']}`;
-            } else if (v['max']) {
-              return `Max Age: ${v['max']}`;
+            if (v[0] && v[1]) {
+              return `Min Age: ${v[0]}, Max Age: ${v[1]}`;
+            } else if (v[0]) {
+              return `Min Age: ${v[0]}`;
+            } else if (v[1]) {
+              return `Max Age: ${v[1]}`;
             }
             return false;
           },
           filterOptions: {
             names: [],
             logic(age, filters) {
-              if (filters['min'] && filters['max']) {
-                return age < filters['min'] || age > filters['max'];
-              } else if (filters['min']) {
-                return age < filters['min'];
-              } else if (filters['max']) {
-                return age > filters['max'];
+              if (filters[0] && filters[1]) {
+                return age < filters[0] || age > filters[1];
+              } else if (filters[0]) {
+                return age < filters[0];
+              } else if (filters[1]) {
+                return age > filters[1];
               }
               return false;
             },
@@ -60,18 +61,18 @@ class Example extends React.Component {
                 <FormGroup row>
                   <TextField
                     label="min"
-                    value={filterList[index]['min'] || ''}
+                    value={filterList[index][0] || ''}
                     onChange={event => {
-                      filterList[index]['min'] = event.target.value;
+                      filterList[index][0] = event.target.value;
                       onChange(filterList[index], index, column);
                     }}
                     style={{ width: '45%', marginRight: '5%' }}
                   />
                   <TextField
                     label="max"
-                    value={filterList[index]['max'] || ''}
+                    value={filterList[index][1] || ''}
                     onChange={event => {
-                      filterList[index]['max'] = event.target.value;
+                      filterList[index][1] = event.target.value;
                       onChange(filterList[index], index, column);
                     }}
                     style={{ width: '45%' }}
diff --git a/examples/expandable-rows/index.js b/examples/expandable-rows/index.js
index 5d3eba3..f541a54 100644
--- a/examples/expandable-rows/index.js
+++ b/examples/expandable-rows/index.js
@@ -82,6 +82,7 @@ class Example extends React.Component {
       responsive: 'scroll',
       expandableRows: true,
       expandableRowsOnClick: true,
+      rowsExpanded: [0, 2, 3],
       renderExpandableRow: (rowData, rowMeta) => {
         const colSpan = rowData.length + 1;
         return (
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index 817459a..13d7511 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -4,7 +4,6 @@ import MuiTable from '@material-ui/core/Table';
 import classnames from 'classnames';
 import cloneDeep from 'lodash.clonedeep';
 import find from 'lodash.find';
-import isEqual from 'lodash.isequal';
 import isUndefined from 'lodash.isundefined';
 import merge from 'lodash.merge';
 import PropTypes from 'prop-types';
@@ -142,6 +141,7 @@ class MUIDataTable extends React.Component {
       page: PropTypes.number,
       count: PropTypes.number,
       rowsSelected: PropTypes.array,
+      rowsExpanded: PropTypes.array,
       rowsPerPage: PropTypes.number,
       rowsPerPageOptions: PropTypes.array,
       filter: PropTypes.bool,
@@ -156,6 +156,10 @@ class MUIDataTable extends React.Component {
       downloadOptions: PropTypes.shape({
         filename: PropTypes.string,
         separator: PropTypes.string,
+        filterOptions: PropTypes.shape({
+          useDisplayedColumnsOnly: PropTypes.bool,
+          useDisplayedRowsOnly: PropTypes.bool
+        })
       }),
       onDownload: PropTypes.func,
     }),
@@ -487,8 +491,14 @@ class MUIDataTable extends React.Component {
       lookup: {},
     };
 
+    let expandedRowsData = {
+      data: [],
+      lookup: {},
+    };
+
     if (TABLE_LOAD.INITIAL) {
-      if (options.rowsSelected && options.rowsSelected.length) {
+      // Multiple row selection customization
+      if (options.rowsSelected && options.rowsSelected.length && options.selectableRows === 'multiple') {
         options.rowsSelected.forEach(row => {
           let rowPos = row;
 
@@ -503,6 +513,39 @@ class MUIDataTable extends React.Component {
           selectedRowsData.lookup[row] = true;
         });
       }
+
+      // Single row selection customization
+      if (options.rowsSelected && options.rowsSelected.length === 1 && options.selectableRows === 'single') {
+        let rowPos = options.rowsSelected[0];
+
+        for (let cIndex = 0; cIndex < this.state.displayData.length; cIndex++) {
+          if (this.state.displayData[cIndex].dataIndex === options.rowsSelected[0]) {
+            rowPos = cIndex;
+            break;
+          }
+        }
+
+        selectedRowsData.data.push({ index: rowPos, dataIndex: options.rowsSelected[0] });
+        selectedRowsData.lookup[options.rowsSelected[0]] = true;
+      } else if (options.rowsSelected && options.rowsSelected.length > 1 && options.selectableRows === 'single') {
+        console.error('Multiple values provided for selectableRows, but selectableRows set to "single". Either supply only a single value or use "multiple".');
+      }
+
+      if (options.rowsExpanded && options.rowsExpanded.length && options.expandableRows) {
+        options.rowsExpanded.forEach(row => {
+          let rowPos = row;
+
+          for (let cIndex = 0; cIndex < this.state.displayData.length; cIndex++) {
+            if (this.state.displayData[cIndex].dataIndex === row) {
+              rowPos = cIndex;
+              break;
+            }
+          }
+
+          expandedRowsData.data.push({ index: rowPos, dataIndex: row });
+          expandedRowsData.lookup[row] = true;
+        });
+      }
     }
 
     if (!options.serverSide && sortIndex !== null) {
@@ -517,6 +560,7 @@ class MUIDataTable extends React.Component {
         filterList: filterList,
         searchText: searchText,
         selectedRows: selectedRowsData,
+        expandedRows: expandedRowsData,
         count: options.count,
         data: tableData,
         displayData: this.getDisplayData(columns, tableData, filterList, searchText),
diff --git a/src/components/TableBody.js b/src/components/TableBody.js
index 6fe61a6..8207b4f 100644
--- a/src/components/TableBody.js
+++ b/src/components/TableBody.js
@@ -162,6 +162,7 @@ class TableBody extends React.Component {
                   options={options}
                   rowSelected={options.selectableRows !== 'none' ? this.isRowSelected(dataIndex) : false}
                   onClick={this.handleRowClick.bind(null, row, { rowIndex, dataIndex })}
+                  data-testid={'MUIDataTableBodyRow-' + dataIndex}
                   id={'MUIDataTableBodyRow-' + dataIndex}>
                   <TableSelectCell
                     onChange={this.handleRowSelect.bind(null, {
@@ -187,6 +188,7 @@ class TableBody extends React.Component {
                           {...(columns[columnIndex].setCellProps
                             ? columns[columnIndex].setCellProps(column, dataIndex, columnIndex)
                             : {})}
+                          data-testid={`MuiDataTableBodyCell-${columnIndex}-${rowIndex}`}
                           dataIndex={dataIndex}
                           rowIndex={rowIndex}
                           colIndex={columnIndex}
diff --git a/src/components/TableFilter.js b/src/components/TableFilter.js
index 154ba23..0ea4c2f 100644
--- a/src/components/TableFilter.js
+++ b/src/components/TableFilter.js
@@ -309,6 +309,7 @@ class TableFilter extends React.Component {
               className={classes.resetLink}
               tabIndex={0}
               aria-label={textLabels.reset}
+              data-testid={'filterReset-button'}
               onClick={onFilterReset}>
               {textLabels.reset}
             </Button>
diff --git a/src/components/TablePagination.js b/src/components/TablePagination.js
index caf6ef6..fcc6768 100644
--- a/src/components/TablePagination.js
+++ b/src/components/TablePagination.js
@@ -69,18 +69,21 @@ class TablePagination extends React.Component {
             labelDisplayedRows={({ from, to, count }) => `${from}-${to} ${textLabels.displayRows} ${count}`}
             backIconButtonProps={{
               id: 'pagination-back',
+              'data-testid': 'pagination-back',
               'aria-label': textLabels.previous,
             }}
             nextIconButtonProps={{
               id: 'pagination-next',
+              'data-testid': 'pagination-next',
               'aria-label': textLabels.next,
             }}
             SelectProps={{
               id: 'pagination-input',
-              SelectDisplayProps: { id: 'pagination-rows' },
+              SelectDisplayProps: { id: 'pagination-rows', 'data-testid': 'pagination-rows' },
               MenuProps: {
                 id: 'pagination-menu',
-                MenuListProps: { id: 'pagination-menu-list' },
+                'data-testid': 'pagination-menu',
+                MenuListProps: { id: 'pagination-menu-list', 'data-testid': 'pagination-menu-list' },
               },
             }}
             rowsPerPageOptions={options.rowsPerPageOptions}
diff --git a/src/components/TableSearch.js b/src/components/TableSearch.js
index 5cacbd6..9eec1d1 100644
--- a/src/components/TableSearch.js
+++ b/src/components/TableSearch.js
@@ -62,6 +62,7 @@ class TableSearch extends React.Component {
             className={classes.searchText}
             autoFocus={true}
             InputProps={{
+              'data-test-id': options.textLabels.toolbar.search,
               'aria-label': options.textLabels.toolbar.search,
             }}
             value={searchText || ''}
diff --git a/src/components/TableSelectCell.js b/src/components/TableSelectCell.js
index f22f328..6f501cb 100644
--- a/src/components/TableSelectCell.js
+++ b/src/components/TableSelectCell.js
@@ -32,11 +32,7 @@ const defaultSelectCellStyles = theme => ({
     zIndex: 110,
     backgroundColor: theme.palette.background.paper,
   },
-  checkboxRoot: {
-    '&$checked': {
-      color: theme.palette.primary.main,
-    },
-  },
+  checkboxRoot: {},
   checked: {},
   disabled: {},
 });
@@ -104,6 +100,7 @@ class TableSelectCell extends React.Component {
             checked: classes.checked,
             disabled: classes.disabled,
           }}
+          color="primary"
           disabled={!isRowSelectable}
           {...otherProps}
         />
diff --git a/src/components/TableToolbar.js b/src/components/TableToolbar.js
index 2cbd812..b594de5 100644
--- a/src/components/TableToolbar.js
+++ b/src/components/TableToolbar.js
@@ -95,8 +95,38 @@ class TableToolbar extends React.Component {
   }
 
   handleCSVDownload = () => {
-    const { data, columns, options } = this.props;
-    createCSVDownload(columns, data, options);
+    const { data, displayData, columns, options } = this.props;
+    let dataToDownload = data;
+    let columnsToDownload = columns;
+
+    if (options.downloadOptions && options.downloadOptions.filterOptions) {
+      // check rows first:
+      if (options.downloadOptions.filterOptions.useDisplayedRowsOnly) {
+        dataToDownload = displayData.map(row => {
+          let i = -1;
+
+          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;
+            })
+          };
+        });
+      }
+
+      // now, check columns:
+      if (options.downloadOptions.filterOptions.useDisplayedColumnsOnly) {
+        columnsToDownload = columns.filter((_, index) => _.display==='true');
+
+        dataToDownload = dataToDownload.map(row => {
+          row.data = row.data.filter((_, index) => columns[index].display==='true');
+          return row;
+        });
+      }
+    }
+    createCSVDownload(columnsToDownload, dataToDownload, options);
   };
 
   setActiveIcon = iconName => {
@@ -195,6 +225,7 @@ class TableToolbar extends React.Component {
             <Tooltip title={search} disableFocusListener>
               <IconButton
                 aria-label={search}
+                data-testid={search + '-iconButton'}
                 buttonRef={el => (this.searchButton = el)}
                 classes={{ root: this.getActiveIcon(classes, 'search') }}
                 onClick={this.setActiveIcon.bind(null, 'search')}>
@@ -204,7 +235,11 @@ class TableToolbar extends React.Component {
           )}
           {options.download && (
             <Tooltip title={downloadCsv}>
-              <IconButton aria-label={downloadCsv} classes={{ root: classes.icon }} onClick={this.handleCSVDownload}>
+              <IconButton
+                data-testid={downloadCsv + '-iconButton'}
+                aria-label={downloadCsv}
+                classes={{ root: classes.icon }}
+                onClick={this.handleCSVDownload}>
                 <DownloadIcon />
               </IconButton>
             </Tooltip>
@@ -214,7 +249,7 @@ class TableToolbar extends React.Component {
               <ReactToPrint
                 trigger={() => (
                   <Tooltip title={print}>
-                    <IconButton aria-label={print} classes={{ root: classes.icon }}>
+                    <IconButton data-testid={print + '-iconButton'} aria-label={print} classes={{ root: classes.icon }}>
                       <PrintIcon />
                     </IconButton>
                   </Tooltip>
@@ -229,6 +264,7 @@ class TableToolbar extends React.Component {
               trigger={
                 <Tooltip title={viewColumns} disableFocusListener>
                   <IconButton
+                    data-testid={viewColumns + '-iconButton'}
                     aria-label={viewColumns}
                     classes={{ root: this.getActiveIcon(classes, 'viewcolumns') }}
                     onClick={this.setActiveIcon.bind(null, 'viewcolumns')}>
@@ -248,6 +284,7 @@ class TableToolbar extends React.Component {
               trigger={
                 <Tooltip title={filterTable} disableFocusListener>
                   <IconButton
+                    data-testid={filterTable + '-iconButton'}
                     aria-label={filterTable}
                     classes={{ root: this.getActiveIcon(classes, 'filter') }}
                     onClick={this.setActiveIcon.bind(null, 'filter')}>


Test patch:
diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js
index f4a05f3..c2aa419 100644
--- a/test/MUIDataTable.test.js
+++ b/test/MUIDataTable.test.js
@@ -772,7 +772,7 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(state.selectedRows.data, [{ index: 1, dataIndex: 1 }]);
   });
 
-  it('If selectableRows=multiple, multiple cells can be selected when calling selectRowUpdate method with type=cell.', () => {
+  it('should allow multiple cells to be selected when selectableRows=multiple and selectRowUpdate method with type=cell.', () => {
     const options = { selectableRows: 'multiple' };
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} options={options} />).dive();
     const instance = shallowWrapper.instance();
@@ -799,6 +799,35 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(state.selectedRows.data, expectedResult);
   });
 
+  it('should update selectedRows when using rowsSelected option with type=multiple', () => {
+    const options = {
+      selectableRows: 'multiple',
+      rowsSelected: [0, 3]
+    };
+    const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} options={options} />).dive();
+    const instance = shallowWrapper.instance();
+
+    const state = shallowWrapper.state();
+    const expectedResult = [{ index: 0, dataIndex: 0 }, { index: 3, dataIndex: 3 }];
+
+    assert.deepEqual(state.selectedRows.data, expectedResult);
+  });
+
+  it('should update expandedRows when using expandableRows option with default rowsExpanded', () => {
+    const options = {
+      expandableRows: true,
+      rowsExpanded: [0, 3],
+      renderExpandableRow: () => <tr><td>opened</td></tr>
+    };
+    const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} options={options} />).dive();
+    const instance = shallowWrapper.instance();
+
+    const state = shallowWrapper.state();
+    const expectedResult = [{ index: 0, dataIndex: 0 }, { index: 3, dataIndex: 3 }];
+
+    assert.deepEqual(state.expandedRows.data, expectedResult);
+  });
+
   it('should remove selected data on selectRowDelete when type=cell', () => {
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />).dive();
     const instance = shallowWrapper.instance();