Repository: gregnb/mui-datatables
Pull request #587 (2019-05-01 18:45:48)

Enhancement/issue 193 424 skip row delete
A lot of people have been asking for the ability to prevent deletion of certain rows via the selection toolbar, so I thought I'd give it a shot. Should close https://github.com/gregnb/mui-datatables/issues/424 and https://github.com/gregnb/mui-datatables/issues/193 but possibly others as well.

The idea is to allow the `onRowsDelete` function to return `false`, which will then skip the deletion. I added some tests and sample functionality around this, as well as updating the docs accordingly.

Let me know what you think!

Also closes https://github.com/gregnb/mui-datatables/issues/557

UPDATE:

Also added documentation to the README around `onRowsDelete`, as it was out of date, but I'm open to suggestions on ways to make it more clear, since the object entering the function is complex.

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

Coverage increased (+1.02%) to 75.694% when pulling **46f5f17daca4068660cb499d832edd2540d59b9f on enhancement/issue-193-424-skip-row-delete** into **d6ec8a3b1da48bbefda60f9112a590942bc06bf5 on master**.

matteo1976: I think it would be a good feature if the onRowDelete function will return an array of objects that where the objects will contain all the data of the rows...(one object for each row deleted) with a simple check on the length of this array would be exactly like having the false ... and at the same time it could be useful to intercept the deleted data and use them for example to delete them in a remote db...
If there are the request to have a control for prevent deletion , I think, it would be better have a function like isRorwsDeleted but onRowDeleted is more like an event to menage.
this is my modest advice
gabrielliwerant: @matteo1976 My idea is to keep the functions as simple as they need to be for people to use them for their own purposes. In the case of the `onRowsDelete` callback, you actually should have all of the data you need to do whatever you want. The function allows you to look at the dataIndex of any selected row about to be deleted, which is enough to find those rows in your state and then do whatever you want to them.
gregnb: @gabrielliwerant Will review when I get some time. At quick glance we're changing the returning data structure right? if we merge should probably go with a minor bump for release

Patch:
diff --git a/README.md b/README.md
index 05aab14..818778b 100644
--- a/README.md
+++ b/README.md
@@ -165,7 +165,7 @@ The component accepts the following props:
 |**`downloadOptions`**|object||Options to change the output of the CSV file. Default options: `{filename: 'tableDownload.csv', separator: ','}`
 |**`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`
-|**`onRowsDelete`**|function||Callback function that triggers when row(s) are deleted. `function(rowsDeleted: array) => void`
+|**`onRowsDelete`**|function||Callback function that triggers when row(s) are deleted. `function(rowsDeleted: object(lookup: {dataindex: boolean}, data: arrayOfObjects: {index, dataIndex})) => 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`
 |**`onChangePage`**|function||Callback function that triggers when a page has changed. `function(currentPage: number) => void`
@@ -363,10 +363,22 @@ const options = {
 }
 ```
 
+## Contributing
+Thanks for taking an interest in the library and the github community!
+
+The following commands should get you started:
+
+```sh
+npm i
+npm run dev
+```
+open  http://localhost:5050/ in browser
+
+After you make your changes locally, you can run the test suite with `npm test`. 
+
 ## License
 The files included in this repository are licensed under the MIT license.
 
-
 ## Thanks
 
 [<img src="https://www.browserstack.com/images/mail/browserstack-logo-footer.png" width="120">](https://www.browserstack.com/)
diff --git a/examples/selectable-rows/index.js b/examples/selectable-rows/index.js
index a08b586..9860ba9 100644
--- a/examples/selectable-rows/index.js
+++ b/examples/selectable-rows/index.js
@@ -57,6 +57,10 @@ class Example extends React.Component {
         console.log(rowsSelected, allRows);
       },
       onRowsDelete: (rowsDeleted) => {
+        if (rowsDeleted.data[0].dataIndex === 0) {
+          window.alert('Can\'t delete this!');
+          return false;
+        };
         console.log(rowsDeleted, "were deleted!");
       },
       onChangePage: (numberRows) => {
diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js
index ba3dee8..ced5edc 100644
--- a/src/MUIDataTable.js
+++ b/src/MUIDataTable.js
@@ -190,7 +190,7 @@ class MUIDataTable extends React.Component {
 
   componentDidUpdate(prevProps) {
     if (this.props.data !== prevProps.data || this.props.columns !== prevProps.columns) {
-      this.setTableData(prevProps, TABLE_LOAD.INITIAL);
+      this.setTableData(this.props, TABLE_LOAD.INITIAL);
     }
 
     if (this.options.resizableColumns) {
@@ -831,6 +831,7 @@ class MUIDataTable extends React.Component {
     const cleanRows = data.filter(({ index }) => !selectedMap[index]);
 
     if (this.options.onRowsDelete) {
+      if (this.options.onRowsDelete(selectedRows) === false) return;
       this.options.onRowsDelete(selectedRows);
     }
 
diff --git a/src/components/TableToolbar.js b/src/components/TableToolbar.js
index 2e93d85..59c0829 100644
--- a/src/components/TableToolbar.js
+++ b/src/components/TableToolbar.js
@@ -199,31 +199,31 @@ class TableToolbar extends React.Component {
             </Tooltip>
           )}
           {options.print && (
-            <Tooltip title={print}>
-              <span>
-                <ReactToPrint
-                  trigger={() => (
+            <span>
+              <ReactToPrint
+                trigger={() => (
+                  <Tooltip title={print}>
                     <IconButton aria-label={print} classes={{ root: classes.icon }}>
                       <PrintIcon />
                     </IconButton>
-                  )}
-                  content={() => this.props.tableRef()}
-                />
-              </span>
-            </Tooltip>
+                  </Tooltip>
+                )}
+                content={() => this.props.tableRef()}
+              />
+            </span>
           )}
           {options.viewColumns && (
             <Popover
               refExit={this.setActiveIcon.bind(null)}
               trigger={
-                <IconButton
-                  aria-label={viewColumns}
-                  classes={{ root: this.getActiveIcon(classes, 'viewcolumns') }}
-                  onClick={this.setActiveIcon.bind(null, 'viewcolumns')}>
-                  <Tooltip title={viewColumns}>
+                <Tooltip title={viewColumns}>
+                  <IconButton
+                    aria-label={viewColumns}
+                    classes={{ root: this.getActiveIcon(classes, 'viewcolumns') }}
+                    onClick={this.setActiveIcon.bind(null, 'viewcolumns')}>
                     <ViewColumnIcon />
-                  </Tooltip>
-                </IconButton>
+                  </IconButton>
+                </Tooltip>
               }
               content={
                 <TableViewCol data={data} columns={columns} options={options} onColumnUpdate={toggleViewColumn} />
@@ -235,14 +235,14 @@ class TableToolbar extends React.Component {
               refExit={this.setActiveIcon.bind(null)}
               classes={{ paper: classes.filterPaper }}
               trigger={
-                <IconButton
-                  aria-label={filterTable}
-                  classes={{ root: this.getActiveIcon(classes, 'filter') }}
-                  onClick={this.setActiveIcon.bind(null, 'filter')}>
-                  <Tooltip title={filterTable}>
+                <Tooltip title={filterTable}>
+                  <IconButton
+                    aria-label={filterTable}
+                    classes={{ root: this.getActiveIcon(classes, 'filter') }}
+                    onClick={this.setActiveIcon.bind(null, 'filter')}>
                     <FilterIcon />
-                  </Tooltip>
-                </IconButton>
+                  </IconButton>
+                </Tooltip>
               }
               content={
                 <TableFilter
diff --git a/webpack.config.js b/webpack.config.js
index 43ad278..f3cd389 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -3,7 +3,7 @@ const webpack = require('webpack');
 
 module.exports = {
   entry: {
-    app: "./examples/custom-action-columns/index.js"
+    app: "./examples/selectable-rows/index.js"
   },
   stats: "verbose",
   context: __dirname,


Test patch:
diff --git a/test/MUIDataTable.test.js b/test/MUIDataTable.test.js
index 0bea639..a5241c3 100644
--- a/test/MUIDataTable.test.js
+++ b/test/MUIDataTable.test.js
@@ -170,6 +170,21 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(JSON.stringify(state.displayData), displayData);
   });
 
+  it('should correctly re-build display after xhr with serverSide=true', done => {
+    const fullWrapper = mount(<MUIDataTable columns={columns} data={[]} options={{ serverSide: true }} />);
+    assert.strictEqual(fullWrapper.find('tbody tr').length, 1);
+
+    // simulate xhr and test number of displayed rows
+    new Promise((resolve, reject) => {
+      setTimeout(() => {
+        fullWrapper.setProps({ data });
+        fullWrapper.update();
+        assert.strictEqual(fullWrapper.find('tbody tr').length, 4);
+        done();
+      }, 10);
+    });
+  });
+
   it('should correctly re-build internal table data and displayData structure with prop change', () => {
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />);
     let state = shallowWrapper.dive().state();
@@ -679,6 +694,45 @@ describe('<MUIDataTable />', function() {
     assert.deepEqual(state.selectedRows.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();
+    const onRowsDelete = () => {};
+    const selectedRows = { data: [1] };
+
+    instance.selectRowUpdate('cell', { index: 0, dataIndex: 0 });
+    shallowWrapper.update();
+    instance.selectRowDelete()
+    shallowWrapper.update();
+
+    const newDisplayData = shallowWrapper.state().displayData;
+    const expectedResult = JSON.stringify([
+      { data: ['Walsh, John', 'Test Corp', renderCities('Hartford', { rowIndex: 0 }), null, undefined], dataIndex: 1, },
+      { data: ['Herm, Bob', 'Test Corp', renderCities('Tampa', { rowIndex: 1 }), 'FL', undefined], dataIndex: 2, },
+      { data: ['Houston, James', 'Test Corp', renderCities('Dallas', { rowIndex: 2 }), 'TX', undefined], dataIndex: 3, },
+    ]);
+
+    assert.deepEqual(JSON.stringify(newDisplayData), expectedResult);
+  });
+
+  it('should not remove selected data on selectRowDelete when type=cell when onRowsDelete returns false', () => {
+    const options = {
+      onRowsDelete: () => false
+    };
+    const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} options={options} />).dive();
+    const instance = shallowWrapper.instance();
+    const selectedRows = { data: [1] };
+
+    instance.selectRowUpdate('cell', { index: 0, dataIndex: 0 });
+    shallowWrapper.update();
+    instance.selectRowDelete()
+    shallowWrapper.update();
+
+    const myDisplayData = shallowWrapper.state().displayData;
+
+    assert.deepEqual(JSON.stringify(myDisplayData), displayData);
+  });
+
   it('should update value when calling updateValue method in customBodyRender', () => {
     const shallowWrapper = shallow(<MUIDataTable columns={columns} data={data} />).dive();
     const instance = shallowWrapper.instance();