* implement selectRow.nonSelectable

* add story for selectRow.nonSelectable

* add testing for selectRow.nonSelectable

* refine tests about row selection

* patch docs for selectRow.nonSelectable
This commit is contained in:
Allen 2017-10-20 03:25:48 -05:00 committed by GitHub
parent 10f06dca10
commit afc41879ee
11 changed files with 294 additions and 49 deletions

View File

@ -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 ...; }
};
```
## <a name='nonSelectable'>selectRow.nonSelectable - [Array]</a>
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]
};
```

View File

@ -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]
};
<BootstrapTable
keyField='id'
data={ products }
columns={ columns }
selectRow={ selectRow }
/>
`;
export default () => (
<div>
<BootstrapTable keyField="id" data={ products } columns={ columns } selectRow={ selectRow } />
<Code>{ sourceCode }</Code>
</div>
);

View File

@ -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', () => <SingleSelectionTable />)
.add('Multiple Selection', () => <MultipleSelectionTable />)
.add('Selection Style', () => <SelectionStyleTable />)
.add('Selection Class', () => <SelectionClassTable />);
.add('Selection Class', () => <SelectionClassTable />)
.add('Not Selectabled Rows', () => <NonSelectableRowsTable />);

View File

@ -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

View File

@ -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 {
<input
type={ inputType }
checked={ selected }
disabled={ disabled }
/>
</td>
);

View File

@ -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);

View File

@ -30,6 +30,9 @@ const Row = (props) => {
...rest
} = cellEdit;
const key = _.get(row, keyField);
const { nonSelectable } = selectRow;
return (
<tr style={ style } className={ className }>
{
@ -40,6 +43,7 @@ const Row = (props) => {
{ ...selectRow }
rowKey={ _.get(row, keyField) }
selected={ selected }
disabled={ nonSelectable && nonSelectable.includes(key) }
/>
)
}

View File

@ -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;
}
}

View File

@ -36,28 +36,65 @@ describe('<SelectionCell />', () => {
describe('handleRowClick', () => {
describe('when <input /> 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(
<SelectionCell
selected
rowKey={ rowKey }
mode={ mode }
onRowSelect={ mockOnRowSelect }
/>
);
describe('when disabled prop is false', () => {
beforeEach(() => {
wrapper = shallow(
<SelectionCell
selected
rowKey={ rowKey }
mode={ mode }
onRowSelect={ mockOnRowSelect }
/>
);
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(
<SelectionCell
selected
rowKey={ rowKey }
mode={ mode }
onRowSelect={ mockOnRowSelect }
disabled
/>
);
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('<SelectionCell />', () => {
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(
<SelectionCell
rowKey={ 1 }
mode={ mode }
selected={ selected }
disabled
/>
);
});
it('should render component with disabled attribute', () => {
expect(wrapper.find('input').get(0).props.disabled).toBeTruthy();
});
});
});
});

View File

@ -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(
<Row { ...mockBodyResolvedProps } rowIndex={ 1 } columns={ defaultColumns } row={ row } />);
<Row
{ ...mockBodyResolvedProps }
rowIndex={ rowIndex }
columns={ defaultColumns }
row={ row }
/>
);
});
it('should render successfully', () => {
@ -48,7 +57,7 @@ describe('Row', () => {
wrapper = shallow(
<Row
{ ...mockBodyResolvedProps }
rowIndex={ 1 }
rowIndex={ rowIndex }
columns={ defaultColumns }
row={ row }
style={ customStyle }
@ -67,7 +76,7 @@ describe('Row', () => {
wrapper = shallow(
<Row
{ ...mockBodyResolvedProps }
rowIndex={ 1 }
rowIndex={ rowIndex }
columns={ defaultColumns }
row={ row }
className={ className }
@ -83,8 +92,6 @@ describe('Row', () => {
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', () => {
<Row
{ ...mockBodyResolvedProps }
row={ row }
rowIndex={ 1 }
rowIndex={ rowIndex }
columns={ columns }
keyField={ keyField }
cellEdit={ cellEdit }
@ -437,29 +444,83 @@ describe('Row', () => {
describe('when selectRow.mode is ROW_SELECT_DISABLED (row was un-selectable)', () => {
beforeEach(() => {
wrapper = shallow(
<Row { ...mockBodyResolvedProps } rowIndex={ 1 } columns={ defaultColumns } row={ row } />);
<Row
{ ...mockBodyResolvedProps }
rowIndex={ rowIndex }
columns={ defaultColumns }
row={ row }
/>
);
});
it('should not render <SelectionCell />', () => {
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(
<Row
{ ...mockBodyResolvedProps }
rowIndex={ 1 }
rowIndex={ rowIndex }
columns={ defaultColumns }
row={ row }
selectRow={ selectRow }
selected
/>);
});
it('should render <SelectionCell />', () => {
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(
<Row
{ ...mockBodyResolvedProps }
rowIndex={ rowIndex }
columns={ defaultColumns }
row={ row }
keyField={ keyField }
selectRow={ selectRow }
/>);
});
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(
<Row
{ ...mockBodyResolvedProps }
rowIndex={ rowIndex }
columns={ defaultColumns }
row={ row }
keyField={ keyField }
selectRow={ selectRow }
/>);
});
it('should render SelectionCell component with correct disable prop correctly', () => {
expect(wrapper.find(SelectionCell).length).toBe(1);
expect(wrapper.find(SelectionCell).prop('disabled')).toBeFalsy();
});
});
});
});

View File

@ -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();