From a50a12426a4331effc1623d0d078c29ff43c25d9 Mon Sep 17 00:00:00 2001 From: AllenFang Date: Thu, 4 Jan 2018 23:19:13 +0800 Subject: [PATCH 1/3] refine remote cell edit --- .../src/wrapper.js | 16 +++++-- .../src/bootstrap-table.js | 5 +- .../src/cell-edit/wrapper.js | 46 ++++++++++--------- .../react-bootstrap-table2/src/container.js | 39 ++-------------- .../src/props-resolver/remote-resolver.js | 12 ++++- .../react-bootstrap-table2/src/store/index.js | 4 ++ 6 files changed, 58 insertions(+), 64 deletions(-) diff --git a/packages/react-bootstrap-table2-filter/src/wrapper.js b/packages/react-bootstrap-table2-filter/src/wrapper.js index 780c4d8..ad08b82 100644 --- a/packages/react-bootstrap-table2-filter/src/wrapper.js +++ b/packages/react-bootstrap-table2-filter/src/wrapper.js @@ -1,3 +1,5 @@ +/* eslint no-param-reassign: 0 */ + import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { filters } from './filter'; @@ -15,14 +17,20 @@ export default (Base, { constructor(props) { super(props); - this.state = { currFilters: {}, isDataChanged: false }; + this.state = { currFilters: {}, isDataChanged: props.isDataChanged || false }; this.onFilter = this.onFilter.bind(this); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps({ isDataChanged, store, columns }) { // consider to use lodash.isEqual - if (JSON.stringify(this.state.currFilters) !== JSON.stringify(nextProps.store.filters)) { - this.setState(() => ({ isDataChanged: true, currFilters: nextProps.store.filters })); + if (JSON.stringify(this.state.currFilters) !== JSON.stringify(store.filters)) { + this.setState(() => ({ isDataChanged: true, currFilters: store.filters })); + } else if (isDataChanged) { + if (!(this.isRemoteFiltering() || this.isRemotePagination()) && + Object.keys(this.state.currFilters).length > 0) { + store.filteredData = filters(store, columns, _)(this.state.currFilters); + } + this.setState(() => ({ isDataChanged })); } else { this.setState(() => ({ isDataChanged: false })); } diff --git a/packages/react-bootstrap-table2/src/bootstrap-table.js b/packages/react-bootstrap-table2/src/bootstrap-table.js index 19e7afa..aeed455 100644 --- a/packages/react-bootstrap-table2/src/bootstrap-table.js +++ b/packages/react-bootstrap-table2/src/bootstrap-table.js @@ -130,13 +130,11 @@ BootstrapTable.propTypes = { filter: PropTypes.object, cellEdit: PropTypes.shape({ mode: PropTypes.oneOf([Const.CLICK_TO_CELL_EDIT, Const.DBCLICK_TO_CELL_EDIT]).isRequired, - onUpdate: PropTypes.func, onErrorMessageDisappear: PropTypes.func, blurToSave: PropTypes.bool, beforeSaveCell: PropTypes.func, afterSaveCell: PropTypes.func, nonEditableRows: PropTypes.func, - editing: PropTypes.bool, timeToCloseMessage: PropTypes.number, errorMessage: PropTypes.string }), @@ -146,8 +144,7 @@ BootstrapTable.propTypes = { currEditCell: PropTypes.shape({ ridx: PropTypes.number, cidx: PropTypes.number, - message: PropTypes.string, - editing: PropTypes.bool + message: PropTypes.string }), selectRow: PropTypes.shape({ mode: PropTypes.oneOf([Const.ROW_SELECT_SINGLE, Const.ROW_SELECT_MULTIPLE]).isRequired, diff --git a/packages/react-bootstrap-table2/src/cell-edit/wrapper.js b/packages/react-bootstrap-table2/src/cell-edit/wrapper.js index ec62fd2..7f21e53 100644 --- a/packages/react-bootstrap-table2/src/cell-edit/wrapper.js +++ b/packages/react-bootstrap-table2/src/cell-edit/wrapper.js @@ -1,44 +1,54 @@ /* eslint react/prop-types: 0 */ import React, { Component } from 'react'; import _ from '../utils'; +import remoteResolver from '../props-resolver/remote-resolver'; -export default (Base, parentProps) => - class CellEditWrapper extends Component { +export default Base => + class CellEditWrapper extends remoteResolver(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 + isDataChanged: false }; } componentWillReceiveProps(nextProps) { - if (nextProps.cellEdit) { - if (nextProps.cellEdit.editing) { + if (nextProps.cellEdit && this.isRemoteCellEdit()) { + if (nextProps.cellEdit.errorMessage) { this.setState(() => ({ - ...this.state, + isDataChanged: false, message: nextProps.cellEdit.errorMessage })); } else { + this.setState(() => ({ + isDataChanged: true + })); this.escapeEditing(); } + } else { + this.setState(() => ({ + isDataChanged: false + })); } } handleCellUpdate(row, column, newValue) { - const { keyField, cellEdit } = this.props; + const { keyField, cellEdit, store } = 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 (parentProps.onUpdateCell(rowId, column.dataField, newValue)) { + if (this.isRemoteCellEdit()) { + this.handleCellChange(rowId, column.dataField, newValue); + } else { + store.edit(rowId, column.dataField, newValue); if (_.isFunction(afterSaveCell)) afterSaveCell(oldValue, newValue, row, column); this.completeEditing(); } @@ -49,7 +59,7 @@ export default (Base, parentProps) => ridx: null, cidx: null, message: null, - editing: false + isDataChanged: true })); } @@ -58,7 +68,7 @@ export default (Base, parentProps) => this.setState(() => ({ ridx, cidx, - editing: true + isDataChanged: false })); }; @@ -69,27 +79,21 @@ export default (Base, parentProps) => escapeEditing() { this.setState(() => ({ ridx: null, - cidx: null, - editing: false - })); - } - - updateEditingWithErr(message) { - this.setState(() => ({ - ...this.state, - message + cidx: null })); } render() { + const { isDataChanged, ...rest } = this.state; return ( ); } diff --git a/packages/react-bootstrap-table2/src/container.js b/packages/react-bootstrap-table2/src/container.js index 04fb908..30fe195 100644 --- a/packages/react-bootstrap-table2/src/container.js +++ b/packages/react-bootstrap-table2/src/container.js @@ -16,11 +16,10 @@ const withDataStore = Base => this.store = new Store(props.keyField); this.store.data = props.data; this.wrapComponents(); - this.handleUpdateCell = this.handleUpdateCell.bind(this); } componentWillReceiveProps(nextProps) { - this.store.data = nextProps.data; + this.store.setAllData(nextProps.data); } wrapComponents() { @@ -45,41 +44,13 @@ const withDataStore = Base => }); } + if (cellEdit) { + this.BaseComponent = withCellEdit(this.BaseComponent); + } + if (selectRow) { this.BaseComponent = withSelection(this.BaseComponent); } - - if (cellEdit) { - this.BaseComponent = withCellEdit(this.BaseComponent, { - ref: node => this.cellEditWrapper = node, - onUpdateCell: this.handleUpdateCell - }); - } - } - - 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.cellEditWrapper.completeEditing(); - } - }).catch((e) => { - this.cellEditWrapper.updateEditingWithErr(e.message); - }); - } - return false; } render() { diff --git a/packages/react-bootstrap-table2/src/props-resolver/remote-resolver.js b/packages/react-bootstrap-table2/src/props-resolver/remote-resolver.js index b59c9e3..6a44e57 100644 --- a/packages/react-bootstrap-table2/src/props-resolver/remote-resolver.js +++ b/packages/react-bootstrap-table2/src/props-resolver/remote-resolver.js @@ -10,7 +10,7 @@ export default ExtendBase => filters: store.filters, sortField: store.sortField, sortOrder: store.sortOrder, - data: store.data, + data: store.getAllData(), ...state }; } @@ -30,6 +30,11 @@ export default ExtendBase => return remote === true || (_.isObject(remote) && remote.sort); } + isRemoteCellEdit() { + const { remote } = this.props; + return remote === true || (_.isObject(remote) && remote.cellEdit); + } + handleRemotePageChange() { this.props.onTableChange('pagination', this.getNewestState()); } @@ -46,4 +51,9 @@ export default ExtendBase => handleSortChange() { this.props.onTableChange('sort', this.getNewestState()); } + + handleCellChange(rowId, dataField, newValue) { + const cellEdit = { rowId, dataField, newValue }; + this.props.onTableChange('cellEdit', this.getNewestState({ cellEdit })); + } }; diff --git a/packages/react-bootstrap-table2/src/store/index.js b/packages/react-bootstrap-table2/src/store/index.js index 9c6e108..6711c99 100644 --- a/packages/react-bootstrap-table2/src/store/index.js +++ b/packages/react-bootstrap-table2/src/store/index.js @@ -34,6 +34,10 @@ export default class Store { return this._data; } + setAllData(data) { + this._data = data; + } + get data() { if (Object.keys(this._filters).length > 0) { return this._filteredData; From beafef9661383ad795fac56ab545ee888463efe6 Mon Sep 17 00:00:00 2001 From: AllenFang Date: Thu, 4 Jan 2018 23:21:17 +0800 Subject: [PATCH 2/3] patch tests for refining remote cell edit --- .../cell-edit/cell-edit-with-promise-table.js | 75 ------- .../cell-edit/cell-edit-with-redux-table.js | 212 ------------------ .../examples/remote/remote-celledit.js | 165 ++++++++++++++ .../stories/index.js | 8 +- .../test/wrapper.test.js | 16 +- .../test/cell-edit/wrapper.test.js | 206 +++++++++-------- .../test/container.test.js | 59 ----- .../test/props-resolver/index.test.js | 1 - .../props-resolver/remote-resolver.test.js | 52 +++++ .../react-bootstrap-table2/test/row.test.js | 1 - 10 files changed, 351 insertions(+), 444 deletions(-) delete mode 100644 packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-promise-table.js delete mode 100644 packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-redux-table.js create mode 100644 packages/react-bootstrap-table2-example/examples/remote/remote-celledit.js 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 deleted file mode 100644 index c66e3bd..0000000 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-promise-table.js +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint no-unused-vars: 0 */ -/* eslint arrow-body-style: 0 */ -import React, { Component } from 'react'; - -import BootstrapTable 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 deleted file mode 100644 index 760c6d9..0000000 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-with-redux-table.js +++ /dev/null @@ -1,212 +0,0 @@ -/* 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 BootstrapTable 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/examples/remote/remote-celledit.js b/packages/react-bootstrap-table2-example/examples/remote/remote-celledit.js new file mode 100644 index 0000000..312c81e --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/remote/remote-celledit.js @@ -0,0 +1,165 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +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 sourceCode = `\ +const RemoteCellEdit = (props) => { + const cellEdit = { + mode: 'click', + errorMessage: props.errorMessage + }; + + return ( +
+ + { sourceCode } +
+ ); +}; + +RemoteCellEdit.propTypes = { + data: PropTypes.array.isRequired, + onTableChange: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired +}; + +class Container extends React.Component { + constructor(props) { + super(props); + this.state = { + data: products, + errorMessage: null + }; + } + + handleTableChange = (type, { data, cellEdit: { rowId, dataField, newValue } }) => { + setTimeout(() => { + if (newValue === 'test' && dataField === 'name') { + this.setState(() => ({ + data, + errorMessage: 'Oops, product name shouldn't be "test"' + })); + } else { + const result = data.map((row) => { + if (row.id === rowId) { + const newRow = { ...row }; + newRow[dataField] = newValue; + return newRow; + } + return row; + }); + this.setState(() => ({ + data: result, + errorMessage: null + })); + } + }, 2000); + } + + render() { + return ( + + ); + } +} +`; + + +const RemoteCellEdit = (props) => { + const cellEdit = { + mode: 'click', + errorMessage: props.errorMessage + }; + + return ( +
+ + { sourceCode } +
+ ); +}; + +RemoteCellEdit.propTypes = { + data: PropTypes.array.isRequired, + onTableChange: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired +}; + +class Container extends React.Component { + constructor(props) { + super(props); + this.state = { + data: products, + errorMessage: null + }; + } + + handleTableChange = (type, { data, cellEdit: { rowId, dataField, newValue } }) => { + setTimeout(() => { + if (newValue === 'test' && dataField === 'name') { + this.setState(() => ({ + data, + errorMessage: 'Oops, product name shouldn\'t be "test"' + })); + } else { + const result = data.map((row) => { + if (row.id === rowId) { + const newRow = { ...row }; + newRow[dataField] = newValue; + return newRow; + } + return row; + }); + this.setState(() => ({ + data: result, + errorMessage: null + })); + } + }, 2000); + } + + render() { + return ( + + ); + } +} + +export default Container; diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js index 909f5b8..3fe704c 100644 --- a/packages/react-bootstrap-table2-example/stories/index.js +++ b/packages/react-bootstrap-table2-example/stories/index.js @@ -63,8 +63,6 @@ import CellEditHooks from 'examples/cell-edit/cell-edit-hooks-table'; import CellEditValidator from 'examples/cell-edit/cell-edit-validator-table'; import CellEditStyleTable from 'examples/cell-edit/cell-edit-style-table'; import CellEditClassTable from 'examples/cell-edit/cell-edit-class-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'; @@ -91,6 +89,7 @@ import TableOverlay from 'examples/loading-overlay/table-overlay'; import RemoteSort from 'examples/remote/remote-sort'; import RemoteFilter from 'examples/remote/remote-filter'; import RemotePaginationTable from 'examples/remote/remote-pagination'; +import RemoteCellEdit from 'examples/remote/remote-celledit'; import RemoteAll from 'examples/remote/remote-all'; // css style @@ -165,9 +164,7 @@ storiesOf('Cell Editing', module) .add('Rich Hook Functions', () => ) .add('Validation', () => ) .add('Custom Cell Style When Editing', () => ) - .add('Custom Cell Classes When Editing', () => ) - .add('Async Cell Editing(Promise)', () => ) - .add('Async Cell Editing(Redux)', () => ); + .add('Custom Cell Classes When Editing', () => ); storiesOf('Row Selection', module) .add('Single Selection', () => ) @@ -194,4 +191,5 @@ storiesOf('Remote', module) .add('Remote Sort', () => ) .add('Remote Filter', () => ) .add('Remote Pagination', () => ) + .add('Remote Cell Editing', () => ) .add('Remote All', () => ); diff --git a/packages/react-bootstrap-table2-filter/test/wrapper.test.js b/packages/react-bootstrap-table2-filter/test/wrapper.test.js index f250c46..b80cd74 100644 --- a/packages/react-bootstrap-table2-filter/test/wrapper.test.js +++ b/packages/react-bootstrap-table2-filter/test/wrapper.test.js @@ -28,7 +28,7 @@ describe('Wrapper', () => { onTableChangeCB.reset(); }); - const createTableProps = () => { + const createTableProps = (props) => { const tableProps = { keyField: 'id', columns: [{ @@ -47,7 +47,8 @@ describe('Wrapper', () => { filter: filter(), _, store: new Store('id'), - onTableChange: onTableChangeCB + onTableChange: onTableChangeCB, + ...props }; tableProps.store.data = data; return tableProps; @@ -105,6 +106,17 @@ describe('Wrapper', () => { }); }); + describe('when props.isDataChanged is true and remote is enable', () => { + beforeEach(() => { + nextProps = createTableProps({ isDataChanged: true }); + instance.componentWillReceiveProps(nextProps); + }); + + it('should setting isDataChanged as true', () => { + expect(instance.state.isDataChanged).toBeTruthy(); + }); + }); + describe('when props.store.filters is different from current state.currFilters', () => { beforeEach(() => { nextProps = createTableProps(); diff --git a/packages/react-bootstrap-table2/test/cell-edit/wrapper.test.js b/packages/react-bootstrap-table2/test/cell-edit/wrapper.test.js index 8f21f5f..c195b16 100644 --- a/packages/react-bootstrap-table2/test/cell-edit/wrapper.test.js +++ b/packages/react-bootstrap-table2/test/cell-edit/wrapper.test.js @@ -31,13 +31,10 @@ describe('CellEditWrapper', () => { }; const keyField = 'id'; - let onUpdateCellCB = sinon.stub(); const store = new Store(keyField); store.data = data; - const CellEditWrapper = wrapperFactory(Container, { - onUpdateCell: onUpdateCellCB - }); + const CellEditWrapper = wrapperFactory(Container); beforeEach(() => { wrapper = shallow( @@ -60,24 +57,24 @@ describe('CellEditWrapper', () => { expect(wrapper.state().ridx).toBeNull(); expect(wrapper.state().cidx).toBeNull(); expect(wrapper.state().message).toBeNull(); - expect(wrapper.state().editing).toBeFalsy(); + expect(wrapper.state().isDataChanged).toBeFalsy(); }); it('should inject correct props to base component', () => { expect(wrapper.props().onCellUpdate).toBeDefined(); expect(wrapper.props().onStartEditing).toBeDefined(); expect(wrapper.props().onEscapeEditing).toBeDefined(); + expect(wrapper.props().isDataChanged).toBe(wrapper.state().isDataChanged); 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', () => { + describe('and cellEdit is not work on remote', () => { beforeEach(() => { wrapper = shallow( { store={ store } /> ); - wrapper.setProps({ cellEdit: { ...cellEdit, editing: false } }); + wrapper.setProps({ cellEdit: { ...cellEdit } }); }); - 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(); + it('should always setting state.isDataChanged as false', () => { + expect(wrapper.state().isDataChanged).toBeFalsy(); }); }); - describe('and cellEdit.editing is true', () => { - const errorMessage = 'test'; + describe('and cellEdit is work on remote', () => { + let errorMessage; const ridx = 1; const cidx = 2; - beforeEach(() => { - wrapper = shallow( - - ); - wrapper.setState({ ridx, cidx, editing: true }); - wrapper.setProps({ cellEdit: { ...cellEdit, editing: true, errorMessage } }); + describe('and cellEdit.errorMessage is defined', () => { + beforeEach(() => { + wrapper = shallow( + + ); + errorMessage = 'test'; + wrapper.setState({ ridx, cidx }); + wrapper.setProps({ cellEdit: { ...cellEdit, errorMessage } }); + }); + + it('should setting correct state', () => { + expect(wrapper.state().ridx).toEqual(ridx); + expect(wrapper.state().cidx).toEqual(cidx); + expect(wrapper.state().isDataChanged).toBeFalsy(); + expect(wrapper.state().message).toEqual(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('and cellEdit.errorMessage is undefined', () => { + beforeEach(() => { + wrapper = shallow( + + ); + errorMessage = null; + wrapper.setState({ ridx, cidx }); + wrapper.setProps({ cellEdit: { ...cellEdit, errorMessage } }); + }); - describe('call updateEditingWithErr function', () => { - it('should set state.message correctly', () => { - const message = 'test'; - wrapper.instance().updateEditingWithErr(message); - expect(wrapper.state().message).toEqual(message); + it('should setting correct state', () => { + expect(wrapper.state().isDataChanged).toBeTruthy(); + }); + + it('should escape current editing', () => { + expect(spy).toHaveBeenCalled(); + }); + }); }); }); @@ -144,7 +156,6 @@ describe('CellEditWrapper', () => { wrapper.instance().escapeEditing(); expect(wrapper.state().ridx).toBeNull(); expect(wrapper.state().cidx).toBeNull(); - expect(wrapper.state().editing).toBeFalsy(); }); }); @@ -155,7 +166,7 @@ describe('CellEditWrapper', () => { wrapper.instance().startEditing(ridx, cidx); expect(wrapper.state().ridx).toEqual(ridx); expect(wrapper.state().cidx).toEqual(cidx); - expect(wrapper.state().editing).toBeTruthy(); + expect(wrapper.state().isDataChanged).toBeFalsy(); }); describe('if selectRow.clickToSelect is defined', () => { @@ -199,7 +210,6 @@ describe('CellEditWrapper', () => { wrapper.instance().startEditing(ridx, cidx); expect(wrapper.state().ridx).toEqual(ridx); expect(wrapper.state().cidx).toEqual(cidx); - expect(wrapper.state().editing).toBeTruthy(); }); }); }); @@ -210,7 +220,7 @@ describe('CellEditWrapper', () => { expect(wrapper.state().ridx).toBeNull(); expect(wrapper.state().cidx).toBeNull(); expect(wrapper.state().message).toBeNull(); - expect(wrapper.state().editing).toBeFalsy(); + expect(wrapper.state().isDataChanged).toBeTruthy(); }); }); @@ -219,35 +229,75 @@ describe('CellEditWrapper', () => { const column = columns[1]; const newValue = 'new name'; - beforeEach(() => { - wrapper = shallow( - - ); - wrapper.instance().handleCellUpdate(row, column, newValue); + describe('when cell edit is work on remote', () => { + const spy = jest.spyOn(CellEditWrapper.prototype, 'handleCellChange'); + const onTableChangeCB = jest.fn(); + + beforeEach(() => { + wrapper = shallow( + + ); + wrapper.instance().handleCellUpdate(row, column, newValue); + }); + + it('should calling handleCellChange correctly', () => { + expect(spy).toHaveBeenCalled(); + expect(spy.mock.calls).toHaveLength(1); + expect(spy.mock.calls[0]).toHaveLength(3); + expect(spy.mock.calls[0][0]).toEqual(row[keyField]); + expect(spy.mock.calls[0][1]).toEqual(column.dataField); + expect(spy.mock.calls[0][2]).toEqual(newValue); + }); }); - it('should calling onUpdateCell callback correctly', () => { - expect(onUpdateCellCB.callCount).toBe(1); - expect(onUpdateCellCB.calledWith(row.id, column.dataField, newValue)).toBe(true); - }); + describe('when cell edit is not work on remote', () => { + const spyOnCompleteEditing = jest.spyOn(CellEditWrapper.prototype, 'completeEditing'); + const spyOnStoreEdit = jest.spyOn(Store.prototype, 'edit'); - describe('when onUpdateCell function return true', () => { - const spy = jest.spyOn(CellEditWrapper.prototype, 'completeEditing'); + beforeEach(() => { + wrapper = shallow( + + ); + wrapper.instance().handleCellUpdate(row, column, newValue); + }); + + afterEach(() => { + spyOnStoreEdit.mockReset(); + spyOnCompleteEditing.mockReset(); + }); + + it('should calling props.store.edit', () => { + expect(spyOnStoreEdit).toHaveBeenCalled(); + expect(spyOnStoreEdit.mock.calls).toHaveLength(1); + expect(spyOnStoreEdit.mock.calls[0]).toHaveLength(3); + expect(spyOnStoreEdit.mock.calls[0][0]).toEqual(row[keyField]); + expect(spyOnStoreEdit.mock.calls[0][1]).toEqual(column.dataField); + expect(spyOnStoreEdit.mock.calls[0][2]).toEqual(newValue); + }); it('should calling completeEditing function', () => { - expect(spy).toHaveBeenCalled(); + expect(spyOnCompleteEditing).toHaveBeenCalled(); }); describe('if cellEdit.afterSaveCell prop defined', () => { const aftereSaveCellCallBack = sinon.stub(); + beforeEach(() => { - cellEdit.beforeSaveCell = aftereSaveCellCallBack; + cellEdit.afterSaveCell = aftereSaveCellCallBack; wrapper = shallow( { }); }); - describe('when onUpdateCell function return false', () => { - const spy = jest.spyOn(CellEditWrapper.prototype, 'completeEditing'); - - beforeEach(() => { - onUpdateCellCB = 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(() => { diff --git a/packages/react-bootstrap-table2/test/container.test.js b/packages/react-bootstrap-table2/test/container.test.js index 42c67bf..066a948 100644 --- a/packages/react-bootstrap-table2/test/container.test.js +++ b/packages/react-bootstrap-table2/test/container.test.js @@ -1,12 +1,10 @@ /* eslint react/prefer-stateless-function: 0 */ /* eslint react/no-multi-comp: 0 */ import React from 'react'; -import sinon from 'sinon'; import { shallow } from 'enzyme'; import BootstrapTable from '../src/bootstrap-table'; import Container from '../src'; -import { getRowByRowId } from '../src/store/rows'; describe('container', () => { let wrapper; @@ -79,63 +77,6 @@ describe('container', () => { it('should render BootstrapTable component successfully', () => { expect(wrapper.dive().find(BootstrapTable)).toHaveLength(1); }); - - 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 = getRowByRowId(store)(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 = getRowByRowId(store)(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', () => {}); - }); }); describe('when selectRow prop is defined', () => { 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 79eabb4..7f4cf59 100644 --- a/packages/react-bootstrap-table2/test/props-resolver/index.test.js +++ b/packages/react-bootstrap-table2/test/props-resolver/index.test.js @@ -93,7 +93,6 @@ describe('TableResolver', () => { const expectNonEditableRows = [1, 2]; const cellEdit = { mode: Const.DBCLICK_TO_CELL_EDIT, - onUpdate: sinon.stub(), blurToSave: true, beforeSaveCell: sinon.stub(), afterSaveCell: sinon.stub(), diff --git a/packages/react-bootstrap-table2/test/props-resolver/remote-resolver.test.js b/packages/react-bootstrap-table2/test/props-resolver/remote-resolver.test.js index a6e260d..b88760f 100644 --- a/packages/react-bootstrap-table2/test/props-resolver/remote-resolver.test.js +++ b/packages/react-bootstrap-table2/test/props-resolver/remote-resolver.test.js @@ -134,6 +134,58 @@ describe('remoteResolver', () => { }); }); + describe('isRemoteCellEdit', () => { + describe('when remote is false', () => { + beforeEach(() => { + shallowContainer(); + }); + + it('should return false', () => { + expect(wrapper.instance().isRemoteCellEdit()).toBeFalsy(); + }); + }); + + describe('when remote is true', () => { + beforeEach(() => { + shallowContainer({ remote: true }); + }); + + it('should return true', () => { + expect(wrapper.instance().isRemoteCellEdit()).toBeTruthy(); + }); + }); + + describe('when remote.cellEdit is true', () => { + beforeEach(() => { + shallowContainer({ remote: { cellEdit: true } }); + }); + + it('should return true', () => { + expect(wrapper.instance().isRemoteCellEdit()).toBeTruthy(); + }); + }); + }); + + describe('handleCellChange', () => { + const onTableChangeCB = sinon.stub(); + const rowId = 1; + const dataField = 'name'; + const newValue = 'test'; + + beforeEach(() => { + onTableChangeCB.reset(); + shallowContainer({ onTableChange: onTableChangeCB }); + wrapper.instance().handleCellChange(rowId, dataField, newValue); + }); + + it('should calling props.onTableChange correctly', () => { + const cellEdit = { rowId, dataField, newValue }; + expect(onTableChangeCB.calledOnce).toBeTruthy(); + expect(onTableChangeCB.calledWith( + 'cellEdit', wrapper.instance().getNewestState({ cellEdit }))).toBeTruthy(); + }); + }); + describe('handleSortChange', () => { const onTableChangeCB = sinon.stub(); beforeEach(() => { diff --git a/packages/react-bootstrap-table2/test/row.test.js b/packages/react-bootstrap-table2/test/row.test.js index 11ccdd6..a44e52f 100644 --- a/packages/react-bootstrap-table2/test/row.test.js +++ b/packages/react-bootstrap-table2/test/row.test.js @@ -273,7 +273,6 @@ describe('Row', () => { cellEdit.cidx = editingColIndex; cellEdit.onUpdate = sinon.stub(); cellEdit.onEscape = sinon.stub(); - cellEdit.onUpdate = sinon.stub(); wrapper = shallow( Date: Thu, 4 Jan 2018 23:34:17 +0800 Subject: [PATCH 3/3] refined docs for remote cell edit --- docs/README.md | 18 ++++++++++-- docs/cell-edit.md | 71 +---------------------------------------------- 2 files changed, 16 insertions(+), 73 deletions(-) diff --git a/docs/README.md b/docs/README.md index 5023f08..6ed4f43 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,7 +40,13 @@ This is a chance that you can connect to your remote server or database to manip For flexibility reason, you can control what functionality should be handled on remote via a object return: ```js -remote={ { filter: true } } +remote={ { + filter: true, + pagination: true, + filter: true, + sort: true, + cellEdit: true +} } ``` In above case, only column filter will be handled on remote. @@ -53,7 +59,7 @@ A special case for remote pagination: remote={ { pagination: true, filter: false, sort: false } } ``` -In pagination case, even you only specified the paignation need to handle as remote, `react-bootstrap-table2` will handle all the table changes(`filter`, `sort` etc) as remote mode, because `react-bootstrap-table` only know the data of current page, but filtering, searching or sort need to work on overall datas. +In pagination case, even you only specified the paignation need to handle as remote, `react-bootstrap-table2` will handle all the table changes(`filter`, `sort`) as remote mode, because `react-bootstrap-table` only know the data of current page, but filtering, searching or sort need to work on overall datas. ### loading - [Bool] Telling if table is loading or not, for example: waiting data loading, filtering etc. It's **only** valid when [`remote`](#remote) is enabled. @@ -250,6 +256,7 @@ There's only two arguments will be passed to `onTableChange`: `type` and `newSta * `filter` * `pagination` * `sort` +* `cellEdit` Following is a shape of `newState` @@ -260,6 +267,11 @@ Following is a shape of `newState` sortField, // newest sort field sortOrder, // newest sort order filters, // an object which have current filter status per column - data // when you enable remote sort, you may need to base on data to sort if data is filtered/searched + data, // when you enable remote sort, you may need to base on data to sort if data is filtered/searched + cellEdit: { // You can only see this prop when type is cellEdit + rowId, + dataField, + newValue + } } ``` \ No newline at end of file diff --git a/docs/cell-edit.md b/docs/cell-edit.md index 328a94e..051dde8 100644 --- a/docs/cell-edit.md +++ b/docs/cell-edit.md @@ -5,8 +5,6 @@ * [timeToCloseMessage](#timeToCloseMessage) * [beforeSaveCell](#beforeSaveCell) * [afterSaveCell](#afterSaveCell) -* [onUpdate](#onUpdate) -* [editing](#editing) * [errorMessage](#errorMessage) * [onErrorMessageDisappear](#onErrorMessageDisappear) @@ -21,9 +19,7 @@ Following is the shape of `cellEdit` object: 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: () => { ... }, @@ -63,73 +59,8 @@ const cellEdit = { }; ``` -### 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. +This prop is not often used. Only used when you keep the error message in your application state and also handle the cell editing on remote mode. ### cellEdit.onErrorMessageDisappear - [Function] This callback function will be called when error message discard.