From afc41879ee6bec84acad9847c3b0fe58171c3cd2 Mon Sep 17 00:00:00 2001 From: Allen Date: Fri, 20 Oct 2017 03:25:48 -0500 Subject: [PATCH] fix #106 * implement selectRow.nonSelectable * add story for selectRow.nonSelectable * add testing for selectRow.nonSelectable * refine tests about row selection * patch docs for selectRow.nonSelectable --- docs/row-selection.md | 11 +++ .../row-selection/non-selectable-rows.js | 55 ++++++++++++ .../stories/index.js | 5 +- .../src/bootstrap-table.js | 3 +- .../src/row-selection/selection-cell.js | 15 ++-- .../src/row-selection/wrapper.js | 9 +- packages/react-bootstrap-table2/src/row.js | 4 + .../react-bootstrap-table2/src/store/base.js | 23 ++++- .../test/row-selection/selection-cell.test.js | 80 +++++++++++++++--- .../react-bootstrap-table2/test/row.test.js | 83 ++++++++++++++++--- .../test/store/base.test.js | 55 +++++++++--- 11 files changed, 294 insertions(+), 49 deletions(-) create mode 100644 packages/react-bootstrap-table2-example/examples/row-selection/non-selectable-rows.js diff --git a/docs/row-selection.md b/docs/row-selection.md index 3b6ddcd..6143295 100644 --- a/docs/row-selection.md +++ b/docs/row-selection.md @@ -11,6 +11,7 @@ The following are available properties in `selectRow`: * [mode (**required**)](#mode) * [style](#style) * [classes)](#classes) +* [nonSelectable)](#nonSelectable) #### Optional @@ -86,3 +87,13 @@ const selectRow = { classes: (row, rowIndex) => { return ...; } }; ``` + +## selectRow.nonSelectable - [Array] +This prop allow you to restrict some rows which can not be selected by user. `selectRow.nonSelectable` accept an rowkeys array. + +```js +const selectRow = { + mode: 'checkbox', + nonSelectable: [1, 3 ,5] +}; +``` \ No newline at end of file diff --git a/packages/react-bootstrap-table2-example/examples/row-selection/non-selectable-rows.js b/packages/react-bootstrap-table2-example/examples/row-selection/non-selectable-rows.js new file mode 100644 index 0000000..d74d785 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/row-selection/non-selectable-rows.js @@ -0,0 +1,55 @@ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table2'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const selectRow = { + mode: 'checkbox', + nonSelectable: [0, 2, 4] +}; + +const sourceCode = `\ +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const selectRow = { + mode: 'checkbox', + nonSelectable: [0, 2, 4] +}; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js index e0ed3ca..95192e0 100644 --- a/packages/react-bootstrap-table2-example/stories/index.js +++ b/packages/react-bootstrap-table2-example/stories/index.js @@ -55,6 +55,7 @@ import SingleSelectionTable from 'examples/row-selection/single-selection'; import MultipleSelectionTable from 'examples/row-selection/multiple-selection'; import SelectionStyleTable from 'examples/row-selection/selection-style'; import SelectionClassTable from 'examples/row-selection/selection-class'; +import NonSelectableRowsTable from 'examples/row-selection/non-selectable-rows'; // css style import 'bootstrap/dist/css/bootstrap.min.css'; @@ -118,5 +119,5 @@ storiesOf('Row Selection', module) .add('Single Selection', () => ) .add('Multiple Selection', () => ) .add('Selection Style', () => ) - .add('Selection Class', () => ); - + .add('Selection Class', () => ) + .add('Not Selectabled Rows', () => ); diff --git a/packages/react-bootstrap-table2/src/bootstrap-table.js b/packages/react-bootstrap-table2/src/bootstrap-table.js index 13a9cce..f336f04 100644 --- a/packages/react-bootstrap-table2/src/bootstrap-table.js +++ b/packages/react-bootstrap-table2/src/bootstrap-table.js @@ -129,7 +129,8 @@ BootstrapTable.propTypes = { selectRow: PropTypes.shape({ mode: PropTypes.oneOf([Const.ROW_SELECT_SINGLE, Const.ROW_SELECT_MULTIPLE]).isRequired, style: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - classes: PropTypes.oneOfType([PropTypes.string, PropTypes.func]) + classes: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + nonSelectable: PropTypes.array }), onRowSelect: PropTypes.func, onAllRowsSelect: PropTypes.func diff --git a/packages/react-bootstrap-table2/src/row-selection/selection-cell.js b/packages/react-bootstrap-table2/src/row-selection/selection-cell.js index 604188b..fa41f3a 100644 --- a/packages/react-bootstrap-table2/src/row-selection/selection-cell.js +++ b/packages/react-bootstrap-table2/src/row-selection/selection-cell.js @@ -11,7 +11,8 @@ export default class SelectionCell extends Component { mode: PropTypes.string.isRequired, rowKey: PropTypes.any, selected: PropTypes.bool, - onRowSelect: PropTypes.func + onRowSelect: PropTypes.func, + disabled: PropTypes.bool } constructor() { @@ -26,15 +27,17 @@ export default class SelectionCell extends Component { } handleRowClick() { - const { ROW_SELECT_SINGLE } = Const; const { mode: inputType, rowKey, selected, - onRowSelect + onRowSelect, + disabled } = this.props; - const checked = inputType === ROW_SELECT_SINGLE + if (disabled) return; + + const checked = inputType === Const.ROW_SELECT_SINGLE ? true : !selected; @@ -44,7 +47,8 @@ export default class SelectionCell extends Component { render() { const { mode: inputType, - selected + selected, + disabled } = this.props; return ( @@ -52,6 +56,7 @@ export default class SelectionCell extends Component { ); diff --git a/packages/react-bootstrap-table2/src/row-selection/wrapper.js b/packages/react-bootstrap-table2/src/row-selection/wrapper.js index ab2fb9f..97909f2 100644 --- a/packages/react-bootstrap-table2/src/row-selection/wrapper.js +++ b/packages/react-bootstrap-table2/src/row-selection/wrapper.js @@ -46,13 +46,16 @@ class RowSelectionWrapper extends Component { * @param {Boolean} option - customized result for all rows selection */ handleAllRowsSelect(option) { - const { store } = this.props; - const selected = store.isAnySelectedRow(); + const { store, selectRow } = this.props; + const selected = store.isAnySelectedRow(selectRow.nonSelectable); // set next status of all row selected by store.selected or customizing by user. const result = option || !selected; - const currSelected = result ? store.selectAllRowKeys() : []; + const currSelected = result ? + store.selectAllRows(selectRow.nonSelectable) : + store.cleanSelectedRows(selectRow.nonSelectable); + store.setSelectedRowKeys(currSelected); diff --git a/packages/react-bootstrap-table2/src/row.js b/packages/react-bootstrap-table2/src/row.js index 1949c33..d93f384 100644 --- a/packages/react-bootstrap-table2/src/row.js +++ b/packages/react-bootstrap-table2/src/row.js @@ -30,6 +30,9 @@ const Row = (props) => { ...rest } = cellEdit; + const key = _.get(row, keyField); + const { nonSelectable } = selectRow; + return ( { @@ -40,6 +43,7 @@ const Row = (props) => { { ...selectRow } rowKey={ _.get(row, keyField) } selected={ selected } + disabled={ nonSelectable && nonSelectable.includes(key) } /> ) } diff --git a/packages/react-bootstrap-table2/src/store/base.js b/packages/react-bootstrap-table2/src/store/base.js index 6006861..eb435d1 100644 --- a/packages/react-bootstrap-table2/src/store/base.js +++ b/packages/react-bootstrap-table2/src/store/base.js @@ -54,15 +54,30 @@ export default class Store { return this.selected; } - selectAllRowKeys() { - return this.data.map(row => _.get(row, this.keyField)); + selectAllRows(nonSelectableRows = []) { + if (nonSelectableRows.length === 0) { + return this.data.map(row => _.get(row, this.keyField)); + } + return this.data + .filter(row => !nonSelectableRows.includes(_.get(row, this.keyField))) + .map(row => _.get(row, this.keyField)); + } + + cleanSelectedRows(nonSelectableRows = []) { + if (nonSelectableRows.length === 0) { + return []; + } + return this.selected.filter(x => nonSelectableRows.includes(x)); } isAllRowsSelected() { return this.data.length === this.selected.length; } - isAnySelectedRow() { - return this.selected.length > 0; + isAnySelectedRow(nonSelectableRows = []) { + if (nonSelectableRows.length === 0) { + return this.selected.length > 0; + } + return this.selected.filter(x => !nonSelectableRows.includes(x)).length; } } diff --git a/packages/react-bootstrap-table2/test/row-selection/selection-cell.test.js b/packages/react-bootstrap-table2/test/row-selection/selection-cell.test.js index d443301..5c92145 100644 --- a/packages/react-bootstrap-table2/test/row-selection/selection-cell.test.js +++ b/packages/react-bootstrap-table2/test/row-selection/selection-cell.test.js @@ -36,28 +36,65 @@ describe('', () => { describe('handleRowClick', () => { describe('when was been clicked', () => { const rowKey = 1; - const mockOnRowSelect = sinon.stub(); + const selected = true; + let mockOnRowSelect; const spy = sinon.spy(SelectionCell.prototype, 'handleRowClick'); beforeEach(() => { + mockOnRowSelect = sinon.stub(); + }); + + afterEach(() => { spy.reset(); mockOnRowSelect.reset(); }); - it('should call handleRowClicked', () => { - wrapper = shallow( - - ); + describe('when disabled prop is false', () => { + beforeEach(() => { + wrapper = shallow( + + ); + wrapper.find('td').simulate('click'); + }); - wrapper.find('td').simulate('click'); + it('should calling handleRowClicked', () => { + expect(spy.calledOnce).toBe(true); + }); - expect(spy.calledOnce).toBe(true); - expect(mockOnRowSelect.calledOnce).toBe(true); + it('should calling onRowSelect callback correctly', () => { + expect(mockOnRowSelect.calledOnce).toBe(true); + expect( + mockOnRowSelect.calledWith(rowKey, !selected) + ).toBe(true); + }); + }); + + describe('when disabled prop is true', () => { + beforeEach(() => { + wrapper = shallow( + + ); + wrapper.find('td').simulate('click'); + }); + + it('should calling handleRowClicked', () => { + expect(spy.calledOnce).toBe(true); + }); + + it('should not calling onRowSelect callback', () => { + expect(mockOnRowSelect.calledOnce).toBe(false); + }); }); describe('if selectRow.mode is radio', () => { @@ -132,5 +169,22 @@ describe('', () => { expect(wrapper.find('input').get(0).props.type).toBe(mode); expect(wrapper.find('input').get(0).props.checked).toBe(selected); }); + + describe('when disabled prop give as true', () => { + beforeEach(() => { + wrapper = shallow( + + ); + }); + + it('should render component with disabled attribute', () => { + expect(wrapper.find('input').get(0).props.disabled).toBeTruthy(); + }); + }); }); }); diff --git a/packages/react-bootstrap-table2/test/row.test.js b/packages/react-bootstrap-table2/test/row.test.js index c332f27..73d01ea 100644 --- a/packages/react-bootstrap-table2/test/row.test.js +++ b/packages/react-bootstrap-table2/test/row.test.js @@ -20,6 +20,9 @@ const defaultColumns = [{ text: 'Price' }]; +const keyField = 'id'; +const rowIndex = 1; + describe('Row', () => { let wrapper; @@ -32,7 +35,13 @@ describe('Row', () => { describe('simplest row', () => { beforeEach(() => { wrapper = shallow( - ); + + ); }); it('should render successfully', () => { @@ -48,7 +57,7 @@ describe('Row', () => { wrapper = shallow( { wrapper = shallow( { describe('when cellEdit prop is defined', () => { let columns; let cellEdit; - const rowIndex = 1; - const keyField = 'id'; beforeEach(() => { columns = defaultColumns; @@ -270,7 +277,7 @@ describe('Row', () => { { describe('when selectRow.mode is ROW_SELECT_DISABLED (row was un-selectable)', () => { beforeEach(() => { wrapper = shallow( - ); + + ); }); - it('should not render ', () => { + it('should not render SelectionCell component', () => { expect(wrapper.find(SelectionCell).length).toBe(0); }); }); describe('when selectRow.mode is checkbox or radio (row was selectable)', () => { + let selectRow; beforeEach(() => { - const selectRow = { mode: 'checkbox' }; + selectRow = { mode: 'checkbox' }; wrapper = shallow( ); }); - it('should render ', () => { + it('should rendering SelectionCell component correctly', () => { expect(wrapper.find(SelectionCell).length).toBe(1); }); + + it('should render SelectionCell component with correct props', () => { + expect(wrapper.find(SelectionCell).props().selected).toBeTruthy(); + expect(wrapper.find(SelectionCell).props().disabled).toBeFalsy(); + expect(wrapper.find(SelectionCell).props().mode).toEqual(selectRow.mode); + }); + + describe('if selectRow.nonSelectable is defined and contain a rowkey which is match to current row', () => { + beforeEach(() => { + selectRow = { mode: 'checkbox', nonSelectable: [row.id] }; + wrapper = shallow( + ); + }); + + it('should render SelectionCell component with correct disable prop correctly', () => { + expect(wrapper.find(SelectionCell).length).toBe(1); + expect(wrapper.find(SelectionCell).prop('disabled')).toBeTruthy(); + }); + }); + + describe('if selectRow.nonSelectable is defined and not contain any rowkey which is match to current row', () => { + beforeEach(() => { + selectRow = { mode: 'checkbox', nonSelectable: [3, 4, 6] }; + wrapper = shallow( + ); + }); + + it('should render SelectionCell component with correct disable prop correctly', () => { + expect(wrapper.find(SelectionCell).length).toBe(1); + expect(wrapper.find(SelectionCell).prop('disabled')).toBeFalsy(); + }); + }); }); }); diff --git a/packages/react-bootstrap-table2/test/store/base.test.js b/packages/react-bootstrap-table2/test/store/base.test.js index d3b0ec3..716a350 100644 --- a/packages/react-bootstrap-table2/test/store/base.test.js +++ b/packages/react-bootstrap-table2/test/store/base.test.js @@ -111,37 +111,72 @@ describe('Store Base', () => { }); }); - describe('selectAllRowKeys', () => { + describe('selectAllRows', () => { it('should return all row keys', () => { - const rowKeys = store.selectAllRowKeys(); + const rowKeys = store.selectAllRows(); expect(Array.isArray(rowKeys)).toBeTruthy(); - expect(rowKeys).toEqual([3, 2, 4, 1]); + expect(rowKeys).toEqual(data.map(x => x[store.keyField])); + }); + + it('should return correct row keys when nonSelectableRows args is not empty', () => { + const nonSelectableRows = [1, 3]; + const rowKeys = store.selectAllRows(nonSelectableRows); + + expect(Array.isArray(rowKeys)).toBeTruthy(); + expect(rowKeys).toEqual( + data + .filter(x => !nonSelectableRows.includes(_.get(x, store.keyField))) + .map(x => x[store.keyField]) + ); }); }); describe('isAllRowsSelected', () => { - it('should return true when all rows was selected', () => { + it('should return true when all rows is selected', () => { store.selected = data.map(row => _.get(row, store.keyField)); expect(store.isAllRowsSelected()).toBeTruthy(); }); - it('should return false when all rows was not selected', () => { + it('should return false when not all of rows is selected', () => { store.selected = [1]; - expect(store.isAllRowsSelected()).not.toBeTruthy(); + expect(store.isAllRowsSelected()).toBeFalsy(); }); }); describe('isAnySelectedRow', () => { - it('should return true when one or more than one rows were selected', () => { - store.selected = data.map(row => _.get(row, store.keyField)); + describe('if store.selected not empty', () => { + it('should return true', () => { + store.selected = data.map(row => _.get(row, store.keyField)); + expect(store.isAnySelectedRow()).toBeTruthy(); + }); - expect(store.isAnySelectedRow()).toBeTruthy(); + describe('when nonSelectableRows given and not all of nonselectable rows are match current store.selected', () => { + const nonSelectableRows = [1, 3]; + beforeEach(() => { + store.selected = [1, 4]; + }); + + it('should return true', () => { + expect(store.isAnySelectedRow(nonSelectableRows)).toBeTruthy(); + }); + }); + + describe('when nonSelectableRows given and all of nonselectable rows are match current store.selected', () => { + const nonSelectableRows = [1, 3]; + beforeEach(() => { + store.selected = nonSelectableRows; + }); + + it('should return false', () => { + expect(store.isAnySelectedRow(nonSelectableRows)).toBeFalsy(); + }); + }); }); - it('should return false when none was selected', () => { + it('should return false if store.selected is empty', () => { store.selected = []; expect(store.isAnySelectedRow()).not.toBeTruthy();