diff --git a/.travis.yml b/.travis.yml index bfcdb77..086f638 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,8 @@ node_js: - "6" cache: - yarn: true + directories: + - node_modules branches: only: @@ -13,8 +14,8 @@ branches: # - master - develop -before_install: - - curl -o- -L https://yarnpkg.com/install.sh | bash -s - - export PATH="$HOME/.yarn/bin:$PATH" +# before_install: +# - curl -o- -L https://yarnpkg.com/install.sh | bash -s +# - export PATH="$HOME/.yarn/bin:$PATH" -install: yarn --frozen-lockfile +# install: yarn --frozen-lockfile diff --git a/docs/README.md b/docs/README.md index fd43f93..19330f8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,18 @@ ## Props on BootstrapTable +#### Required +* [keyField (**required**)](#keyField) +* [data (**required**)](#data) +* [columns (**required**)](#columns) + +#### Optional +* [striped](#striped) +* [bordered](#bordered) +* [hover](#hover) +* [condensed](#condensed) +* [cellEdit](#cellEdit) + ### keyField(**required**) - [String] `keyField` is a prop to tell `react-bootstrap-table2` which column is unigue key. @@ -18,4 +30,29 @@ Same as `.table-bordered` class for adding borders on all sides of the table and ### hover - [Bool] Same as `.table-hover` class for adding a hover effect (grey background color) on table rows ### condensed - [Bool] -Same as `.table-condensed` class for makeing a table more compact by cutting cell padding in half \ No newline at end of file +Same as `.table-condensed` class for makeing a table more compact by cutting cell padding in half + +### cellEdit - [Bool] +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, + 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`) diff --git a/docs/columns.md b/docs/columns.md index a517532..8ac781e 100644 --- a/docs/columns.md +++ b/docs/columns.md @@ -25,6 +25,7 @@ Available properties in a column object: * [headerEvents](#headerEvents) * [headerAlign](#headerAlign) * [headerAttrs](#headerAttrs) +* [editable](#editable) Following is a most simplest and basic usage: @@ -411,5 +412,9 @@ Additionally, customize the header attributes by a `2-arguments` callback functi A new `Object` will be the result of element headerAttrs. -#### * Caution -Same as [column.attrs](#attrs), it has lower priority and will be overwrited when other props related to HTML attributes were given. +> Caution: +> Same as [column.attrs](#attrs), it has lower priority and will be +> overwrited when other props related to HTML attributes were given. + +## column.editable - [Bool] +`column.editable` default is true, means every column is editable if you configure [`cellEdit`](./README.md#cellEdit). But you can disable some columns editable via setting `false`. \ No newline at end of file diff --git a/package.json b/package.json index dd2cd5b..4e1840d 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,8 @@ "eslint-plugin-react": "^7.2.1", "html-webpack-plugin": "^2.30.1", "jest": "^20.0.4", + "jsdom": "^11.2.0", + "jsdom-global": "^3.0.2", "lerna": "^2.0.0", "node-sass": "^4.5.3", "react-test-renderer": "^15.6.1", @@ -71,7 +73,7 @@ ], "testEnvironment": "node", "testMatch": [ - "**/test/**/*.js" + "**/test/**/*.test.js" ] } } diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/blur-to-save-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/blur-to-save-table.js new file mode 100644 index 0000000..9452c8f --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/blur-to-save-table.js @@ -0,0 +1,54 @@ +import React from 'react'; + +import { BootstrapTableful } 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 columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const cellEdit = { + mode: 'click', + blurToSave: true +}; + + +`; + +const cellEdit = { + mode: 'click', + blurToSave: true +}; +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-hooks-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-hooks-table.js new file mode 100644 index 0000000..0238ec5 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-hooks-table.js @@ -0,0 +1,58 @@ +/* eslint no-unused-vars: 0 */ +/* eslint no-console: 0 */ +import React from 'react'; + +import { BootstrapTableful } 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 columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const cellEdit = { + mode: 'click', + beforeSaveCell: (oldValue, newValue, row, column) => { console.log('Before Saving Cell!!'); }, + afterSaveCell: (oldValue, newValue, row, column) => { console.log('After Saving Cell!!'); } +}; + + +`; + +const cellEdit = { + mode: 'click', + beforeSaveCell: (oldValue, newValue, row, column) => { console.log('Before Saving Cell!!'); }, + afterSaveCell: (oldValue, newValue, row, column) => { console.log('After Saving Cell!!'); } +}; +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/click-to-edit-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/click-to-edit-table.js new file mode 100644 index 0000000..0030936 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/click-to-edit-table.js @@ -0,0 +1,52 @@ +import React from 'react'; + +import { BootstrapTableful } 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 columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const cellEdit = { + mode: 'click' +}; + + +`; + +const cellEdit = { + mode: 'click' +}; +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/column-level-editable-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/column-level-editable-table.js new file mode 100644 index 0000000..b777486 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/column-level-editable-table.js @@ -0,0 +1,57 @@ +import React from 'react'; + +import { BootstrapTableful } 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', + editable: false +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' + // Product Name column can't be edit anymore + editable: false +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const cellEdit = { + mode: 'click', + blurToSave: true +}; + + +`; + +const cellEdit = { + mode: 'click', + blurToSave: true +}; +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/dbclick-to-edit-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/dbclick-to-edit-table.js new file mode 100644 index 0000000..37a1061 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/dbclick-to-edit-table.js @@ -0,0 +1,52 @@ +import React from 'react'; + +import { BootstrapTableful } 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 columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const cellEdit = { + mode: 'dbclick' +}; + + +`; + +const cellEdit = { + mode: 'dbclick' +}; +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/row-level-editable-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/row-level-editable-table.js new file mode 100644 index 0000000..4033f75 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/row-level-editable-table.js @@ -0,0 +1,57 @@ +import React from 'react'; + +import { BootstrapTableful } 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 columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const cellEdit = { + mode: 'click', + blurToSave: true, + // Product ID: 0, 3 will be non-editable + nonEditableRows: () => [0, 3] +}; + + +`; + +const cellEdit = { + mode: 'click', + blurToSave: true, + nonEditableRows: () => [0, 3] +}; +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js index 8e40fcf..9ad48f2 100644 --- a/packages/react-bootstrap-table2-example/stories/index.js +++ b/packages/react-bootstrap-table2-example/stories/index.js @@ -35,6 +35,14 @@ import HeaderColumnAttrsTable from 'examples/header-columns/column-attrs-table'; import EnableSortTable from 'examples/sort/enable-sort-table'; import CustomSortTable from 'examples/sort/custom-sort-table'; +// cell editing +import ClickToEditTable from 'examples/cell-edit/click-to-edit-table'; +import DoubleClickToEditTable from 'examples/cell-edit/dbclick-to-edit-table'; +import BlurToSaveTable from 'examples/cell-edit/blur-to-save-table'; +import RowLevelEditableTable from 'examples/cell-edit/row-level-editable-table'; +import ColumnLevelEditableTable from 'examples/cell-edit/column-level-editable-table'; +import CellEditHooks from 'examples/cell-edit/cell-edit-hooks-table'; + // css style import 'bootstrap/dist/css/bootstrap.min.css'; import 'stories/stylesheet/tomorrow.min.css'; @@ -77,3 +85,11 @@ storiesOf('Work on Header Columns', module) storiesOf('Sort Table', module) .add('Enable Sort', () => ) .add('Custom Sort Fuction', () => ); + +storiesOf('Cell Editing', module) + .add('Click to Edit', () => ) + .add('DoubleClick to Edit', () => ) + .add('Blur to Save Cell', () => ) + .add('Row Level Editable', () => ) + .add('Column Level Editable', () => ) + .add('Rich Hook Functions', () => ); diff --git a/packages/react-bootstrap-table2/src/body.js b/packages/react-bootstrap-table2/src/body.js index a69af71..76cbd22 100644 --- a/packages/react-bootstrap-table2/src/body.js +++ b/packages/react-bootstrap-table2/src/body.js @@ -13,7 +13,8 @@ const Body = (props) => { keyField, isEmpty, noDataIndication, - visibleColumnSize + visibleColumnSize, + cellEdit } = props; let content; @@ -22,14 +23,21 @@ const Body = (props) => { const indication = _.isFunction(noDataIndication) ? noDataIndication() : noDataIndication; content = ; } else { - content = data.map((row, index) => ( - - )); + content = data.map((row, index) => { + const key = _.get(row, keyField); + const editable = !(cellEdit && cellEdit.nonEditableRows.indexOf(key) > -1); + return ( + + ); + }); } return ( diff --git a/packages/react-bootstrap-table2/src/bootstrap-table.js b/packages/react-bootstrap-table2/src/bootstrap-table.js index 5acfb2f..d7ea363 100644 --- a/packages/react-bootstrap-table2/src/bootstrap-table.js +++ b/packages/react-bootstrap-table2/src/bootstrap-table.js @@ -7,6 +7,8 @@ import Header from './header'; 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) { @@ -16,8 +18,15 @@ class BootstrapTable extends PropsBaseResolver(Component) { 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.state = { - data: this.store.get() + data: this.store.get(), + currEditCell: { + ridx: null, + cidx: null + } }; } @@ -39,6 +48,12 @@ class BootstrapTable extends PropsBaseResolver(Component) { 'table-condensed': condensed }); + const cellEditInfo = this.resolveCellEditProps({ + onStart: this.startEditing, + onEscape: this.escapeEditing, + onComplete: this.completeEditing + }); + return (
@@ -55,6 +70,7 @@ class BootstrapTable extends PropsBaseResolver(Component) { isEmpty={ this.isEmpty() } visibleColumnSize={ this.visibleColumnSize() } noDataIndication={ noDataIndication } + cellEdit={ cellEditInfo } />
@@ -70,6 +86,39 @@ class BootstrapTable extends PropsBaseResolver(Component) { }; }); } + + 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 } + }; + }); + } } BootstrapTable.propTypes = { @@ -81,7 +130,15 @@ BootstrapTable.propTypes = { striped: PropTypes.bool, bordered: PropTypes.bool, hover: PropTypes.bool, - condensed: PropTypes.bool + condensed: PropTypes.bool, + cellEdit: PropTypes.shape({ + mode: PropTypes.oneOf([Const.CLICK_TO_CELL_EDIT, Const.DBCLICK_TO_CELL_EDIT]).isRequired, + onEditing: PropTypes.func.isRequired, + blurToSave: PropTypes.bool, + beforeSaveCell: PropTypes.func, + afterSaveCell: PropTypes.func, + nonEditableRows: PropTypes.func + }) }; BootstrapTable.defaultProps = { diff --git a/packages/react-bootstrap-table2/src/cell.js b/packages/react-bootstrap-table2/src/cell.js index 2126c93..e4431e6 100644 --- a/packages/react-bootstrap-table2/src/cell.js +++ b/packages/react-bootstrap-table2/src/cell.js @@ -1,63 +1,103 @@ -import React from 'react'; +/* eslint react/prop-types: 0 */ +import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import Const from './const'; import _ from './utils'; -const Cell = ({ row, rowIndex, column, columnIndex }) => { - const { - dataField, - hidden, - formatter, - formatExtraData, - style, - classes, - title, - events, - align, - attrs - } = column; - let cellTitle; - let cellStyle = {}; - let content = _.get(row, dataField); - - const cellAttrs = { - ..._.isFunction(attrs) ? attrs(content, row, rowIndex, columnIndex) : attrs, - ...events - }; - - const cellClasses = _.isFunction(classes) - ? classes(content, row, rowIndex, columnIndex) - : classes; - - if (style) { - cellStyle = _.isFunction(style) ? style(content, row, rowIndex, columnIndex) : style; +class Cell extends Component { + constructor(props) { + super(props); + this.handleEditingCell = this.handleEditingCell.bind(this); } - if (title) { - cellTitle = _.isFunction(title) ? title(content, row, rowIndex, columnIndex) : content; - cellAttrs.title = cellTitle; + handleEditingCell(e) { + const { editMode, column, onStart, rowIndex, columnIndex } = this.props; + const { events } = column; + if (events) { + if (editMode === Const.CLICK_TO_CELL_EDIT) { + const customClick = events.onClick; + if (_.isFunction(customClick)) customClick(e); + } else { + const customDbClick = events.onDoubleClick; + if (_.isFunction(customDbClick)) customDbClick(e); + } + } + onStart(rowIndex, columnIndex); } - if (formatter) { - content = column.formatter(content, row, rowIndex, formatExtraData); + render() { + const { + row, + rowIndex, + column, + columnIndex, + editMode, + editable + } = this.props; + const { + dataField, + hidden, + formatter, + formatExtraData, + style, + classes, + title, + events, + align, + attrs + } = column; + let cellTitle; + let cellStyle = {}; + let content = _.get(row, dataField); + + const cellAttrs = { + ..._.isFunction(attrs) ? attrs(content, row, rowIndex, columnIndex) : attrs, + ...events + }; + + const cellClasses = _.isFunction(classes) + ? classes(content, row, rowIndex, columnIndex) + : classes; + + if (style) { + cellStyle = _.isFunction(style) ? style(content, row, rowIndex, columnIndex) : style; + } + + if (title) { + cellTitle = _.isFunction(title) ? title(content, row, rowIndex, columnIndex) : content; + cellAttrs.title = cellTitle; + } + + if (formatter) { + content = column.formatter(content, row, rowIndex, formatExtraData); + } + + if (align) { + cellStyle.textAlign = + _.isFunction(align) ? align(content, row, rowIndex, columnIndex) : align; + } + + if (hidden) { + cellStyle.display = 'none'; + } + + if (cellClasses) cellAttrs.className = cellClasses; + + if (!_.isEmptyObject(cellStyle)) cellAttrs.style = cellStyle; + + if (editable && editMode !== Const.UNABLE_TO_CELL_EDIT) { + if (editMode === Const.CLICK_TO_CELL_EDIT) { // click to edit + cellAttrs.onClick = this.handleEditingCell; + } else { // dbclick to edit + cellAttrs.onDoubleClick = this.handleEditingCell; + } + } + return ( + { content } + ); } - - if (align) { - cellStyle.textAlign = _.isFunction(align) ? align(content, row, rowIndex, columnIndex) : align; - } - - if (hidden) { - cellStyle.display = 'none'; - } - - if (cellClasses) cellAttrs.className = cellClasses; - - if (!_.isEmptyObject(cellStyle)) cellAttrs.style = cellStyle; - - return ( - { content } - ); -}; +} Cell.propTypes = { row: PropTypes.object.isRequired, diff --git a/packages/react-bootstrap-table2/src/const.js b/packages/react-bootstrap-table2/src/const.js index acc75d2..8a812ee 100644 --- a/packages/react-bootstrap-table2/src/const.js +++ b/packages/react-bootstrap-table2/src/const.js @@ -1,4 +1,7 @@ export default { SORT_ASC: 'asc', - SORT_DESC: 'desc' + SORT_DESC: 'desc', + UNABLE_TO_CELL_EDIT: 'none', + CLICK_TO_CELL_EDIT: 'click', + DBCLICK_TO_CELL_EDIT: 'dbclick' }; diff --git a/packages/react-bootstrap-table2/src/editing-cell.js b/packages/react-bootstrap-table2/src/editing-cell.js new file mode 100644 index 0000000..f32e1ad --- /dev/null +++ b/packages/react-bootstrap-table2/src/editing-cell.js @@ -0,0 +1,66 @@ +/* eslint react/prop-types: 0 */ +/* eslint no-return-assign: 0 */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import _ from './utils'; +import TextEditor from './text-editor'; + +class EditingCell extends Component { + constructor(props) { + super(props); + this.handleBlur = this.handleBlur.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + } + + handleBlur() { + const { onEscape, onComplete, blurToSave, row, column } = this.props; + if (blurToSave) { + const value = this.editor.text.value; + if (!_.isDefined(value)) { + // TODO: for other custom or embed editor + } + onComplete(row, column, value); + } else { + onEscape(); + } + } + + handleKeyDown(e) { + const { onEscape, onComplete, row, column } = this.props; + if (e.keyCode === 27) { // ESC + onEscape(); + } else if (e.keyCode === 13) { // ENTER + const value = e.currentTarget.value; + if (!_.isDefined(value)) { + // TODO: for other custom or embed editor + } + onComplete(row, column, value); + } + } + + render() { + const { row, column } = this.props; + const { dataField } = column; + + const value = _.get(row, dataField); + const editorAttrs = { + onKeyDown: this.handleKeyDown, + onBlur: this.handleBlur + }; + return ( + + this.editor = node } defaultValue={ value } { ...editorAttrs } /> + + ); + } +} + +EditingCell.propTypes = { + row: PropTypes.object.isRequired, + column: PropTypes.object.isRequired, + onComplete: PropTypes.func.isRequired, + onEscape: PropTypes.func.isRequired +}; + +export default EditingCell; diff --git a/packages/react-bootstrap-table2/src/props-resolver/index.js b/packages/react-bootstrap-table2/src/props-resolver/index.js index 627f796..d7ea8af 100644 --- a/packages/react-bootstrap-table2/src/props-resolver/index.js +++ b/packages/react-bootstrap-table2/src/props-resolver/index.js @@ -1,4 +1,6 @@ import ColumnResolver from './column-resolver'; +import Const from '../const'; +import _ from '../utils'; export default ExtendBase => class TableResolver extends ColumnResolver(ExtendBase) { @@ -15,4 +17,27 @@ export default ExtendBase => isEmpty() { return this.props.data.length === 0; } + + resolveCellEditProps(options) { + const { cellEdit } = this.props; + const { currEditCell } = this.state; + const nonEditableRows = + (cellEdit && _.isFunction(cellEdit.nonEditableRows)) ? cellEdit.nonEditableRows() : []; + const cellEditInfo = { + ...currEditCell, + nonEditableRows + }; + + if (_.isDefined(cellEdit)) { + return { + ...cellEdit, + ...cellEditInfo, + ...options + }; + } + return { + mode: Const.UNABLE_TO_CELL_EDIT, + ...cellEditInfo + }; + } }; diff --git a/packages/react-bootstrap-table2/src/row.js b/packages/react-bootstrap-table2/src/row.js index 842127c..c0e06b2 100644 --- a/packages/react-bootstrap-table2/src/row.js +++ b/packages/react-bootstrap-table2/src/row.js @@ -1,25 +1,64 @@ +/* eslint react/prop-types: 0 */ import React from 'react'; import PropTypes from 'prop-types'; import _ from './utils'; import Cell from './cell'; +import EditingCell from './editing-cell'; -const Row = ({ row, rowIndex, columns }) => ( - - { - columns.map((column, index) => - ( - - )) - } - -); +const Row = (props) => { + const { + row, + columns, + keyField, + rowIndex, + cellEdit, + editable: editableRow + } = props; + const { + ridx: editingRowIdx, + cidx: editingColIdx, + mode, + onStart, + onEscape, + onComplete, + blurToSave + } = cellEdit; + return ( + + { + columns.map((column, index) => { + let editable = _.isDefined(column.editable) ? column.editable : true; + if (column.dataField === keyField || !editableRow) editable = false; + if (rowIndex === editingRowIdx && index === editingColIdx) { + return ( + + ); + } + return ( + + ); + }) + } + + ); +}; Row.propTypes = { row: PropTypes.object.isRequired, @@ -27,4 +66,8 @@ Row.propTypes = { columns: PropTypes.array.isRequired }; +Row.defaultProps = { + editable: true +}; + export default Row; diff --git a/packages/react-bootstrap-table2/src/stateful-layer.js b/packages/react-bootstrap-table2/src/stateful-layer.js index ba0ad9d..c6a0133 100644 --- a/packages/react-bootstrap-table2/src/stateful-layer.js +++ b/packages/react-bootstrap-table2/src/stateful-layer.js @@ -7,11 +7,20 @@ const withStateful = (Base) => { constructor(props) { super(props); this.store = new Store(props); + this.edit = this.edit.bind(this); + } + + edit(rowId, dataField, newValue) { + this.store.edit(rowId, dataField, newValue); } render() { const { props } = this; - return ; + const newProps = { ...props }; + if (newProps.cellEdit && !newProps.cellEdit.onEditing) { + newProps.cellEdit.onEditing = this.edit; + } + return ; } } return StatefulComponent; diff --git a/packages/react-bootstrap-table2/src/store/base.js b/packages/react-bootstrap-table2/src/store/base.js index c44a5ef..2c92725 100644 --- a/packages/react-bootstrap-table2/src/store/base.js +++ b/packages/react-bootstrap-table2/src/store/base.js @@ -1,9 +1,11 @@ import { sort } from './sort'; import Const from '../const'; +import _ from '../utils'; export default class Store { constructor(props) { - const { data } = props; + const { data, keyField } = props; + this.keyField = keyField; this.data = data ? data.slice() : []; this.sortOrder = undefined; @@ -25,7 +27,16 @@ export default class Store { this.sortField = dataField; } + edit(rowId, dataField, newValue) { + const row = this.getRowByRowId(rowId); + if (row) _.set(row, dataField, newValue); + } + get() { return this.data; } + + getRowByRowId(rowId) { + return this.get().find(row => _.get(row, this.keyField) === rowId); + } } diff --git a/packages/react-bootstrap-table2/src/text-editor.js b/packages/react-bootstrap-table2/src/text-editor.js new file mode 100644 index 0000000..06c9cfe --- /dev/null +++ b/packages/react-bootstrap-table2/src/text-editor.js @@ -0,0 +1,21 @@ +/* eslint no-return-assign: 0 */ +import React, { Component } from 'react'; + +class TextEditor extends Component { + componentDidMount() { + this.text.focus(); + } + + render() { + return ( + this.text = node } + type="text" + className="form-control editor edit-text" + { ...this.props } + /> + ); + } +} + +export default TextEditor; diff --git a/packages/react-bootstrap-table2/src/utils.js b/packages/react-bootstrap-table2/src/utils.js index 780be47..bc4febf 100644 --- a/packages/react-bootstrap-table2/src/utils.js +++ b/packages/react-bootstrap-table2/src/utils.js @@ -1,11 +1,16 @@ /* eslint no-empty: 0 */ +/* eslint no-param-reassign: 0 */ -function get(target, field) { - const pathArray = [field] +function splitNested(str) { + return [str] .join('.') .replace(/\[/g, '.') .replace(/\]/g, '') .split('.'); +} + +function get(target, field) { + const pathArray = splitNested(field); let result; try { result = pathArray.reduce((curr, path) => curr[path], target); @@ -13,6 +18,25 @@ function get(target, field) { return result; } +function set(target, field, value, safe = false) { + const pathArray = splitNested(field); + let level = 0; + pathArray.reduce((a, b) => { + level += 1; + if (typeof a[b] === 'undefined') { + if (!safe) throw new Error(`${a}.${b} is undefined`); + a[b] = {}; + return a[b]; + } + + if (level === pathArray.length) { + a[b] = value; + return value; + } + return a[b]; + }, target); +} + function isFunction(obj) { return obj && (typeof obj === 'function'); } @@ -46,6 +70,7 @@ function isDefined(value) { export default { get, + set, isFunction, isObject, isEmptyObject, diff --git a/packages/react-bootstrap-table2/style/react-bootstrap-table.scss b/packages/react-bootstrap-table2/style/react-bootstrap-table.scss index 3a8a126..91cc609 100644 --- a/packages/react-bootstrap-table2/style/react-bootstrap-table.scss +++ b/packages/react-bootstrap-table2/style/react-bootstrap-table.scss @@ -1,5 +1,9 @@ .react-bootstrap-table-container { + table { + table-layout: fixed; + } + th.sortable { cursor: pointer; } diff --git a/packages/react-bootstrap-table2/test/body.test.js b/packages/react-bootstrap-table2/test/body.test.js index e1bb741..54ba506 100644 --- a/packages/react-bootstrap-table2/test/body.test.js +++ b/packages/react-bootstrap-table2/test/body.test.js @@ -4,6 +4,7 @@ import { shallow } from 'enzyme'; import Body from '../src/body'; import Row from '../src/row'; +import Const from '../src/const'; import RowSection from '../src/row-section'; describe('Body', () => { @@ -111,4 +112,35 @@ describe('Body', () => { }); }); }); + + describe('when cellEdit.nonEditableRows props is defined', () => { + const nonEditableRows = [data[1].id]; + const keyField = 'id'; + const cellEdit = { + mode: Const.CLICK_TO_CELL_EDIT, + nonEditableRows + }; + beforeEach(() => { + wrapper = shallow( + + ); + }); + + it('should render Row component with correct editable prop', () => { + expect(wrapper.length).toBe(1); + const rows = wrapper.find(Row); + for (let i = 0; i < rows.length; i += 1) { + if (nonEditableRows.indexOf(rows.get(i).props.row[keyField]) > -1) { + expect(rows.get(i).props.editable).toBeFalsy(); + } else { + expect(rows.get(i).props.editable).toBeTruthy(); + } + } + }); + }); }); diff --git a/packages/react-bootstrap-table2/test/bootstrap-table.test.js b/packages/react-bootstrap-table2/test/bootstrap-table.test.js index c712b72..a475549 100644 --- a/packages/react-bootstrap-table2/test/bootstrap-table.test.js +++ b/packages/react-bootstrap-table2/test/bootstrap-table.test.js @@ -1,9 +1,11 @@ import React from 'react'; +import sinon from 'sinon'; import { shallow } from 'enzyme'; import Header from '../src/header'; import Body from '../src/body'; import BootstrapTable from '../src/bootstrap-table'; +import Const from '../src/const'; describe('BootstrapTable', () => { let wrapper; @@ -36,6 +38,12 @@ describe('BootstrapTable', () => { expect(wrapper.find('.react-bootstrap-table-container').length).toBe(1); }); + it('should have correct default state', () => { + expect(wrapper.state().currEditCell).toBeDefined(); + expect(wrapper.state().currEditCell.ridx).toBeNull(); + expect(wrapper.state().currEditCell.cidx).toBeNull(); + }); + it('should have table-bordered class as default', () => { expect(wrapper.find('table.table-bordered').length).toBe(1); }); @@ -80,4 +88,36 @@ describe('BootstrapTable', () => { expect(wrapper.find('table.table-condensed').length).toBe(0); }); }); + + describe('when cellEdit props is defined', () => { + const nonEditableRows = [data[1].id]; + const cellEdit = { + mode: Const.CLICK_TO_CELL_EDIT, + onEditing: sinon.stub(), + nonEditableRows: () => nonEditableRows + }; + + beforeEach(() => { + wrapper = shallow( + + ); + }); + + it('should resolve correct cellEdit object to Body component', () => { + 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.onStart).toBeDefined(); + expect(body.props().cellEdit.onEscape).toBeDefined(); + expect(body.props().cellEdit.onComplete).toBeDefined(); + }); + }); }); diff --git a/packages/react-bootstrap-table2/test/cell.test.js b/packages/react-bootstrap-table2/test/cell.test.js index 21e7400..49d9458 100644 --- a/packages/react-bootstrap-table2/test/cell.test.js +++ b/packages/react-bootstrap-table2/test/cell.test.js @@ -2,6 +2,7 @@ import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; +import Const from '../src/const'; import Cell from '../src/cell'; describe('Cell', () => { @@ -447,4 +448,98 @@ describe('Cell', () => { }); }); }); + + describe('when editable prop is true', () => { + let onStartCallBack; + const rowIndex = 1; + const columnIndex = 1; + const column = { + dataField: 'id', + text: 'ID' + }; + + beforeEach(() => { + onStartCallBack = sinon.stub().withArgs(rowIndex, columnIndex); + }); + + describe(`and editMode is ${Const.CLICK_TO_CELL_EDIT}`, () => { + beforeEach(() => { + wrapper = shallow( + + ); + }); + + it('should render onClick attribute', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find('td').prop('onClick')).toBeDefined(); + }); + + it('should call onStart correctly when clicking cell', () => { + wrapper.find('td').simulate('click'); + expect(onStartCallBack.callCount).toBe(1); + expect(onStartCallBack.calledWith(rowIndex, columnIndex)).toBe(true); + }); + + describe('if when column.events.onClick prop is defined', () => { + beforeEach(() => { + column.events = { + onClick: sinon.stub() + }; + }); + it('should calling custom onClick callback also', () => { + wrapper.find('td').simulate('click'); + expect(onStartCallBack.callCount).toBe(1); + expect(column.events.onClick.callCount).toBe(1); + }); + }); + }); + + describe(`and editMode is ${Const.DBCLICK_TO_CELL_EDIT}`, () => { + beforeEach(() => { + wrapper = shallow( + + ); + }); + + it('should render onDoubleClick attribute', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find('td').prop('onDoubleClick')).toBeDefined(); + }); + + it('should call onStart correctly when double clicking cell', () => { + wrapper.find('td').simulate('doubleclick'); + expect(onStartCallBack.callCount).toBe(1); + expect(onStartCallBack.calledWith(rowIndex, columnIndex)).toBe(true); + }); + + describe('if when column.events.onDoubleClick prop is defined', () => { + beforeEach(() => { + column.events = { + onDoubleClick: sinon.stub() + }; + }); + it('should calling custom onDoubleClick callback also', () => { + wrapper.find('td').simulate('doubleclick'); + expect(onStartCallBack.callCount).toBe(1); + expect(column.events.onDoubleClick.callCount).toBe(1); + }); + }); + }); + }); }); diff --git a/packages/react-bootstrap-table2/test/editing-cell.test.js b/packages/react-bootstrap-table2/test/editing-cell.test.js new file mode 100644 index 0000000..9bf0f19 --- /dev/null +++ b/packages/react-bootstrap-table2/test/editing-cell.test.js @@ -0,0 +1,93 @@ +import 'jsdom-global/register'; +import React from 'react'; +import sinon from 'sinon'; +import { shallow, mount } from 'enzyme'; + +import { TableRowWrapper } from './test-helpers/table-wrapper'; +import EditingCell from '../src/editing-cell'; +import TextEditor from '../src/text-editor'; + + +describe('EditingCell', () => { + let wrapper; + let onEscape; + let onComplete; + const row = { + id: 1, + name: 'A' + }; + + const column = { + dataField: 'id', + text: 'ID' + }; + + beforeEach(() => { + onComplete = sinon.stub(); + onEscape = sinon.stub(); + wrapper = shallow( + + ); + }); + + it('should render default editor successfully', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find('td').length).toBe(1); + expect(wrapper.find(TextEditor).length).toBe(1); + }); + + it('should render TextEditor with correct props', () => { + const textEditor = wrapper.find(TextEditor); + expect(textEditor.props().defaultValue).toEqual(row[column.dataField]); + expect(textEditor.props().onKeyDown).toBeDefined(); + expect(textEditor.props().onBlur).toBeDefined(); + }); + + it('when press ENTER on TextEditor should call onComplete 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); + }); + + it('when press ESC on TextEditor should call onEscape correctly', () => { + const textEditor = wrapper.find(TextEditor); + textEditor.simulate('keyDown', { keyCode: 27 }); + expect(onEscape.callCount).toBe(1); + }); + + it('when blur from TextEditor should call onEscape correctly', () => { + const textEditor = wrapper.find(TextEditor); + textEditor.simulate('blur'); + expect(onEscape.callCount).toBe(1); + }); + + describe('if blurToSave prop is true', () => { + beforeEach(() => { + wrapper = mount( + + + + ); + }); + + it('when blur from TextEditor should call onComplete correctly', () => { + const textEditor = wrapper.find(TextEditor); + textEditor.simulate('blur'); + expect(onComplete.callCount).toBe(1); + expect(onComplete.calledWith(row, column, `${row[column.dataField]}`)).toBe(true); + }); + }); +}); diff --git a/packages/react-bootstrap-table2/test/props-resolver/index.test.js b/packages/react-bootstrap-table2/test/props-resolver/index.test.js new file mode 100644 index 0000000..d27897a --- /dev/null +++ b/packages/react-bootstrap-table2/test/props-resolver/index.test.js @@ -0,0 +1,146 @@ +import React, { Component } from 'react'; +import sinon from 'sinon'; +import { shallow } from 'enzyme'; + +import { extendTo } from '../test-helpers/mock-component'; +import baseResolver from '../../src/props-resolver/index'; +import Const from '../../src/const'; + +describe('TableResolver', () => { + const keyField = 'id'; + const columns = [{ + dataField: keyField, + text: 'ID' + }, { + dataField: 'name', + text: 'Name' + }]; + const data = [{ + id: 1, + name: 'A' + }, { + id: 2, + name: 'B' + }]; + const ExtendBase = baseResolver(Component); + const BootstrapTableMock = extendTo(ExtendBase); + let wrapper; + + describe('validateProps', () => { + describe('if keyField is defined and columns is all visible', () => { + beforeEach(() => { + const mockElement = React.createElement(BootstrapTableMock, { + data, columns, keyField + }, null); + wrapper = shallow(mockElement); + }); + + it('should not throw any errors', () => { + expect(() => wrapper.instance().validateProps()).not.toThrow(); + }); + }); + + describe('if keyField is not defined on props', () => { + beforeEach(() => { + const mockElement = React.createElement(BootstrapTableMock, { + data, columns + }, null); + wrapper = shallow(mockElement); + }); + + it('should throw error', () => { + expect(() => + wrapper.instance().validateProps() + ).toThrow(new Error('Please specify a field as key via keyField')); + }); + }); + + describe('if columns is all unvisible', () => { + beforeEach(() => { + const mockElement = React.createElement(BootstrapTableMock, { + data, keyField, columns: [] + }, null); + wrapper = shallow(mockElement); + }); + + it('should throw error', () => { + expect(() => + wrapper.instance().validateProps() + ).toThrow(new Error('No any visible columns detect')); + }); + }); + }); + + describe('resolveCellEditProps', () => { + describe('if cellEdit prop not defined', () => { + beforeEach(() => { + const mockElement = React.createElement(BootstrapTableMock, { + data, keyField, columns + }, null); + wrapper = shallow(mockElement); + }); + + it('should resolve a default cellEdit instance', () => { + const cellEdit = wrapper.instance().resolveCellEditProps(); + 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); + }); + }); + }); + + describe('if cellEdit prop defined', () => { + const expectNonEditableRows = [1, 2]; + const cellEdit = { + mode: Const.DBCLICK_TO_CELL_EDIT, + onEditing: sinon.stub(), + blurToSave: true, + beforeSaveCell: sinon.stub(), + afterSaveCell: sinon.stub(), + nonEditableRows: sinon.stub().returns(expectNonEditableRows) + }; + + beforeEach(() => { + const mockElement = React.createElement(BootstrapTableMock, { + data, keyField, columns, cellEdit + }, null); + wrapper = shallow(mockElement); + }); + + 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.blurToSave).toEqual(cellEdit.blurToSave); + expect(cellEditInfo.beforeSaveCell).toEqual(cellEdit.beforeSaveCell); + expect(cellEditInfo.afterSaveCell).toEqual(cellEdit.afterSaveCell); + expect(cellEditInfo.nonEditableRows).toEqual(expectNonEditableRows); + }); + + it('should attach options to cellEdit props', () => { + const something = { + test: 1, + cb: sinon.stub() + }; + const cellEditInfo = wrapper.instance().resolveCellEditProps(something); + expect(cellEditInfo).toBeDefined(); + expect(cellEditInfo.test).toEqual(something.test); + expect(cellEditInfo.cb).toEqual(something.cb); + }); + }); +}); diff --git a/packages/react-bootstrap-table2/test/row.test.js b/packages/react-bootstrap-table2/test/row.test.js index 0b111cc..84e54c3 100644 --- a/packages/react-bootstrap-table2/test/row.test.js +++ b/packages/react-bootstrap-table2/test/row.test.js @@ -1,27 +1,36 @@ import React from 'react'; +import sinon from 'sinon'; import { shallow } from 'enzyme'; import Cell from '../src/cell'; import Row from '../src/row'; +import Const from '../src/const'; +import EditingCell from '../src/editing-cell'; + +const defaultColumns = [{ + dataField: 'id', + text: 'ID' +}, { + dataField: 'name', + text: 'Name' +}, { + dataField: 'price', + text: 'Price' +}]; describe('Row', () => { let wrapper; - const columns = [{ - dataField: 'id', - text: 'ID' - }, { - dataField: 'name', - text: 'Name' - }]; const row = { id: 1, - name: 'A' + name: 'A', + price: 1000 }; describe('simplest row', () => { beforeEach(() => { - wrapper = shallow(); + wrapper = shallow( + ); }); it('should render successfully', () => { @@ -30,4 +39,158 @@ describe('Row', () => { expect(wrapper.find(Cell).length).toBe(Object.keys(row).length); }); }); + + describe('when cellEdit prop is defined', () => { + let columns; + let cellEdit; + const rowIndex = 1; + const keyField = 'id'; + + beforeEach(() => { + columns = defaultColumns; + cellEdit = { + mode: Const.CLICK_TO_CELL_EDIT + }; + wrapper = shallow( + + ); + }); + + afterEach(() => { + columns = undefined; + cellEdit = undefined; + }); + + it('Cell component should receive correct editable props', () => { + expect(wrapper.length).toBe(1); + for (let i = 0; i < columns.length; i += 1) { + const column = columns[i]; + if (column.dataField === keyField) { + expect(wrapper.find(Cell).get(i).props.editable).toBeFalsy(); + } else { + expect(wrapper.find(Cell).get(i).props.editable).toBeTruthy(); + } + } + }); + + it('Cell component should receive correct editMode props', () => { + expect(wrapper.length).toBe(1); + for (let i = 0; i < columns.length; i += 1) { + expect(wrapper.find(Cell).get(i).props.editMode).toEqual(cellEdit.mode); + } + }); + + describe('when have column.editable defined false', () => { + const nonEditableColIndex = 1; + beforeEach(() => { + columns[nonEditableColIndex].editable = false; + wrapper = shallow( + + ); + }); + + it('Cell component should receive correct editMode props', () => { + expect(wrapper.length).toBe(1); + for (let i = 0; i < columns.length; i += 1) { + const column = columns[i]; + if (i === nonEditableColIndex || column.dataField === keyField) { + expect(wrapper.find(Cell).get(i).props.editable).toBeFalsy(); + } else { + expect(wrapper.find(Cell).get(i).props.editable).toBeTruthy(); + } + } + }); + }); + + // Means user defined cellEdit.nonEditableRows + // and some rows will be treated as noneditable by this rules + describe('when editable prop is false', () => { + beforeEach(() => { + wrapper = shallow( + + ); + }); + + it('All the Cell components should be noneditable', () => { + expect(wrapper.length).toBe(1); + for (let i = 0; i < columns.length; i += 1) { + expect(wrapper.find(Cell).get(i).props.editable).toBeFalsy(); + } + }); + }); + + // Means a cell now is undering editing + describe('when cellEdit.ridx and cellEdit.cidx is defined', () => { + describe('and cellEdit.ridx is match to current row index', () => { + const editingColIndex = 1; + beforeEach(() => { + cellEdit.ridx = rowIndex; + cellEdit.cidx = editingColIndex; + cellEdit.onComplete = sinon.stub(); + cellEdit.onEscape = sinon.stub(); + wrapper = shallow( + + ); + }); + + it('should render EditingCell correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(EditingCell).length).toBe(1); + expect(wrapper.find('tr').children().at(editingColIndex).type()).toEqual(EditingCell); + }); + }); + + describe('and cellEdit.ridx is not match to current row index', () => { + const editingColIndex = 1; + beforeEach(() => { + cellEdit.ridx = 3; + cellEdit.cidx = editingColIndex; + cellEdit.onComplete = sinon.stub(); + cellEdit.onEscape = sinon.stub(); + wrapper = shallow( + + ); + }); + + it('should not render any EditingCell component', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(EditingCell).length).toBe(0); + expect(wrapper.find(Cell).length).toBe(columns.length); + }); + }); + }); + }); }); diff --git a/packages/react-bootstrap-table2/test/store/base.test.js b/packages/react-bootstrap-table2/test/store/base.test.js index e633071..82e6d6c 100644 --- a/packages/react-bootstrap-table2/test/store/base.test.js +++ b/packages/react-bootstrap-table2/test/store/base.test.js @@ -3,15 +3,16 @@ import Const from '../../src/const'; describe('Store Base', () => { let store; - const data = [ - { id: 3, name: 'name2' }, - { id: 2, name: 'ABC' }, - { id: 4, name: '123tester' }, - { id: 1, name: '!@#' } - ]; + let data; beforeEach(() => { - store = new Base({ data }); + data = [ + { id: 3, name: 'name2' }, + { id: 2, name: 'ABC' }, + { id: 4, name: '123tester' }, + { id: 1, name: '!@#' } + ]; + store = new Base({ data, keyField: 'id' }); }); describe('initialize', () => { @@ -72,4 +73,40 @@ describe('Store Base', () => { }); }); }); + + describe('getRowByRowId', () => { + it('should return data if specified id existing', () => { + const result = store.getRowByRowId(3); + expect(result).toBeDefined(); + }); + + it('should return undefined if specified id existing', () => { + const result = store.getRowByRowId(88); + expect(result).toBeUndefined(); + }); + }); + + describe('edit', () => { + it('should update a specified field correctly', () => { + const newValue = 'newValue'; + const dataField = 'name'; + const rowId = 2; + store.edit(rowId, dataField, newValue); + + const row = store.data.find(d => d[store.keyField] === rowId); + expect(row[dataField]).toEqual(newValue); + }); + + it('should not throw any error even if rowId is not existing', () => { + expect(() => { + store.edit('123', 'name', 'value'); + }).not.toThrow(); + }); + + it('should throwing error if dataField is not existing', () => { + expect(() => { + store.edit(2, 'non_exist_field', 'value'); + }).toThrow(); + }); + }); }); diff --git a/packages/react-bootstrap-table2/test/test-helpers/mock-component.js b/packages/react-bootstrap-table2/test/test-helpers/mock-component.js new file mode 100644 index 0000000..9f2d943 --- /dev/null +++ b/packages/react-bootstrap-table2/test/test-helpers/mock-component.js @@ -0,0 +1,15 @@ +export const extendTo = Base => + class MockComponent extends Base { + constructor(props) { + super(props); + this.state = { + data: this.props.data, + currEditCell: { + ridx: null, + cidx: null + } + }; + } + + render() { return null; } + }; diff --git a/packages/react-bootstrap-table2/test/test-helpers/table-wrapper.js b/packages/react-bootstrap-table2/test/test-helpers/table-wrapper.js new file mode 100644 index 0000000..e2decdb --- /dev/null +++ b/packages/react-bootstrap-table2/test/test-helpers/table-wrapper.js @@ -0,0 +1,11 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +export const TableRowWrapper = props => ( + + + { props.children } + +
+); + diff --git a/packages/react-bootstrap-table2/test/text-editor.test.js b/packages/react-bootstrap-table2/test/text-editor.test.js new file mode 100644 index 0000000..dbdaeef --- /dev/null +++ b/packages/react-bootstrap-table2/test/text-editor.test.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import TextEditor from '../src/text-editor'; + +describe('TextEditor', () => { + let wrapper; + const value = 'test'; + + beforeEach(() => { + wrapper = shallow( + + ); + }); + + it('should render TextEditor correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find('input').length).toBe(1); + expect(wrapper.find('input').prop('type')).toEqual('text'); + expect(wrapper.find('.form-control.editor.edit-text').length).toBe(1); + }); +}); diff --git a/packages/react-bootstrap-table2/test/utils.test.js b/packages/react-bootstrap-table2/test/utils.test.js index 491cc04..2aa43b1 100644 --- a/packages/react-bootstrap-table2/test/utils.test.js +++ b/packages/react-bootstrap-table2/test/utils.test.js @@ -22,6 +22,42 @@ describe('Utils', () => { }); }); + describe('set', () => { + const newValue = 'test'; + const data = { + name: 'A', + address: { + road: 'BCD', + postal: '1234-12345', + city: { + name: 'B' + } + } + }; + + it('should set data successfully', () => { + _.set(data, 'name', newValue); + _.set(data, 'address.road', newValue); + _.set(data, 'address.city.name', newValue); + expect(data.name).toEqual(newValue); + expect(data.address.road).toEqual(newValue); + expect(data.address.city.name).toEqual(newValue); + }); + + it('should throw error if target not existing', () => { + expect(() => { + _.set(data, 'address.not.existing', newValue); + }).toThrow(); + }); + + it('should not throw error if target not existing but with safe=true', () => { + expect(() => { + _.set(data, 'address.not.existing', newValue, true); + }).not.toThrow(); + expect(data.address.not.existing).toEqual({}); + }); + }); + describe('isObject', () => { describe('when given Object', () => { it('should return true', () => {