Repository: gregnb/mui-datatables (js)

Issue:
Uncaught TypeError when clicking on "Download CSV" with no data in the table
<!--- Provide a general summary of the issue in the Title above -->
Clicking on "Download CSV" when the table is empty throws `TypeError`.

<!--
    Thank you very much for contributing to MUI-datatables by creating an issue! ❤️
-->

## Expected Behavior
<!---
    If you're describing a bug, tell us what should happen.
    If you're suggesting a change/improvement, tell us how it should work.
-->
If rows are empty, don't try to build the full CSV body.
The button is disabled, or download empty CSV.

## Current Behavior
<!---
    If describing a bug, tell us what happens instead of the expected behavior.
    If suggesting a change/improvement, explain the difference from current behavior.
-->
Errors are displayed in console and execution doesn't reach `onDownload` custom callback.
```javascript
utils.js?2b4c:51 Uncaught TypeError: data.reduce(...).trim is not a function
    at buildBody (utils.js?2b4c:51)
    at createCSVDownload (utils.js?2b4c:63)
    at TableToolbar._this.handleCSVDownload (TableToolbar.js?9399:125)
    at HTMLUnknownElement.callCallback (react-dom.development.js?cada:149)
    at Object.invokeGuardedCallbackDev (react-dom.development.js?cada:199)
    at invokeGuardedCallback (react-dom.development.js?cada:256)
    at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js?cada:270)
    at executeDispatch (react-dom.development.js?cada:561)
    at executeDispatchesInOrder (react-dom.development.js?cada:583)
    at executeDispatchesAndRelease (react-dom.development.js?cada:680)
    at executeDispatchesAndReleaseTopLevel (react-dom.development.js?cada:688)
    at forEachAccumulated (react-dom.development.js?cada:662)
    at runEventsInBatch (react-dom.development.js?cada:816)
    at runExtractedEventsInBatch (react-dom.development.js?cada:824)
    at handleTopLevel (react-dom.development.js?cada:4820)
    at batchedUpdates$1 (react-dom.development.js?cada:18932)
    at batchedUpdates (react-dom.development.js?cada:2150)
    at dispatchEvent (react-dom.development.js?cada:4899)
    at interactiveUpdates$1 (react-dom.development.js?cada:18987)
    at interactiveUpdates (react-dom.development.js?cada:2169)
    at dispatchInteractiveEvent (react-dom.development.js?cada:4876)
buildBody @ utils.js?2b4c:51
createCSVDownload @ utils.js?2b4c:63
TableToolbar._this.handleCSVDownload @ TableToolbar.js?9399:125
callCallback @ react-dom.development.js?cada:149
invokeGuardedCallbackDev @ react-dom.development.js?cada:199
invokeGuardedCallback @ react-dom.development.js?cada:256
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js?cada:270
executeDispatch @ react-dom.development.js?cada:561
executeDispatchesInOrder @ react-dom.development.js?cada:583
executeDispatchesAndRelease @ react-dom.development.js?cada:680
executeDispatchesAndReleaseTopLevel @ react-dom.development.js?cada:688
forEachAccumulated @ react-dom.development.js?cada:662
runEventsInBatch @ react-dom.development.js?cada:816
runExtractedEventsInBatch @ react-dom.development.js?cada:824
handleTopLevel @ react-dom.development.js?cada:4820
batchedUpdates$1 @ react-dom.development.js?cada:18932
batchedUpdates @ react-dom.development.js?cada:2150
dispatchEvent @ react-dom.development.js?cada:4899
interactiveUpdates$1 @ react-dom.development.js?cada:18987
interactiveUpdates @ react-dom.development.js?cada:2169
dispatchInteractiveEvent @ react-dom.development.js?cada:4876
15:14:28.095 react-dom.development.js?cada:289 Uncaught TypeError: data.reduce(...).trim is not a function
    at buildBody (utils.js?2b4c:51)
    at createCSVDownload (utils.js?2b4c:63)
    at TableToolbar._this.handleCSVDownload (TableToolbar.js?9399:125)
    at HTMLUnknownElement.callCallback (react-dom.development.js?cada:149)
    at Object.invokeGuardedCallbackDev (react-dom.development.js?cada:199)
    at invokeGuardedCallback (react-dom.development.js?cada:256)
    at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js?cada:270)
    at executeDispatch (react-dom.development.js?cada:561)
    at executeDispatchesInOrder (react-dom.development.js?cada:583)
    at executeDispatchesAndRelease (react-dom.development.js?cada:680)
    at executeDispatchesAndReleaseTopLevel (react-dom.development.js?cada:688)
    at forEachAccumulated (react-dom.development.js?cada:662)
    at runEventsInBatch (react-dom.development.js?cada:816)
    at runExtractedEventsInBatch (react-dom.development.js?cada:824)
    at handleTopLevel (react-dom.development.js?cada:4820)
    at batchedUpdates$1 (react-dom.development.js?cada:18932)
    at batchedUpdates (react-dom.development.js?cada:2150)
    at dispatchEvent (react-dom.development.js?cada:4899)
    at interactiveUpdates$1 (react-dom.development.js?cada:18987)
    at interactiveUpdates (react-dom.development.js?cada:2169)
    at dispatchInteractiveEvent (react-dom.development.js?cada:4876)
```
![image](https://user-images.githubusercontent.com/24522620/65883242-eea68200-e396-11e9-9437-482cb5e09889.png)

## Steps to Reproduce (for bugs)
<!---
    Provide a link to a live example (you can use codesandbox.io) and an unambiguous set of steps to reproduce this bug.
    Include code to reproduce, if relevant (which it most likely is).
-->
[codesandbox](https://codesandbox.io/s/muidatatables-download-empty-csv-jtctl)
1. Click on "Download CSV" Icon

## Your Environment
<!--- Include as many relevant details about the environment with which you experienced the bug. -->

| Tech         | Version |
|--------------|---------|
| Material-UI  |   4.3.1      |
| MUI-datatables  |     2.10.2    |
| React        |   16.8.6      |

Pull request:
Fix crash on downloading empty table as CSV
Fixes #967 
Similar to #929 without refactoring.

Patch:
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index ecae4a8..12c4ad2 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -17,7 +17,7 @@ import TableResize from './components/TableResize';
 import TableToolbar from './components/TableToolbar';
 import TableToolbarSelect from './components/TableToolbarSelect';
 import textLabels from './textLabels';
-import { buildMap, getCollatorComparator, sortCompare } from './utils';
+import { buildMap, getCollatorComparator, sortCompare, getPageValue } from './utils';
 
 const defaultTableStyles = theme => ({
   root: {},
@@ -886,20 +886,16 @@ class MUIDataTable extends React.Component {
   };
 
   changeRowsPerPage = rows => {
-    /**
-     * After changing rows per page recalculate totalPages and checks its if current page not higher.
-     * Otherwise sets current page the value of nextTotalPages
-     */
     const rowCount = this.options.count || this.state.displayData.length;
-    const nextTotalPages = Math.floor(rowCount / rows);
 
     this.setState(
       () => ({
         rowsPerPage: rows,
-        page: this.state.page > nextTotalPages ? nextTotalPages : this.state.page,
+        page: getPageValue(rowCount, rows, this.state.page),
       }),
       () => {
         this.setTableAction('changeRowsPerPage');
+
         if (this.options.onChangeRowsPerPage) {
           this.options.onChangeRowsPerPage(this.state.rowsPerPage);
         }
diff --git a/src/components/TableBody.js b/src/components/TableBody.js
index a8ed200..a55558e 100644
--- a/src/components/TableBody.js
+++ b/src/components/TableBody.js
@@ -7,6 +7,7 @@ import TableBodyRow from './TableBodyRow';
 import TableSelectCell from './TableSelectCell';
 import { withStyles } from '@material-ui/core/styles';
 import cloneDeep from 'lodash.clonedeep';
+import { getPageValue } from '../utils';
 
 const defaultBodyStyles = {
   root: {},
@@ -55,12 +56,12 @@ class TableBody extends React.Component {
     if (this.props.options.serverSide) return data.length ? data : null;
 
     let rows = [];
-    const totalPages = Math.floor(count / rowsPerPage);
-    const fromIndex = page === 0 ? 0 : page * rowsPerPage;
-    const toIndex = Math.min(count, (page + 1) * rowsPerPage);
+    const highestPageInRange = getPageValue(count, rowsPerPage, page);
+    const fromIndex = highestPageInRange === 0 ? 0 : highestPageInRange * rowsPerPage;
+    const toIndex = Math.min(count, (highestPageInRange + 1) * rowsPerPage);
 
-    if (page > totalPages && totalPages !== 0) {
-      console.warn('Current page is out of range.');
+    if (page > highestPageInRange) {
+      console.warn('Current page is out of range, using the highest page that is in range instead.');
     }
 
     for (let rowIndex = fromIndex; rowIndex < count && rowIndex < toIndex; rowIndex++) {
diff --git a/src/components/TablePagination.js b/src/components/TablePagination.js
index cc20ea1..e091164 100644
--- a/src/components/TablePagination.js
+++ b/src/components/TablePagination.js
@@ -4,6 +4,7 @@ import MuiTableRow from '@material-ui/core/TableRow';
 import MuiTableFooter from '@material-ui/core/TableFooter';
 import MuiTablePagination from '@material-ui/core/TablePagination';
 import { withStyles } from '@material-ui/core/styles';
+import { getPageValue } from '../utils';
 
 const defaultPaginationStyles = {
   root: {
@@ -63,7 +64,7 @@ class TablePagination extends React.Component {
             }}
             count={count}
             rowsPerPage={rowsPerPage}
-            page={page}
+            page={getPageValue(count, rowsPerPage, page)}
             labelRowsPerPage={textLabels.rowsPerPage}
             labelDisplayedRows={({ from, to, count }) => `${from}-${to} ${textLabels.displayRows} ${count}`}
             backIconButtonProps={{
diff --git a/src/utils.js b/src/utils.js
index e376eb1..0e8fcb6 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -5,6 +5,12 @@ function buildMap(rows) {
   }, {});
 }
 
+function getPageValue(count, rowsPerPage, page) {
+  const totalPages = count === rowsPerPage ? 0 : Math.floor(count / rowsPerPage);
+
+  return page > totalPages ? totalPages : page;
+}
+
 function getCollatorComparator() {
   if (!!Intl) {
     const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
@@ -46,6 +52,7 @@ function createCSVDownload(columns, data, options) {
   const CSVHead = buildHead(columns);
 
   const buildBody = data => {
+    if (!data.length) return '';
     return data
       .reduce(
         (soFar, row) =>
@@ -90,4 +97,4 @@ function createCSVDownload(columns, data, options) {
   }
 }
 
-export { buildMap, getCollatorComparator, sortCompare, createCSVDownload };
+export { buildMap, getPageValue, getCollatorComparator, sortCompare, createCSVDownload };


Test patch:
diff --git a/test/MUIDataTablePagination.test.js b/test/MUIDataTablePagination.test.js
index a5a6e9b..71719c1 100644
--- a/test/MUIDataTablePagination.test.js
+++ b/test/MUIDataTablePagination.test.js
@@ -46,4 +46,13 @@ describe('<TablePagination />', function() {
     instance.handlePageChange(null, 1);
     assert.strictEqual(changePage.callCount, 1);
   });
+
+  it('should correctly change page to be in bounds if out of bounds page was set', () => {
+    // Set a page that is too high for the count and rowsPerPage
+    const mountWrapper = mount(<TablePagination options={options} count={5} page={1} rowsPerPage={10} />);
+    const actualResult = mountWrapper.find(MuiTablePagination).props().page;
+
+    // material-ui v3 does some internal calculations to protect against out of bounds pages, but material v4 does not
+    assert.strictEqual(actualResult, 0);
+  });
 });