From 7a5b88bcf626cc26b6f40dd79233849e9a21385c Mon Sep 17 00:00:00 2001 From: Allen Date: Tue, 17 Oct 2017 02:27:48 -0500 Subject: [PATCH] fix #63 * support async cell editing * refine cellEdit.onUpdate and cellEdit.editing * refine cell edit * add redux * add stories for async cell edit * fix test case and patch tests for async cell editing * patch docs for cellEdit prop * fix bug produced by rebasing lol --- docs/README.md | 31 +- docs/cell-edit-prop.md | 136 +++++++++ .../cell-edit/cell-edit-with-promise-table.js | 75 +++++ .../cell-edit/cell-edit-with-redux-table.js | 212 ++++++++++++++ .../package.json | 3 + .../src/index.js | 84 ++++-- .../src/utils/common.js | 2 + .../stories/index.js | 7 +- .../src/bootstrap-table.js | 104 +++---- .../src/cell-edit-wrapper.js | 105 +++++++ .../src/editing-cell.js | 38 ++- .../src/props-resolver/index.js | 13 +- .../src/stateful-layer.js | 62 +++- .../react-bootstrap-table2/src/store/base.js | 7 +- packages/react-bootstrap-table2/src/utils.js | 7 +- .../test/bootstrap-table.test.js | 52 +++- .../test/cell-edit-wrapper.test.js | 268 ++++++++++++++++++ .../test/editing-cell.test.js | 28 +- .../test/props-resolver/index.test.js | 51 ++-- .../react-bootstrap-table2/test/row.test.js | 5 +- .../test/stateful-layer.test.js | 133 +++++++++ .../test/test-helpers/mock-component.js | 8 +- 22 files changed, 1232 insertions(+), 199 deletions(-) create mode 100644 docs/cell-edit-prop.md create mode 100644 packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-promise-table.js create mode 100644 packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-redux-table.js create mode 100644 packages/react-bootstrap-table2/src/cell-edit-wrapper.js create mode 100644 packages/react-bootstrap-table2/test/cell-edit-wrapper.test.js create mode 100644 packages/react-bootstrap-table2/test/stateful-layer.test.js diff --git a/docs/README.md b/docs/README.md index 66e6923..14c3cf3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,33 +38,4 @@ Same as `.table-hover` class for adding a hover effect (grey background color) o Same as `.table-condensed` class for makeing a table more compact by cutting cell padding in half ### cellEdit - [Object] -Assign a valid `cellEdit` object can enable the cell editing on the cell. The default usage is click/dbclick to trigger cell editing and press `ENTER` to save cell or press `ESC` to cancel editing. - -> Note: The `keyField` column can't be edited - -Following is a `cellEdit` object: -```js -{ - mode: 'click', - blurToSave: true, - timeToCloseMessage: 2500, - onEditing: (rowId, dataField, newValue) => { ... }, - beforeSaveCell: (oldValue, newValue, row, column) => { ... }, - afterSaveCell: (oldValue, newValue, row, column) => { ... }, - nonEditableRows: () => { ... } -} -``` -#### cellEdit.mode - [String] -`cellEdit.mode` possible value is `click` and `dbclick`. **It's required value** that tell `react-bootstrap-table2` how to trigger the cell editing. - -#### cellEdit.blurToSave - [Bool] -Default is `false`, enable it will be able to save the cell automatically when blur from the cell editor. - -#### cellEdit.nonEditableRows - [Function] -`cellEdit.nonEditableRows` accept a callback function and expect return an array which used to restrict all the columns of some rows as non-editable. So the each item in return array should be rowkey(`keyField`) - -#### cellEdit.timeToCloseMessage - [Function] -If a [`column.validator`](./columns.md#validator) defined and the new value is invalid, `react-bootstrap-table2` will popup a alert at the bottom of editor. `cellEdit.timeToCloseMessage` is a chance to let you decide how long the alert should be stay. Default is 3000 millisecond. - -### selectRow - [Object] -Pass prop `selectRow` to enable row selection. For more detail, please navigate to [row selection document](./row-selection.md). +`cellEdit` props accept an object, please see [cellEdit definition](./cell-edit-prop.md) for more detail. diff --git a/docs/cell-edit-prop.md b/docs/cell-edit-prop.md new file mode 100644 index 0000000..328a94e --- /dev/null +++ b/docs/cell-edit-prop.md @@ -0,0 +1,136 @@ +# Properties on cellEdit prop +* [mode (**required**)](#mode) +* [blurToSave](#blurToSave) +* [nonEditableRows](#nonEditableRows) +* [timeToCloseMessage](#timeToCloseMessage) +* [beforeSaveCell](#beforeSaveCell) +* [afterSaveCell](#afterSaveCell) +* [onUpdate](#onUpdate) +* [editing](#editing) +* [errorMessage](#errorMessage) +* [onErrorMessageDisappear](#onErrorMessageDisappear) + +## cellEdit - [Object] +Assign a valid `cellEdit` object can enable the cell editing on the cell. The default usage is click/dbclick to trigger cell editing and press `ENTER` to save cell or press `ESC` to cancel editing. + +> Note: The `keyField` column can't be edited + +Following is the shape of `cellEdit` object: +```js +{ + mode: 'click', + blurToSave: true, + timeToCloseMessage: 2500, + editing: false|true, + errorMessage: '', + onUpdate: (rowId, dataField, newValue) => { ... }, + beforeSaveCell: (oldValue, newValue, row, column) => { ... }, + afterSaveCell: (oldValue, newValue, row, column) => { ... }, + onErrorMessageDisappear: () => { ... }, + nonEditableRows: () => { ... } +} +``` + +### cellEdit.mode - [String] +`cellEdit.mode` possible value is `click` and `dbclick`. **It's required value** that tell `react-bootstrap-table2` how to trigger the cell editing. + +### cellEdit.blurToSave - [Bool] +Default is `false`, enable it will be able to save the cell automatically when blur from the cell editor. + +### cellEdit.nonEditableRows - [Function] +`cellEdit.nonEditableRows` accept a callback function and expect return an array which used to restrict all the columns of some rows as non-editable. So the each item in return array should be rowkey(`keyField`) + +### cellEdit.timeToCloseMessage - [Function] +If a [`column.validator`](./columns.md#validator) defined and the new value is invalid, `react-bootstrap-table2` will popup a alert at the bottom of editor. `cellEdit.timeToCloseMessage` is a chance to let you decide how long the alert should be stay. Default is 3000 millisecond. + +### cellEdit.beforeSaveCell - [Function] +This callback function will be called before triggering cell update. + +```js +const cellEdit = { + // omit... + beforeSaveCell: (oldValue, newValue, row, column) => { ... } +} +``` + +### cellEdit.afterSaveCell - [Function] +This callback function will be called after updating cell. + +```js +const cellEdit = { + // omit... + afterSaveCell: (oldValue, newValue, row, column) => { ... } +}; +``` + +### cellEdit.onUpdate - [Function] +If you want to control the cell updating process by yourself, for example, connect with `Redux` or saving data to backend database, `cellEdit.onUpdate` is a great chance you can work on it. + +Firsylt, `react-bootstrap-table2` allow `cellEdit.onUpdate` to return a promise: + +```js +const cellEdit = { + // omit... + onUpdate: (rowId, dataField, newValue) => { + return apiCall().then(response => { + console.log('update cell to backend successfully'); + // Actually, you dont do any thing here, we will update the new value when resolve your promise + }) + .catch(err => throw new Error(err.message)); + } +}; +``` + +If your promise is resolved successfully, `react-bootstrap-table2` will default help you to update the new cell value. +If your promise is resolved failure, you can throw an `Error` instance, `react-bootstrap-table2` will show up the error message (Same behavior like [`column.validator`](./columns.md#validator)). + +In some case, backend will return a new value to client side and you want to apply this new value instead of the value that user input. In this situation, you can return an object which contain a `value` property: + +```js +const cellEdit = { + // omit... + onUpdate: (rowId, dataField, newValue) => { + return apiCall().then(response => { + return { value: response.value }; // response.value is from your backend api + }) + .catch(err => throw new Error(err.message)); + } +}; +``` + +If your application integgrate with `Redux`, you may need to dispatch an action in `cellEdit.onUpdate` callback. In this circumstances, you need to return `false` explicity which `react-bootstrap-table2` will stop any operation internally and wait rerender by your application. + +In a simple redux application, you probably need to handle those props by your application: + +* [`cellEdit.editing`](#editing): Is cell still on editing or not? This value should always be `true` when saving cell failure. +* [`cellEdit.errorMessage`](#errorMessage): Error message when save the cell failure. +* [`cellEdit.onErrorMessageDisappear`](#onErrorMessageDisappear): This callback will be called when error message alert closed automatically. +* `cellEdit.onUpdate` + +```js +const cellEdit = { + editing: this.props.editing, + errorMessage: this.props.errorMessage, + onErrorMessageDisappear: () => { + // cleanErrorMessage is an action creator + this.props.dispatch(cleanErrorMessage()); + }, + onUpdate: (rowId, dataField, newValue) => { + // updateCell is an action creator + this.props.dispatch(updateCell(rowId, dataField, newValue))); + return false; // Have to return false here + } +}; +``` + +Please check [this](https://github.com/react-bootstrap-table/react-bootstrap-table2/blob/develop/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-redux-table.js) exmaple to learn how use `cellEdit` with a redux application + +### cellEdit.editing - [Bool] +This only used when you want to control cell saving externally, `cellEdit.editing` will be a flag to tell `react-bootstrap-table2` whether currecnt editing cell is still editing or not. Because, it's possible that some error happen when you saving cell, in this situation, you need to configre this value as `false` to keep the cell as edtiable and show up an error message. + +### cellEdit.errorMessage - [String] +Same as [`cellEdit.editing`](#editing). This prop is not often used. Only used when you keep the error message in your application state. + +### cellEdit.onErrorMessageDisappear - [Function] +This callback function will be called when error message discard. + diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-promise-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-promise-table.js new file mode 100644 index 0000000..c9e6874 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-promise-table.js @@ -0,0 +1,75 @@ +/* eslint no-unused-vars: 0 */ +/* eslint arrow-body-style: 0 */ +import React, { Component } from 'react'; + +import { BootstrapTableful } from 'react-bootstrap-table2'; +import Code from 'components/common/code-block'; +import { productsGenerator, sleep } from 'utils/common'; + +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +class CellEditWithPromise extends Component { + handleCellEditing = (rowId, dataField, newValue) => { + return sleep(1000).then(() => { + if (dataField === 'price' && (newValue < 2000 || isNaN(newValue))) { + throw new Error('Product Price should bigger than $2000'); + } + }); + } + + render() { + const cellEdit = { + mode: 'click', + blurToSave: true, + onUpdate: this.handleCellEditing + }; + + return ( +
+ + { sourceCode } +
+ ); + } +} +`; + +class CellEditWithPromise extends Component { + handleCellEditing = (rowId, dataField, newValue) => { + return sleep(1000).then(() => { + if (dataField === 'price' && (newValue < 2000 || isNaN(newValue))) { + throw new Error('Product Price should bigger than $2000'); + } + }); + } + + render() { + const cellEdit = { + mode: 'click', + blurToSave: true, + onUpdate: this.handleCellEditing + }; + + return ( +
+ + { sourceCode } +
+ ); + } +} + +export default CellEditWithPromise; + diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-redux-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-redux-table.js new file mode 100644 index 0000000..d961ad1 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-redux-table.js @@ -0,0 +1,212 @@ +/* eslint no-unused-vars: 0 */ +/* eslint react/prop-types: 0 */ +/* eslint arrow-body-style: 0 */ +/* eslint consistent-return: 0 */ +/* eslint no-class-assign: 0 */ +import React, { Component } from 'react'; +import thunk from 'redux-thunk'; +import { Provider, connect } from 'react-redux'; +import { createStore, applyMiddleware } from 'redux'; +import { BootstrapTableful } from 'react-bootstrap-table2'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +/////////////////////// Action Creator /////////////////////// +const setErrorMessage = (errorMessage = null) => { + return { type: 'SET_ERR_MESSAGE', errorMessage }; +}; + +// Async Action, using redux-thunk +const cellEditingAsync = (rowId, dataField, newValue) => { + return (dispatch) => { + setTimeout(() => { + if (dataField === 'price' && (newValue < 2000 || isNaN(newValue))) { + dispatch(setErrorMessage('Product Price should bigger than $2000')); + } else { + dispatch({ type: 'ADD_SUCCESS', rowId, dataField, newValue }); + } + }, 1200); + }; +}; + +/////////////////////// Component /////////////////////// +class CellEditWithRedux extends Component { + // dispatch a async action + handleCellEditing = (rowId, dataField, newValue) => { + this.props.dispatch(cellEditingAsync(rowId, dataField, newValue)); + return false; + } + + handleErrorMsgDisappear = () => { + this.props.dispatch(setErrorMessage()); + } + + render() { + const cellEdit = { + mode: 'click', + editing: this.props.cellEditing, + errorMessage: this.props.errorMessage, + onUpdate: this.handleCellEditing, + onErrorMessageDisappear: this.handleErrorMsgDisappear + }; + + return ( +
+ + { sourceCode } +
+ ); + } +} +// connect +CellEditWithRedux = connect(state => state)(CellEditWithRedux); + +/////////////////////// Reducer /////////////////////// +// initial state object and simple reducers +const initialState = { + data: productsGenerator(), + cellEditing: false, + errorMessage: null +}; + +const reducers = (state, action) => { + switch (action.type) { + case 'ADD_SUCCESS': { + const { rowId, dataField, newValue } = action; + const data = [...state.data]; + const rowIndex = data.findIndex(r => r.id === rowId); + data[rowIndex][dataField] = newValue; + return { + data, + cellEditing: false, + errorMessage: null + }; + } + case 'SET_ERR_MESSAGE': { + const { errorMessage } = action; + return { + ...state, + cellEditing: true, + errorMessage + }; + } + default: { + return { ...state }; + } + } +}; + +/////////////////////// Index /////////////////////// +const store = createStore(reducers, initialState, applyMiddleware(thunk)); + +const Index = () => ( + + + +); +`; + +const setErrorMessage = (errorMessage = null) => { + return { type: 'SET_ERR_MESSAGE', errorMessage }; +}; + +// Async Action, using redux-thunk +const cellEditingAsync = (rowId, dataField, newValue) => { + return (dispatch) => { + setTimeout(() => { + if (dataField === 'price' && (newValue < 2000 || isNaN(newValue))) { + dispatch(setErrorMessage('Product Price should bigger than $2000')); + } else { + dispatch({ type: 'ADD_SUCCESS', rowId, dataField, newValue }); + } + }, 1200); + }; +}; + +class CellEditWithRedux extends Component { + // dispatch a async action + handleCellEditing = (rowId, dataField, newValue) => { + this.props.dispatch(cellEditingAsync(rowId, dataField, newValue)); + return false; + } + + handleErrorMsgDisappear = () => { + this.props.dispatch(setErrorMessage()); + } + + render() { + const cellEdit = { + mode: 'click', + editing: this.props.cellEditing, + errorMessage: this.props.errorMessage, + onUpdate: this.handleCellEditing, + onErrorMessageDisappear: this.handleErrorMsgDisappear + }; + + return ( +
+ + { sourceCode } +
+ ); + } +} +// connect +CellEditWithRedux = connect(state => state)(CellEditWithRedux); + +// initial state object and simple reducers +const initialState = { + data: productsGenerator(), + cellEditing: false, + errorMessage: null +}; + +const reducers = (state, action) => { + switch (action.type) { + case 'ADD_SUCCESS': { + const { rowId, dataField, newValue } = action; + const data = JSON.parse(JSON.stringify(state.data)); + const rowIndex = data.findIndex(r => r.id === rowId); + data[rowIndex][dataField] = newValue; + return { + data, + cellEditing: false, + errorMessage: null + }; + } + case 'SET_ERR_MESSAGE': { + const { errorMessage } = action; + return { + ...state, + cellEditing: true, + errorMessage + }; + } + default: { + return { ...state }; + } + } +}; + +const store = createStore(reducers, initialState, applyMiddleware(thunk)); + +const Index = () => ( + + + +); + +export default Index; + diff --git a/packages/react-bootstrap-table2-example/package.json b/packages/react-bootstrap-table2-example/package.json index 4e8bb00..76efab3 100644 --- a/packages/react-bootstrap-table2-example/package.json +++ b/packages/react-bootstrap-table2-example/package.json @@ -21,6 +21,9 @@ }, "devDependencies": { "@storybook/react": "^3.2.8", + "react-redux": "^5.0.6", + "redux": "^3.7.2", + "redux-thunk": "^2.2.0", "typed.js": "^2.0.5" } } diff --git a/packages/react-bootstrap-table2-example/src/index.js b/packages/react-bootstrap-table2-example/src/index.js index 6b2f5f9..a1af3b3 100644 --- a/packages/react-bootstrap-table2-example/src/index.js +++ b/packages/react-bootstrap-table2-example/src/index.js @@ -1,7 +1,12 @@ +/* eslint no-unused-vars: 0 */ +/* eslint no-debugger: 0 */ +/* eslint arrow-body-style: 0 */ import React from 'react'; import ReactDom from 'react-dom'; -import { BootstrapTableful } from 'react-bootstrap-table2'; +import { BootstrapTableful, createTable } from 'react-bootstrap-table2'; + +require('react-bootstrap-table2/style/react-bootstrap-table.scss'); const products = []; @@ -29,38 +34,85 @@ const columns = [{ style: { backgroundColor: 'red' }, - headerTitle: (column, colIndex) => { - console.log(column); - console.log(colIndex); - return 'yes~~~ oh'; - }, classes: 'my-xxx' }, { dataField: 'name', text: 'Product Name', headerTitle: true, formatter: (cell, row) => - (

{ cell }::: ${ row.price }

) + (

{ cell }::: ${ row.price }

), + validator: (newValue, row, column) => { + const validationForm = { + valid: true, + message: null + }; + validationForm.valid = false; + validationForm.message = 'Invalid message'; + return validationForm; + } }, { dataField: 'price', text: 'Product Price', - style: (cell, row, colIndex) => { - console.log(cell); - console.log(row); - console.log(colIndex); - return { - backgroundColor: 'blue' - }; + validator: (newValue, row, column) => { + if (newValue < 2000) { + return { + valid: false, + message: 'Price should bigger than 2000' + }; + } + return true; } }, { dataField: 'nest.address', text: 'Address' }, { dataField: 'nest.postcal', - text: 'Postal' + text: 'Postal', + editable: true, + validator: (newValue, row, column) => true }]; +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +const cellEdit = { + mode: 'click', + blurToSave: true, + // beforeSaveCell: (oldValue, newValue, row, column) => { + // console.log('beforeSavecell'); + // // console.log(oldValue); + // // console.log(newValue); + // // console.log(row); + // // console.log(column); + // }, + // afterSaveCell: (oldValue, newValue, row, column) => { + // console.log('aftersavecell'); + // // console.log(oldValue); + // // console.log(newValue); + // // console.log(row); + // // console.log(column); + // } + onUpdate: (rowId, dataField, newValue) => { + return sleep(1000).then(() => { + // return { forceUpdate: true }; + throw new Error('test is not successfully'); + }); + } + // nonEditableRows: () => [1, 3, 7] +}; + + +// let Table = withCellEdit({ +// mode: 'click', +// blurToSave: true, +// onEditing: (rowId, dataField, newValue) => { +// return sleep(1000).then(() => { +// // return { forceUpdate: true }; +// throw new Error('test is not successfully'); +// }); +// } +// })(BootstrapTable); +// Table = createTable(Table); ReactDom.render( - , + , document.getElementById('example')); diff --git a/packages/react-bootstrap-table2-example/src/utils/common.js b/packages/react-bootstrap-table2-example/src/utils/common.js index 8d95013..724e042 100644 --- a/packages/react-bootstrap-table2-example/src/utils/common.js +++ b/packages/react-bootstrap-table2-example/src/utils/common.js @@ -19,3 +19,5 @@ export const productsGenerator = (quantity = 5, callback) => { })) ); }; + +export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js index 4f0d85f..d3c1bec 100644 --- a/packages/react-bootstrap-table2-example/stories/index.js +++ b/packages/react-bootstrap-table2-example/stories/index.js @@ -45,6 +45,8 @@ import ColumnLevelEditableTable from 'examples/cell-edit/column-level-editable-t import CellLevelEditable from 'examples/cell-edit/cell-level-editable-table'; import CellEditHooks from 'examples/cell-edit/cell-edit-hooks-table'; import CellEditValidator from 'examples/cell-edit/cell-edit-validator-table'; +import CellEditWithPromise from 'examples/cell-edit/cell-edit-with-promise-table'; +import CellEditWithRedux from 'examples/cell-edit/cell-edit-with-redux-table'; // work on row selection import SingleSelectionTable from 'examples/row-selection/single-selection'; @@ -102,8 +104,11 @@ storiesOf('Cell Editing', module) .add('Column Level Editable', () => ) .add('Cell Level Editable', () => ) .add('Rich Hook Functions', () => ) - .add('Validation', () => ); + .add('Validation', () => ) + .add('Async Cell Editing(Promise)', () => ) + .add('Async Cell Editing(Redux)', () => ); storiesOf('Row Selection', module) .add('Single selection', () => ) .add('Multiple selection', () => ); + diff --git a/packages/react-bootstrap-table2/src/bootstrap-table.js b/packages/react-bootstrap-table2/src/bootstrap-table.js index 2aaf510..d0332c0 100644 --- a/packages/react-bootstrap-table2/src/bootstrap-table.js +++ b/packages/react-bootstrap-table2/src/bootstrap-table.js @@ -6,36 +6,32 @@ import cs from 'classnames'; import Header from './header'; import Caption from './caption'; import Body from './body'; -import Store from './store/base'; import PropsBaseResolver from './props-resolver'; import Const from './const'; -import _ from './utils'; class BootstrapTable extends PropsBaseResolver(Component) { constructor(props) { super(props); this.validateProps(); - const { store } = this.props; - this.store = !store ? new Store(props) : store; this.handleSort = this.handleSort.bind(this); - this.startEditing = this.startEditing.bind(this); - this.escapeEditing = this.escapeEditing.bind(this); - this.completeEditing = this.completeEditing.bind(this); this.handleRowSelect = this.handleRowSelect.bind(this); this.handleAllRowsSelect = this.handleAllRowsSelect.bind(this); this.state = { - data: this.store.get(), - selectedRowKeys: this.store.getSelectedRowKeys(), - currEditCell: { - ridx: null, - cidx: null - } + data: props.store.get(), + selectedRowKeys: props.store.getSelectedRowKeys() }; } + componentWillReceiveProps(nextProps) { + this.setState({ + data: nextProps.store.get() + }); + } + render() { const { + store, columns, keyField, striped, @@ -54,9 +50,10 @@ class BootstrapTable extends PropsBaseResolver(Component) { }); const cellEditInfo = this.resolveCellEditProps({ - onStart: this.startEditing, - onEscape: this.escapeEditing, - onComplete: this.completeEditing + onStart: this.props.onStartEditing, + onEscape: this.props.onEscapeEditing, + onUpdate: this.props.onCellUpdate, + currEditCell: this.props.currEditCell }); const cellSelectionInfo = this.resolveCellSelectionProps({ @@ -64,7 +61,9 @@ class BootstrapTable extends PropsBaseResolver(Component) { }); const headerCellSelectionInfo = this.resolveHeaderCellSelectionProps({ - onAllRowsSelect: this.handleAllRowsSelect + onAllRowsSelect: this.handleAllRowsSelect, + selected: store.selected, + allRowsSelected: store.isAllRowsSelected() }); return ( @@ -73,8 +72,8 @@ class BootstrapTable extends PropsBaseResolver(Component) { { caption }
@@ -100,10 +99,10 @@ class BootstrapTable extends PropsBaseResolver(Component) { * @param {Boolean} checked - next checked status of input button. */ handleRowSelect(rowKey, checked) { - const { mode } = this.props.selectRow; + const { selectRow: { mode }, store } = this.props; const { ROW_SELECT_SINGLE } = Const; - let currSelected = [...this.store.getSelectedRowKeys()]; + let currSelected = [...store.getSelectedRowKeys()]; if (mode === ROW_SELECT_SINGLE) { // when select mode is radio currSelected = [rowKey]; @@ -113,7 +112,7 @@ class BootstrapTable extends PropsBaseResolver(Component) { currSelected = currSelected.filter(value => value !== rowKey); } - this.store.setSelectedRowKeys(currSelected); + store.setSelectedRowKeys(currSelected); this.setState(() => ({ selectedRowKeys: currSelected @@ -125,14 +124,15 @@ class BootstrapTable extends PropsBaseResolver(Component) { * @param {Boolean} option - customized result for all rows selection */ handleAllRowsSelect(option) { - const selected = this.store.isAnySelectedRow(); + const { store } = this.props; + const selected = store.isAnySelectedRow(); // set next status of all row selected by store.selected or customizing by user. const result = option || !selected; - const currSelected = result ? this.store.selectAllRowKeys() : []; + const currSelected = result ? store.selectAllRowKeys() : []; - this.store.setSelectedRowKeys(currSelected); + store.setSelectedRowKeys(currSelected); this.setState(() => ({ selectedRowKeys: currSelected @@ -140,44 +140,12 @@ class BootstrapTable extends PropsBaseResolver(Component) { } handleSort(column) { - this.store.sortBy(column); + const { store } = this.props; + store.sortBy(column); this.setState(() => { return { - data: this.store.get() - }; - }); - } - - completeEditing(row, column, newValue) { - const { cellEdit, keyField } = this.props; - const { beforeSaveCell, onEditing, afterSaveCell } = cellEdit; - const oldValue = _.get(row, column.dataField); - const rowId = _.get(row, keyField); - if (_.isFunction(beforeSaveCell)) beforeSaveCell(oldValue, newValue, row, column); - onEditing(rowId, column.dataField, newValue); - if (_.isFunction(afterSaveCell)) afterSaveCell(oldValue, newValue, row, column); - - this.setState(() => { - return { - data: this.store.get(), - currEditCell: { ridx: null, cidx: null } - }; - }); - } - - startEditing(ridx, cidx) { - this.setState(() => { - return { - currEditCell: { ridx, cidx } - }; - }); - } - - escapeEditing() { - this.setState(() => { - return { - currEditCell: { ridx: null, cidx: null } + data: store.get() }; }); } @@ -199,15 +167,27 @@ BootstrapTable.propTypes = { ]), cellEdit: PropTypes.shape({ mode: PropTypes.oneOf([Const.CLICK_TO_CELL_EDIT, Const.DBCLICK_TO_CELL_EDIT]).isRequired, - onEditing: PropTypes.func.isRequired, + onUpdate: PropTypes.func, + onErrorMessageDisappear: PropTypes.func, blurToSave: PropTypes.bool, beforeSaveCell: PropTypes.func, afterSaveCell: PropTypes.func, nonEditableRows: PropTypes.func, - timeToCloseMessage: PropTypes.number + editing: PropTypes.bool, + timeToCloseMessage: PropTypes.number, + errorMessage: PropTypes.string }), selectRow: PropTypes.shape({ mode: PropTypes.oneOf([Const.ROW_SELECT_SINGLE, Const.ROW_SELECT_MULTIPLE]).isRequired + }), + onCellUpdate: PropTypes.func, + onStartEditing: PropTypes.func, + onEscapeEditing: PropTypes.func, + currEditCell: PropTypes.shape({ + ridx: PropTypes.number, + cidx: PropTypes.number, + message: PropTypes.string, + editing: PropTypes.bool }) }; diff --git a/packages/react-bootstrap-table2/src/cell-edit-wrapper.js b/packages/react-bootstrap-table2/src/cell-edit-wrapper.js new file mode 100644 index 0000000..404d796 --- /dev/null +++ b/packages/react-bootstrap-table2/src/cell-edit-wrapper.js @@ -0,0 +1,105 @@ +/* eslint arrow-body-style: 0 */ +/* eslint react/prop-types: 0 */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import _ from './utils'; + +class CellEditWrapper extends Component { + constructor(props) { + super(props); + this.startEditing = this.startEditing.bind(this); + this.escapeEditing = this.escapeEditing.bind(this); + this.completeEditing = this.completeEditing.bind(this); + this.handleCellUpdate = this.handleCellUpdate.bind(this); + this.updateEditingWithErr = this.updateEditingWithErr.bind(this); + this.state = { + ridx: null, + cidx: null, + message: null, + editing: false + }; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.cellEdit) { + if (nextProps.cellEdit.editing) { + this.setState(() => { + return { + ...this.state, + message: nextProps.cellEdit.errorMessage + }; + }); + } else { + this.escapeEditing(); + } + } + } + + handleCellUpdate(row, column, newValue) { + const { keyField, cellEdit, onUpdateCell } = this.props; + const { beforeSaveCell, afterSaveCell } = cellEdit; + const oldValue = _.get(row, column.dataField); + const rowId = _.get(row, keyField); + if (_.isFunction(beforeSaveCell)) beforeSaveCell(oldValue, newValue, row, column); + if (onUpdateCell(rowId, column.dataField, newValue)) { + if (_.isFunction(afterSaveCell)) afterSaveCell(oldValue, newValue, row, column); + this.completeEditing(); + } + } + + completeEditing() { + this.setState(() => { + return { + ridx: null, + cidx: null, + message: null, + editing: false + }; + }); + } + + startEditing(ridx, cidx) { + this.setState(() => { + return { + ridx, + cidx, + editing: true + }; + }); + } + + escapeEditing() { + this.setState(() => { + return { + ridx: null, + cidx: null, + editing: false + }; + }); + } + + updateEditingWithErr(message) { + this.setState(() => { + return { + ...this.state, + message + }; + }); + } + + render() { + return React.cloneElement(this.props.elem, { + onCellUpdate: this.handleCellUpdate, + onStartEditing: this.startEditing, + onEscapeEditing: this.escapeEditing, + currEditCell: { ...this.state } + }); + } +} + +CellEditWrapper.propTypes = { + elem: PropTypes.element.isRequired, + onUpdateCell: PropTypes.func.isRequired +}; + +export default CellEditWrapper; diff --git a/packages/react-bootstrap-table2/src/editing-cell.js b/packages/react-bootstrap-table2/src/editing-cell.js index 97320c9..339426c 100644 --- a/packages/react-bootstrap-table2/src/editing-cell.js +++ b/packages/react-bootstrap-table2/src/editing-cell.js @@ -23,6 +23,15 @@ class EditingCell extends Component { }; } + componentWillReceiveProps({ message }) { + if (_.isDefined(message)) { + this.createTimer(); + this.setState(() => { + return { invalidMessage: message }; + }); + } + } + componentWillUnmount() { this.clearTimer(); } @@ -33,24 +42,30 @@ class EditingCell extends Component { } } - beforeComplete(row, column, newValue) { + createTimer() { this.clearTimer(); - const { onComplete, timeToCloseMessage } = this.props; + const { timeToCloseMessage, onErrorMessageDisappear } = this.props; + this.indicatorTimer = _.sleep(() => { + this.setState(() => { + return { invalidMessage: null }; + }); + if (_.isFunction(onErrorMessageDisappear)) onErrorMessageDisappear(); + }, timeToCloseMessage); + } + + beforeComplete(row, column, newValue) { + const { onUpdate } = this.props; if (_.isFunction(column.validator)) { const validateForm = column.validator(newValue, row, column); if (_.isObject(validateForm) && !validateForm.valid) { this.setState(() => { return { invalidMessage: validateForm.message }; }); - this.indicatorTimer = setTimeout(() => { - this.setState(() => { - return { invalidMessage: null }; - }); - }, timeToCloseMessage); + this.createTimer(); return; } } - onComplete(row, column, newValue); + onUpdate(row, column, newValue); } handleBlur() { @@ -90,7 +105,8 @@ class EditingCell extends Component { onBlur: this.handleBlur }; - const editorClass = invalidMessage ? cs('animated', 'shake') : null; + const hasError = _.isDefined(invalidMessage); + const editorClass = hasError ? cs('animated', 'shake') : null; return ( - { invalidMessage ? : null } + { hasError ? : null } ); } @@ -108,7 +124,7 @@ class EditingCell extends Component { EditingCell.propTypes = { row: PropTypes.object.isRequired, column: PropTypes.object.isRequired, - onComplete: PropTypes.func.isRequired, + onUpdate: PropTypes.func.isRequired, onEscape: PropTypes.func.isRequired, timeToCloseMessage: PropTypes.number }; diff --git a/packages/react-bootstrap-table2/src/props-resolver/index.js b/packages/react-bootstrap-table2/src/props-resolver/index.js index f01790c..5e227f8 100644 --- a/packages/react-bootstrap-table2/src/props-resolver/index.js +++ b/packages/react-bootstrap-table2/src/props-resolver/index.js @@ -18,13 +18,12 @@ export default ExtendBase => return this.props.data.length === 0; } - resolveCellEditProps(options) { + resolveCellEditProps(options = { currEditCell: null }) { const { cellEdit } = this.props; - const { currEditCell } = this.state; const nonEditableRows = (cellEdit && _.isFunction(cellEdit.nonEditableRows)) ? cellEdit.nonEditableRows() : []; const cellEditInfo = { - ...currEditCell, + ...options.currEditCell, nonEditableRows }; @@ -72,9 +71,9 @@ export default ExtendBase => * @returns {String} result.mode - input type of row selection or disabled. * @returns {String} result.checkedStatus - checkbox status depending on selected rows counts */ - resolveHeaderCellSelectionProps(options) { - const { selected } = this.store; + resolveHeaderCellSelectionProps(options = {}) { const { selectRow } = this.props; + const { allRowsSelected, selected = [], ...rest } = options; const { ROW_SELECT_DISABLED, CHECKBOX_STATUS_CHECKED, CHECKBOX_STATUS_INDETERMINATE, CHECKBOX_STATUS_UNCHECKED @@ -83,8 +82,6 @@ export default ExtendBase => if (_.isDefined(selectRow)) { let checkedStatus; - const allRowsSelected = this.store.isAllRowsSelected(); - // checkbox status depending on selected rows counts if (allRowsSelected) checkedStatus = CHECKBOX_STATUS_CHECKED; else if (selected.length === 0) checkedStatus = CHECKBOX_STATUS_UNCHECKED; @@ -92,7 +89,7 @@ export default ExtendBase => return { ...selectRow, - ...options, + ...rest, checkedStatus }; } diff --git a/packages/react-bootstrap-table2/src/stateful-layer.js b/packages/react-bootstrap-table2/src/stateful-layer.js index c6a0133..8ddfa77 100644 --- a/packages/react-bootstrap-table2/src/stateful-layer.js +++ b/packages/react-bootstrap-table2/src/stateful-layer.js @@ -1,26 +1,72 @@ /* eslint arrow-body-style: 0 */ +/* eslint react/jsx-no-bind: 0 */ +/* eslint no-return-assign: 0 */ +/* eslint react/prop-types: 0 */ import React, { Component } from 'react'; import Store from './store/base'; +import CellEditWrapper from './cell-edit-wrapper'; +import _ from './utils'; const withStateful = (Base) => { class StatefulComponent extends Component { constructor(props) { super(props); this.store = new Store(props); - this.edit = this.edit.bind(this); + this.handleUpdateCell = this.handleUpdateCell.bind(this); } - edit(rowId, dataField, newValue) { - this.store.edit(rowId, dataField, newValue); + componentWillReceiveProps(nextProps) { + this.store.set(nextProps.data); + } + + handleUpdateCell(rowId, dataField, newValue) { + const { cellEdit } = this.props; + // handle cell editing internal + if (!cellEdit.onUpdate) { + this.store.edit(rowId, dataField, newValue); + return true; + } + + // handle cell editing external + const aPromise = cellEdit.onUpdate(rowId, dataField, newValue); + if (_.isDefined(aPromise) && aPromise !== false) { // TODO: should be a promise here + aPromise.then((result = true) => { + const response = result === true ? {} : result; + if (_.isObject(response)) { + const { value } = response; + this.store.edit(rowId, dataField, value || newValue); + this.table.completeEditing(); + } + }).catch((e) => { + this.table.updateEditingWithErr(e.message); + }); + } + return false; + } + + renderCellEdit(elem) { + return ( + this.table = node } + elem={ elem } + onUpdateCell={ this.handleUpdateCell } + /> + ); } render() { - const { props } = this; - const newProps = { ...props }; - if (newProps.cellEdit && !newProps.cellEdit.onEditing) { - newProps.cellEdit.onEditing = this.edit; + const baseProps = { + ...this.props, + store: this.store + }; + + let element = React.createElement(Base, baseProps); + if (this.props.cellEdit) { + element = this.renderCellEdit(element); } - return ; + return element; } } return StatefulComponent; diff --git a/packages/react-bootstrap-table2/src/store/base.js b/packages/react-bootstrap-table2/src/store/base.js index 31a96cd..6006861 100644 --- a/packages/react-bootstrap-table2/src/store/base.js +++ b/packages/react-bootstrap-table2/src/store/base.js @@ -1,3 +1,4 @@ +/* eslint class-methods-use-this: 0 */ import { sort } from './sort'; import Const from '../const'; import _ from '../utils'; @@ -6,7 +7,7 @@ export default class Store { constructor(props) { const { data, keyField } = props; this.keyField = keyField; - this.data = data ? data.slice() : []; + this.set(data); this.sortOrder = undefined; this.sortField = undefined; @@ -37,6 +38,10 @@ export default class Store { return this.data; } + set(data) { + this.data = data ? JSON.parse(JSON.stringify(data)) : []; + } + getRowByRowId(rowId) { return this.get().find(row => _.get(row, this.keyField) === rowId); } diff --git a/packages/react-bootstrap-table2/src/utils.js b/packages/react-bootstrap-table2/src/utils.js index bc4febf..4360d0d 100644 --- a/packages/react-bootstrap-table2/src/utils.js +++ b/packages/react-bootstrap-table2/src/utils.js @@ -68,11 +68,16 @@ function isDefined(value) { return typeof value !== 'undefined' && value !== null; } +function sleep(fn, ms) { + return setTimeout(() => fn(), ms); +} + export default { get, set, isFunction, isObject, isEmptyObject, - isDefined + isDefined, + sleep }; diff --git a/packages/react-bootstrap-table2/test/bootstrap-table.test.js b/packages/react-bootstrap-table2/test/bootstrap-table.test.js index 2ee2b96..95cc99e 100644 --- a/packages/react-bootstrap-table2/test/bootstrap-table.test.js +++ b/packages/react-bootstrap-table2/test/bootstrap-table.test.js @@ -3,6 +3,7 @@ import sinon from 'sinon'; import { shallow } from 'enzyme'; import Caption from '../src/caption'; +import Store from '../src/store/base'; import Header from '../src/header'; import Body from '../src/body'; import BootstrapTable from '../src/bootstrap-table'; @@ -26,9 +27,12 @@ describe('BootstrapTable', () => { name: 'B' }]; + const store = new Store({ data }); + describe('simplest table', () => { beforeEach(() => { - wrapper = shallow(); + wrapper = shallow( + ); }); it('should render successfully', () => { @@ -40,9 +44,8 @@ describe('BootstrapTable', () => { }); it('should have correct default state', () => { - expect(wrapper.state().currEditCell).toBeDefined(); - expect(wrapper.state().currEditCell.ridx).toBeNull(); - expect(wrapper.state().currEditCell.cidx).toBeNull(); + expect(wrapper.state().data).toBeDefined(); + expect(wrapper.state().data).toEqual(store.get()); }); it('should have table-bordered class as default', () => { @@ -52,7 +55,8 @@ describe('BootstrapTable', () => { describe('when hover props is true', () => { beforeEach(() => { - wrapper = shallow(); + wrapper = shallow( + ); }); it('should have table-hover class on table', () => { @@ -62,7 +66,8 @@ describe('BootstrapTable', () => { describe('when striped props is true', () => { beforeEach(() => { - wrapper = shallow(); + wrapper = shallow( + ); }); it('should have table-striped class on table', () => { @@ -72,7 +77,8 @@ describe('BootstrapTable', () => { describe('when condensed props is true', () => { beforeEach(() => { - wrapper = shallow(); + wrapper = shallow( + ); }); it('should have table-condensed class on table', () => { @@ -82,7 +88,8 @@ describe('BootstrapTable', () => { describe('when bordered props is false', () => { beforeEach(() => { - wrapper = shallow(); + wrapper = shallow( + ); }); it('should not have table-condensed class on table', () => { @@ -94,6 +101,7 @@ describe('BootstrapTable', () => { beforeEach(() => { wrapper = shallow( test } keyField="id" columns={ columns } @@ -111,6 +119,12 @@ describe('BootstrapTable', () => { describe('when cellEdit props is defined', () => { const nonEditableRows = [data[1].id]; + const currEditCell = { + ridx: 1, + cidx: 2, + message: null, + editing: false + }; const cellEdit = { mode: Const.CLICK_TO_CELL_EDIT, onEditing: sinon.stub(), @@ -124,7 +138,12 @@ describe('BootstrapTable', () => { columns={ columns } data={ data } bordered={ false } + store={ store } cellEdit={ cellEdit } + onCellUpdate={ sinon.stub() } + onStartEditing={ sinon.stub() } + onEscapeEditing={ sinon.stub() } + currEditCell={ currEditCell } /> ); }); @@ -133,11 +152,13 @@ describe('BootstrapTable', () => { const body = wrapper.find(Body); expect(body.length).toBe(1); expect(body.props().cellEdit.nonEditableRows).toEqual(nonEditableRows); - expect(body.props().cellEdit.ridx).toEqual(wrapper.state().currEditCell.ridx); - expect(body.props().cellEdit.cidx).toEqual(wrapper.state().currEditCell.cidx); + expect(body.props().cellEdit.ridx).toEqual(currEditCell.ridx); + expect(body.props().cellEdit.cidx).toEqual(currEditCell.cidx); + expect(body.props().cellEdit.message).toEqual(currEditCell.message); + expect(body.props().cellEdit.editing).toEqual(currEditCell.editing); expect(body.props().cellEdit.onStart).toBeDefined(); expect(body.props().cellEdit.onEscape).toBeDefined(); - expect(body.props().cellEdit.onComplete).toBeDefined(); + expect(body.props().cellEdit.onUpdate).toBeDefined(); }); }); @@ -149,6 +170,7 @@ describe('BootstrapTable', () => { wrapper = shallow( { wrapper = shallow( { wrapper = shallow( @@ -208,7 +232,7 @@ describe('BootstrapTable', () => { describe('when customized option was not given', () => { describe('when nothing was selected', () => { it('should select all rows', () => { - wrapper.instance().store.setSelectedRowKeys([]); + store.setSelectedRowKeys([]); wrapper.instance().handleAllRowsSelect(); @@ -218,7 +242,7 @@ describe('BootstrapTable', () => { describe('when one or more than one row was selected', () => { it('should unselect all rows', () => { - wrapper.instance().store.setSelectedRowKeys([1]); + store.setSelectedRowKeys([1]); wrapper.instance().handleAllRowsSelect(); @@ -238,7 +262,7 @@ describe('BootstrapTable', () => { describe('when option is falsy', () => { it('should unselect all rows', () => { - wrapper.instance().store.setSelectedRowKeys([1]); + store.setSelectedRowKeys([1]); wrapper.instance().handleAllRowsSelect(false); diff --git a/packages/react-bootstrap-table2/test/cell-edit-wrapper.test.js b/packages/react-bootstrap-table2/test/cell-edit-wrapper.test.js new file mode 100644 index 0000000..cfea0cf --- /dev/null +++ b/packages/react-bootstrap-table2/test/cell-edit-wrapper.test.js @@ -0,0 +1,268 @@ +import React from 'react'; +import sinon from 'sinon'; +import { shallow } from 'enzyme'; + +import Store from '../src/store/base'; +import BootstrapTable from '../src/bootstrap-table'; +import CellEditWrapper from '../src/cell-edit-wrapper'; + +describe('CellEditWrapper', () => { + let wrapper; + let elem; + + const columns = [{ + dataField: 'id', + text: 'ID' + }, { + dataField: 'name', + text: 'Name' + }]; + + const data = [{ + id: 1, + name: 'A' + }, { + id: 2, + name: 'B' + }]; + + const cellEdit = { + mode: 'click' + }; + + const keyField = 'id'; + + const store = new Store({ data, keyField }); + + beforeEach(() => { + elem = React.createElement(BootstrapTable, { data, cellEdit, columns, keyField, store }); + wrapper = shallow( + + ); + }); + + it('should render CellEditWrapper correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(BootstrapTable)).toBeDefined(); + }); + + it('should have correct state', () => { + expect(wrapper.state().ridx).toBeNull(); + expect(wrapper.state().cidx).toBeNull(); + expect(wrapper.state().message).toBeNull(); + expect(wrapper.state().editing).toBeFalsy(); + }); + + it('should inject correct props to elem', () => { + expect(wrapper.props().onCellUpdate).toBeDefined(); + expect(wrapper.props().onStartEditing).toBeDefined(); + expect(wrapper.props().onEscapeEditing).toBeDefined(); + expect(wrapper.props().currEditCell).toBeDefined(); + expect(wrapper.props().currEditCell.ridx).toBeNull(); + expect(wrapper.props().currEditCell.cidx).toBeNull(); + expect(wrapper.props().currEditCell.message).toBeNull(); + expect(wrapper.props().currEditCell.editing).toBeFalsy(); + }); + + describe('when receive new cellEdit prop', () => { + const spy = jest.spyOn(CellEditWrapper.prototype, 'escapeEditing'); + + describe('and cellEdit.editing is false', () => { + beforeEach(() => { + elem = React.createElement(BootstrapTable, { data, cellEdit, columns, keyField, store }); + wrapper = shallow( + + ); + wrapper.setProps({ cellEdit: { ...cellEdit, editing: false } }); + }); + + it('should call escapeEditing', () => { + expect(spy).toHaveBeenCalled(); + }); + + it('should have correct state', () => { + expect(wrapper.state().ridx).toBeNull(); + expect(wrapper.state().cidx).toBeNull(); + expect(wrapper.state().message).toBeNull(); + expect(wrapper.state().editing).toBeFalsy(); + }); + }); + + describe('and cellEdit.editing is true', () => { + const errorMessage = 'test'; + const ridx = 1; + const cidx = 2; + + beforeEach(() => { + elem = React.createElement(BootstrapTable, { data, cellEdit, columns, keyField, store }); + wrapper = shallow( + + ); + wrapper.setState({ ridx, cidx, editing: true }); + wrapper.setProps({ cellEdit: { ...cellEdit, editing: true, errorMessage } }); + }); + + it('should have correct state', () => { + expect(wrapper.state().ridx).toEqual(ridx); + expect(wrapper.state().cidx).toEqual(cidx); + expect(wrapper.state().editing).toBeTruthy(); + expect(wrapper.state().message).toEqual(errorMessage); + }); + }); + }); + + describe('call updateEditingWithErr function', () => { + it('should set state.message correctly', () => { + const message = 'test'; + wrapper.instance().updateEditingWithErr(message); + expect(wrapper.state().message).toEqual(message); + }); + }); + + describe('call escapeEditing function', () => { + it('should set state correctly', () => { + wrapper.instance().escapeEditing(); + expect(wrapper.state().ridx).toBeNull(); + expect(wrapper.state().cidx).toBeNull(); + expect(wrapper.state().editing).toBeFalsy(); + }); + }); + + describe('call startEditing function', () => { + it('should set state correctly', () => { + const ridx = 1; + const cidx = 3; + wrapper.instance().startEditing(ridx, cidx); + expect(wrapper.state().ridx).toEqual(ridx); + expect(wrapper.state().cidx).toEqual(cidx); + expect(wrapper.state().editing).toBeTruthy(); + }); + }); + + describe('call completeEditing function', () => { + it('should set state correctly', () => { + wrapper.instance().completeEditing(); + expect(wrapper.state().ridx).toBeNull(); + expect(wrapper.state().cidx).toBeNull(); + expect(wrapper.state().message).toBeNull(); + expect(wrapper.state().editing).toBeFalsy(); + }); + }); + + describe('call handleCellUpdate function', () => { + let onUpdateCellCallBack; + const row = data[0]; + const column = columns[1]; + const newValue = 'new name'; + + beforeEach(() => { + onUpdateCellCallBack = sinon.stub().returns(true); + wrapper = shallow( + + ); + wrapper.instance().handleCellUpdate(row, column, newValue); + }); + + afterEach(() => { onUpdateCellCallBack.reset(); }); + + it('should calling onUpdateCell callback correctly', () => { + expect(onUpdateCellCallBack.callCount).toBe(1); + expect(onUpdateCellCallBack.calledWith(row.id, column.dataField, newValue)).toBe(true); + }); + + describe('when onUpdateCell function return true', () => { + const spy = jest.spyOn(CellEditWrapper.prototype, 'completeEditing'); + + it('should calling completeEditing function', () => { + expect(spy).toHaveBeenCalled(); + }); + + describe('if cellEdit.afterSaveCell prop defined', () => { + const aftereSaveCellCallBack = sinon.stub(); + beforeEach(() => { + cellEdit.beforeSaveCell = aftereSaveCellCallBack; + wrapper = shallow( + + ); + wrapper.instance().handleCellUpdate(row, column, newValue); + }); + + it('should calling cellEdit.afterSaveCell correctly', () => { + expect(aftereSaveCellCallBack.callCount).toBe(1); + expect(aftereSaveCellCallBack.calledWith( + row[column.dataField], newValue, row, column) + ).toBe(true); + }); + }); + }); + + describe('when onUpdateCell function return false', () => { + const spy = jest.spyOn(CellEditWrapper.prototype, 'completeEditing'); + + beforeEach(() => { + onUpdateCellCallBack = sinon.stub().returns(false); + wrapper = shallow( + + ); + wrapper.instance().handleCellUpdate(row, column, newValue); + }); + + it('shouldn\'t calling completeEditing function', () => { + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('if cellEdit.beforeSaveCell prop defined', () => { + const beforeSaveCellCallBack = sinon.stub(); + beforeEach(() => { + cellEdit.beforeSaveCell = beforeSaveCellCallBack; + wrapper = shallow( + + ); + wrapper.instance().handleCellUpdate(row, column, newValue); + }); + + it('should calling cellEdit.beforeSaveCell correctly', () => { + expect(beforeSaveCellCallBack.callCount).toBe(1); + expect(beforeSaveCellCallBack.calledWith( + row[column.dataField], newValue, row, column) + ).toBe(true); + }); + }); + }); +}); diff --git a/packages/react-bootstrap-table2/test/editing-cell.test.js b/packages/react-bootstrap-table2/test/editing-cell.test.js index 864b7c4..9523f60 100644 --- a/packages/react-bootstrap-table2/test/editing-cell.test.js +++ b/packages/react-bootstrap-table2/test/editing-cell.test.js @@ -10,8 +10,8 @@ import EditorIndicator from '../src/editor-indicator'; describe('EditingCell', () => { let wrapper; + let onUpdate; let onEscape; - let onComplete; const row = { id: 1, name: 'A' @@ -23,14 +23,14 @@ describe('EditingCell', () => { }; beforeEach(() => { - onComplete = sinon.stub(); onEscape = sinon.stub(); + onUpdate = sinon.stub(); wrapper = shallow( ); }); @@ -55,12 +55,12 @@ describe('EditingCell', () => { expect(indicator.length).toEqual(0); }); - it('when press ENTER on TextEditor should call onComplete correctly', () => { + it('when press ENTER on TextEditor should call onUpdate correctly', () => { const newValue = 'test'; const textEditor = wrapper.find(TextEditor); textEditor.simulate('keyDown', { keyCode: 13, currentTarget: { value: newValue } }); - expect(onComplete.callCount).toBe(1); - expect(onComplete.calledWith(row, column, newValue)).toBe(true); + expect(onUpdate.callCount).toBe(1); + expect(onUpdate.calledWith(row, column, newValue)).toBe(true); }); it('when press ESC on TextEditor should call onEscape correctly', () => { @@ -82,19 +82,19 @@ describe('EditingCell', () => { ); }); - it('when blur from TextEditor should call onComplete correctly', () => { + it('when blur from TextEditor should call onUpdate correctly', () => { const textEditor = wrapper.find(TextEditor); textEditor.simulate('blur'); - expect(onComplete.callCount).toBe(1); - expect(onComplete.calledWith(row, column, `${row[column.dataField]}`)).toBe(true); + expect(onUpdate.callCount).toBe(1); + expect(onUpdate.calledWith(row, column, `${row[column.dataField]}`)).toBe(true); }); }); @@ -121,8 +121,8 @@ describe('EditingCell', () => { expect(validatorCallBack.calledWith(newValue, row, column)).toBe(true); }); - it('should not call onComplete', () => { - expect(onComplete.callCount).toBe(0); + it('should not call onUpdate', () => { + expect(onUpdate.callCount).toBe(0); }); it('should set indicatorTimer successfully', () => { @@ -164,8 +164,8 @@ describe('EditingCell', () => { expect(validatorCallBack.calledWith(newValue, row, column)).toBe(true); }); - it('should call onComplete', () => { - expect(onComplete.callCount).toBe(1); + it('should call onUpdate', () => { + expect(onUpdate.callCount).toBe(1); }); }); }); diff --git a/packages/react-bootstrap-table2/test/props-resolver/index.test.js b/packages/react-bootstrap-table2/test/props-resolver/index.test.js index e865126..840db69 100644 --- a/packages/react-bootstrap-table2/test/props-resolver/index.test.js +++ b/packages/react-bootstrap-table2/test/props-resolver/index.test.js @@ -86,18 +86,6 @@ describe('TableResolver', () => { expect(cellEdit).toBeDefined(); expect(cellEdit.mode).toEqual(Const.UNABLE_TO_CELL_EDIT); expect(cellEdit.nonEditableRows.length).toEqual(0); - expect(cellEdit.ridx).toBeNull(); - expect(cellEdit.cidx).toBeNull(); - }); - - it('should resolve a default cellEdit instance even if state.currEditCell changed', () => { - const ridx = 1; - const cidx = 1; - wrapper.setState({ currEditCell: { ridx, cidx } }); - const cellEdit = wrapper.instance().resolveCellEditProps(); - expect(cellEdit).toBeDefined(); - expect(cellEdit.ridx).toEqual(ridx); - expect(cellEdit.cidx).toEqual(cidx); }); }); @@ -105,7 +93,7 @@ describe('TableResolver', () => { const expectNonEditableRows = [1, 2]; const cellEdit = { mode: Const.DBCLICK_TO_CELL_EDIT, - onEditing: sinon.stub(), + onUpdate: sinon.stub(), blurToSave: true, beforeSaveCell: sinon.stub(), afterSaveCell: sinon.stub(), @@ -122,10 +110,8 @@ describe('TableResolver', () => { it('should resolve a cellEdit correctly', () => { const cellEditInfo = wrapper.instance().resolveCellEditProps(); expect(cellEditInfo).toBeDefined(); - expect(cellEditInfo.ridx).toBeNull(); - expect(cellEditInfo.cidx).toBeNull(); expect(cellEditInfo.mode).toEqual(cellEdit.mode); - expect(cellEditInfo.onEditing).toEqual(cellEdit.onEditing); + expect(cellEditInfo.onUpdate).toEqual(cellEdit.onUpdate); expect(cellEditInfo.blurToSave).toEqual(cellEdit.blurToSave); expect(cellEditInfo.beforeSaveCell).toEqual(cellEdit.beforeSaveCell); expect(cellEditInfo.afterSaveCell).toEqual(cellEdit.afterSaveCell); @@ -280,7 +266,9 @@ describe('TableResolver', () => { selectRow = {}; const mockOptions = { foo: 'test', - bar: sinon.stub() + bar: sinon.stub(), + allRowsSelected: false, + selected: [] }; const selectedRowKeys = []; const mockElement = React.createElement(BootstrapTableMock, { @@ -290,12 +278,20 @@ describe('TableResolver', () => { headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps(mockOptions); }); - it('should return object which contain options', () => { + it('should return object which contain specified options', () => { expect(headerCellSelectionInfo).toEqual(expect.objectContaining({ foo: 'test', bar: expect.any(Function) })); }); + + it('should return object which can not contain allRowsSelected option', () => { + expect(headerCellSelectionInfo.allRowsSelected).not.toBeDefined(); + }); + + it('should return object which can not contain allRowsSelected option', () => { + expect(headerCellSelectionInfo.selected).not.toBeDefined(); + }); }); describe('if all rows were selected', () => { @@ -307,9 +303,11 @@ describe('TableResolver', () => { }, null); wrapper = shallow(mockElement); - wrapper.instance().store.setSelectedRowKeys(selectedRowKeys); - headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps(); + headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps({ + allRowsSelected: true, + selected: selectedRowKeys + }); }); it('should return checkedStatus which eqauls to checked', () => { @@ -318,6 +316,7 @@ describe('TableResolver', () => { })); }); }); + describe('if part of rows were selected', () => { beforeEach(() => { selectRow = {}; @@ -327,8 +326,10 @@ describe('TableResolver', () => { }, null); wrapper = shallow(mockElement); - wrapper.instance().store.setSelectedRowKeys(selectedRowKeys); - headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps(); + headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps({ + allRowsSelected: false, + selected: selectedRowKeys + }); }); it('should return checkedStatus which eqauls to indeterminate', () => { @@ -347,9 +348,11 @@ describe('TableResolver', () => { }, null); wrapper = shallow(mockElement); - wrapper.instance().store.setSelectedRowKeys(selectedRowKeys); - headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps(); + headerCellSelectionInfo = wrapper.instance().resolveHeaderCellSelectionProps({ + allRowsSelected: false, + selected: selectedRowKeys + }); }); it('should return checkedStatus which eqauls to unchecked', () => { diff --git a/packages/react-bootstrap-table2/test/row.test.js b/packages/react-bootstrap-table2/test/row.test.js index cdbf885..c03848a 100644 --- a/packages/react-bootstrap-table2/test/row.test.js +++ b/packages/react-bootstrap-table2/test/row.test.js @@ -225,8 +225,9 @@ describe('Row', () => { beforeEach(() => { cellEdit.ridx = rowIndex; cellEdit.cidx = editingColIndex; - cellEdit.onComplete = sinon.stub(); + cellEdit.onUpdate = sinon.stub(); cellEdit.onEscape = sinon.stub(); + cellEdit.onUpdate = sinon.stub(); wrapper = shallow( { beforeEach(() => { cellEdit.ridx = 3; cellEdit.cidx = editingColIndex; - cellEdit.onComplete = sinon.stub(); + cellEdit.onUpdate = sinon.stub(); cellEdit.onEscape = sinon.stub(); wrapper = shallow( { + let wrapper; + + const keyField = 'id'; + + const columns = [{ + dataField: keyField, + text: 'ID' + }, { + dataField: 'name', + text: 'Name' + }]; + + const data = [{ + id: 1, + name: 'A' + }, { + id: 2, + name: 'B' + }]; + + describe('initialization', () => { + beforeEach(() => { + wrapper = shallow( + + ); + }); + + it('should render BootstrapTable successfully', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(BootstrapTable).length).toBe(1); + }); + + it('should creating store successfully', () => { + const store = wrapper.instance().store; + expect(store).toBeDefined(); + expect(store.get()).toEqual(data); + expect(store.keyField).toEqual(keyField); + }); + }); + + describe('when cellEdit is defined', () => { + const spy = jest.spyOn(BootstrapTableful.prototype, 'renderCellEdit'); + const cellEdit = { + mode: 'click' + }; + + beforeEach(() => { + wrapper = shallow( + + ); + }); + + it('should calling renderCellEdit function', () => { + expect(spy).toHaveBeenCalled(); + }); + + it('should injecting correct props', () => { + expect(wrapper.props().keyField).toEqual('id'); + expect(wrapper.props().cellEdit).toEqual(cellEdit); + expect(wrapper.props().elem).toBeDefined(); + expect(wrapper.props().onUpdateCell).toBeDefined(); + }); + + describe('for handleUpdateCell function', () => { + const rowId = data[1].id; + const dataField = columns[1].dataField; + const newValue = 'tester'; + let result; + + describe('when cellEdit.onUpdate callback is not defined', () => { + beforeEach(() => { + result = wrapper.instance().handleUpdateCell(rowId, dataField, newValue); + }); + + it('should return true', () => { + expect(result).toBeTruthy(); + }); + + it('should update store data directly', () => { + const store = wrapper.instance().store; + const row = store.getRowByRowId(rowId); + expect(row[dataField]).toEqual(newValue); + }); + }); + + describe('when cellEdit.onUpdate callback is define and which return false', () => { + beforeEach(() => { + cellEdit.onUpdate = sinon.stub().returns(false); + wrapper = shallow( + + ); + result = wrapper.instance().handleUpdateCell(rowId, dataField, newValue); + }); + + it('should calling cellEdit.onUpdate callback correctly', () => { + expect(cellEdit.onUpdate.callCount).toBe(1); + expect(cellEdit.onUpdate.calledWith(rowId, dataField, newValue)).toBe(true); + }); + + it('should return false', () => { + expect(result).toBeFalsy(); + }); + + it('shouldn\'t update store data', () => { + const store = wrapper.instance().store; + const row = store.getRowByRowId(rowId); + expect(row[dataField]).not.toEqual(newValue); + }); + }); + + // We need refactoring handleUpdateCell function for handling promise firstly + // then it will be much easier to test + describe.skip('when cellEdit.onUpdate callback is define and which return a Promise', () => {}); + }); + }); +}); diff --git a/packages/react-bootstrap-table2/test/test-helpers/mock-component.js b/packages/react-bootstrap-table2/test/test-helpers/mock-component.js index ca7114d..f09890e 100644 --- a/packages/react-bootstrap-table2/test/test-helpers/mock-component.js +++ b/packages/react-bootstrap-table2/test/test-helpers/mock-component.js @@ -8,13 +8,7 @@ export const extendTo = Base => const { data } = props; this.store = new Store(props); - this.state = { - data, - currEditCell: { - ridx: null, - cidx: null - } - }; + this.state = { data }; } render() { return null; }