diff --git a/README.md b/README.md index 7d90d42..39f14b1 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Rebuilt [react-bootstrap-table](https://github.com/AllenFang/react-bootstrap-tab * [`react-bootstrap-table2-editor`](https://www.npmjs.com/package/react-bootstrap-table2-editor) * [`react-bootstrap-table2-paginator`](https://www.npmjs.com/package/react-bootstrap-table2-paginator) * [`react-bootstrap-table2-overlay`](https://www.npmjs.com/package/react-bootstrap-table2-overlay) +* [`react-bootstrap-table2-toolkit`](https://www.npmjs.com/package/react-bootstrap-table2-toolkit) This can help your application with less bundled size and also help us have clean design to avoid handling to much logic in kernal module(SRP). diff --git a/docs/README.md b/docs/README.md index 14782fe..b5bd0bd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,7 @@ #### Optional * [remote](#remote) +* [bootstrap4](#bootstrap4) * [loading](#loading) * [caption](#caption) * [striped](#striped) @@ -21,6 +22,7 @@ * [headerClasses](#headerClasses) * [cellEdit](#cellEdit) * [selectRow](#selectRow) +* [expandRow](#expandRow) * [rowStyle](#rowStyle) * [rowClasses](#rowClasses) * [rowEvents](#rowEvents) @@ -66,6 +68,9 @@ remote={ { pagination: true, filter: false, sort: false } } There is a special case for remote pagination, even you only specified the pagination need to handle as remote, `react-bootstrap-table2` will handle all the table changes(filter, sort etc) as remote mode, because `react-bootstrap-table2` only know the data of current page, but filtering, searching or sort need to work on overall data. +### bootstrap4 - [Bool] +`true` to indicate your bootstrap version is 4. Default version is 3. + ### 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. When `loading` is `true`, `react-bootstrap-table2` will attend to render a overlay on table via [`overlay`](#overlay) prop, if [`overlay`](#overlay) prop is not given, `react-bootstrap-table2` will ignore the overlay rendering. @@ -122,6 +127,9 @@ Makes table cells editable, please see [cellEdit definition](./cell-edit.md) for ### selectRow - [Object] Makes table rows selectable, please see [selectRow definition](./row-selection.md) for more detail. +### expandRow - [Object] +Makes table rows expandable, please see [expandRow definition](./row-expand.md) for more detail. + ### rowStyle = [Object | Function] Custom the style of table rows: diff --git a/docs/columns.md b/docs/columns.md index 4b78e36..89c62cf 100644 --- a/docs/columns.md +++ b/docs/columns.md @@ -38,6 +38,10 @@ Available properties in a column object: * [editorRenderer](#editorRenderer) * [filter](#filter) * [filterValue](#filterValue) +* [csvType](#csvType) +* [csvFormatter](#csvFormatter) +* [csvText](#csvText) +* [csvExport](#csvExport) Following is a most simplest and basic usage: @@ -685,4 +689,17 @@ A final `String` value you want to be filtered. filter: textFilter(), filterValue: (cell, row) => owners[cell] } -``` \ No newline at end of file +``` + +## column.csvType - [Object] +Default is `String`. Currently, the available value is `String` and `Number`. If `Number` assigned, the cell value will not wrapped with double quote. + +## column.csvFormatter - [Function] + +This is same as [`column.formatter`](#formatter). But `csvFormatter` only for CSV export and called when export CSV. + +## column.csvText - [String] +Custom the CSV header cell, Default is [`column.text`](#text). + +## column.csvExport - [Bool] +Default is `true`, `false` will hide this column when export CSV. diff --git a/docs/migration.md b/docs/migration.md index 32d137d..ffe7861 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -22,6 +22,8 @@ Currently, **I still can't implement all the mainly features in legacy `react-bo * Pagination Addons * [`react-bootstrap-table2-overlay`](https://www.npmjs.com/package/react-bootstrap-table2-overlay) * Overlay/Loading Addons +* [`react-bootstrap-table2-toolkit`](https://www.npmjs.com/package/react-bootstrap-table2-toolkit) + * Table Toolkits, like search, csv etc. This can help your application with less bundled size and also help `react-bootstrap-table2` have clean design to avoid handling to much logic in kernel module(SRP). Hence, which means you probably need to install above addons when you need specific features. @@ -113,6 +115,34 @@ Remember to install [`react-bootstrap-table2-paginator`](https://www.npmjs.com/p No big changes for pagination, but still can't custom the pagination list, button and sizePerPage dropdown. +## Table Search +The usage of search functionality is a little bit different from legacy search. The mainly different thing is developer have to render the search input field, we do believe it will be very flexible for all the developers who want to custom the search position or search field itself. + +- [x] Custom search component and position +- [x] Custom search value +- [ ] Clear search +- [ ] Multiple search +- [ ] Strict search + +## Row Expand +- [x] Expand Row Events +- [x] Expand Row Indicator +- [x] Expand Row Management +- [x] Custom Expand Row Indicators +- [ ] Compatiable with Row Selection +- [ ] Expand Column position +- [ ] Expand Column Style/Class + +## Export CSV +Export CSV functionality is like search, which is one of functionality in the `react-bootstrap-table2-toolkit`. All of the legacy functions we already implemented. + ## Remote -> It's totally different in `react-bootstrap-table2`. Please [see](https://react-bootstrap-table.github.io/react-bootstrap-table2/docs/basic-remote.html). \ No newline at end of file +> It's totally different in `react-bootstrap-table2`. Please [see](https://react-bootstrap-table.github.io/react-bootstrap-table2/docs/basic-remote.html). + + +## Row insert/Delete +Not support yet + +## Keyboard Navigation +Not support yet diff --git a/docs/row-expand.md b/docs/row-expand.md new file mode 100644 index 0000000..1aa958a --- /dev/null +++ b/docs/row-expand.md @@ -0,0 +1,129 @@ + +# Row expand +`react-bootstrap-table2` supports the row expand feature. By passing prop `expandRow` to enable this functionality. + +> Default is click to expand/collapse a row. In addition, we don't support any way to chagne this mechanism! + +## Required +* [renderer (**required**)](#renderer) + +## Optional +* [expanded](#expanded) +* [nonExpandable](#nonExpandable) +* [onExpand](#onExpand) +* [onExpandAll](#onExpandAll) +* [showExpandColumn](#showExpandColumn) +* [expandColumnRenderer](#expandColumnRenderer) +* [expandHeaderColumnRenderer](#expandHeaderColumnRenderer) + +### expandRow.renderer - [Function] + +Specify the content of expand row, `react-bootstrap-table2` will pass a row object as argument and expect return a react element. + +#### values +* **row** + +#### examples + +```js +const expandRow = { + renderer: row => ( +
+

{ `This Expand row is belong to rowKey ${row.id}` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ) +}; + + +``` + +### expandRow.expanded - [Array] +`expandRow.expanded` allow you have default row expandations on table. + +```js +const expandRow = { + renderer: (row) => ... + expanded: [1, 3] // should be a row keys array +}; +``` + +### expandRow.nonExpandable - [Array] +This prop allow you to restrict some rows which can not be expanded by user. `expandRow.nonExpandable` accept an rowkeys array. + +```js +const expandRow = { + renderer: (row) => ... + nonExpandable: [1, 3 ,5] +}; +``` + +### expandRow.onExpand - [Function] +This callback function will be called when a row is expand/collapse and pass following four arguments: +`row`, `isExpand`, `rowIndex` and `e`. + +```js +const expandRow = { + renderer: (row) => ... + onExpand: (row, isExpand, rowIndex, e) => { + // ... + } +}; +``` + +### expandRow.onExpandAll - [Function] +This callback function will be called when expand/collapse all. It only work when you configure [`expandRow.showExpandColumn`](#showExpandColumn) as `true`. + +```js +const expandRow = { + renderer: (row) => ... + onExpandAll: (isExpandAll, results, e) => { + // ... + } +}; +``` + +### expandRow.expandColumnRenderer - [Function] +Provide a callback function which allow you to custom the expand indicator. This callback only have one argument which is an object and contain one property `expanded` which indicate if current row is expanded + + +```js +const expandRow = { + renderer: (row) => ... + expandColumnRenderer: ({ expanded }) => ( + // .... + ) +}; +``` + +> By default, `react-bootstrap-table2` will help you to handle the click event, it's not necessary to handle again by developer. + +### expandRow.expandHeaderColumnRenderer - [Function] +Provide a callback function which allow you to custom the expand indicator in the expand header column. This callback only have one argument which is an object and contain one property `isAnyExpands` which indicate if there's any rows are expanded: + +```js +const expandRow = { + renderer: (row) => ... + expandHeaderColumnRenderer: ({ isAnyExpands }) => ( + // .... + ) +}; +``` + +> By default, `react-bootstrap-table2` will help you to handle the click event, it's not necessary to handle again by developer. + +### expandRow.showExpandColumn - [Bool] +Default is `false`, if you want to have a expand indicator, give this prop as `true` + +```js +const expandRow = { + renderer: (row) => ... + showExpandColumn: true +}; +``` diff --git a/gulpfile.babel.js b/gulpfile.babel.js index f202fda..fea8b18 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -17,7 +17,8 @@ const JS_PKGS = [ 'react-bootstrap-table2-editor', 'react-bootstrap-table2-filter', 'react-bootstrap-table2-overlay', - 'react-bootstrap-table2-paginator' + 'react-bootstrap-table2-paginator', + 'react-bootstrap-table2-toolkit' ].reduce((pkg, curr) => `${curr}|${pkg}`, ''); const JS_SKIPS = `+(${TEST}|${LIB}|${DIST}|${NODE_MODULES})`; @@ -25,7 +26,8 @@ const JS_SKIPS = `+(${TEST}|${LIB}|${DIST}|${NODE_MODULES})`; const STYLE_PKGS = [ 'react-bootstrap-table2', 'react-bootstrap-table2-filter', - 'react-bootstrap-table2-paginator' + 'react-bootstrap-table2-paginator', + 'react-bootstrap-table2-toolkit', ].reduce((pkg, curr) => `${curr}|${pkg}`, ''); const STYLE_SKIPS = `+(${NODE_MODULES})`; @@ -78,7 +80,8 @@ function umd(done) { () => gulp.src('./webpack/editor.umd.babel.js').pipe(shell(['webpack --config <%= file.path %>'])), () => gulp.src('./webpack/filter.umd.babel.js').pipe(shell(['webpack --config <%= file.path %>'])), () => gulp.src('./webpack/overlay.umd.babel.js').pipe(shell(['webpack --config <%= file.path %>'])), - () => gulp.src('./webpack/paginator.umd.babel.js').pipe(shell(['webpack --config <%= file.path %>'])) + () => gulp.src('./webpack/paginator.umd.babel.js').pipe(shell(['webpack --config <%= file.path %>'])), + () => gulp.src('./webpack/toolkit.umd.babel.js').pipe(shell(['webpack --config <%= file.path %>'])) )(); done(); } diff --git a/package.json b/package.json index 5564378..5cb8003 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "pretest": "yarn lint --cache", "test": "jest", "test:coverage": "jest --coverage", - "test:watch": "jest --watch", + "test:watch": "jest --coverage --watch", "storybook": "cd ./packages/react-bootstrap-table2-example && yarn storybook", "gh-pages:clean": "cd ./packages/react-bootstrap-table2-example && yarn gh-pages:clean", "gh-pages:build": "cd ./packages/react-bootstrap-table2-example && yarn gh-pages:build", @@ -50,8 +50,8 @@ "babel-preset-stage-0": "6.24.1", "babel-register": "6.24.1", "css-loader": "0.28.1", - "enzyme": "3.1.1", - "enzyme-adapter-react-16": "1.0.4", + "enzyme": "3.3.0", + "enzyme-adapter-react-16": "1.1.1", "eslint": "4.5.0", "eslint-config-airbnb": "15.1.0", "eslint-loader": "1.9.0", @@ -81,12 +81,12 @@ "dependencies": { "classnames": "2.2.5", "prop-types": "15.5.10", - "react": "16.0.0", - "react-dom": "16.0.0" + "react": "16.3.2", + "react-dom": "16.3.2" }, "jest": { "collectCoverageFrom": [ - "packages/*/src/*.js", + "packages/*/src/**/*.js", "packages/*/index.js" ], "roots": [ diff --git a/packages/react-bootstrap-table2-editor/index.js b/packages/react-bootstrap-table2-editor/index.js index 3d07c2c..0bc57b5 100644 --- a/packages/react-bootstrap-table2-editor/index.js +++ b/packages/react-bootstrap-table2-editor/index.js @@ -1,4 +1,4 @@ -import wrapperFactory from './src/wrapper'; +import createContext from './src/context'; import editingCellFactory from './src/editing-cell'; import { EDITTYPE, @@ -8,7 +8,7 @@ import { } from './src/const'; export default (options = {}) => ({ - wrapperFactory, + createContext, editingCellFactory, CLICK_TO_CELL_EDIT, DBCLICK_TO_CELL_EDIT, diff --git a/packages/react-bootstrap-table2-editor/package.json b/packages/react-bootstrap-table2-editor/package.json index f42e3df..ffdd996 100644 --- a/packages/react-bootstrap-table2-editor/package.json +++ b/packages/react-bootstrap-table2-editor/package.json @@ -41,7 +41,7 @@ ], "peerDependencies": { "prop-types": "^15.0.0", - "react": "^16.0.0", - "react-dom": "^16.0.0" + "react": "^16.3.0", + "react-dom": "^16.3.0" } } diff --git a/packages/react-bootstrap-table2-editor/src/wrapper.js b/packages/react-bootstrap-table2-editor/src/context.js similarity index 72% rename from packages/react-bootstrap-table2-editor/src/wrapper.js rename to packages/react-bootstrap-table2-editor/src/context.js index 97138d4..fb12068 100644 --- a/packages/react-bootstrap-table2-editor/src/wrapper.js +++ b/packages/react-bootstrap-table2-editor/src/context.js @@ -1,16 +1,22 @@ /* eslint react/prop-types: 0 */ -import React, { Component } from 'react'; +/* eslint react/require-default-props: 0 */ +import React from 'react'; import PropTypes from 'prop-types'; - import { CLICK_TO_CELL_EDIT, DBCLICK_TO_CELL_EDIT } from './const'; export default ( - Base, - { _, remoteResolver } + _, + dataOperator, + isRemoteCellEdit, + handleCellChange ) => { let EditingCell; - return class CellEditWrapper extends remoteResolver(Component) { + const CellEditContext = React.createContext(); + + class CellEditProvider extends React.Component { static propTypes = { + data: PropTypes.array.isRequired, + selectRow: PropTypes.object, options: PropTypes.shape({ mode: PropTypes.oneOf([CLICK_TO_CELL_EDIT, DBCLICK_TO_CELL_EDIT]).isRequired, onErrorMessageDisappear: PropTypes.func, @@ -19,7 +25,7 @@ export default ( afterSaveCell: PropTypes.func, nonEditableRows: PropTypes.func, timeToCloseMessage: PropTypes.number, - errorMessage: PropTypes.string + errorMessage: PropTypes.any }) } @@ -33,41 +39,32 @@ export default ( this.state = { ridx: null, cidx: null, - message: null, - isDataChanged: false + message: null }; } componentWillReceiveProps(nextProps) { - if (nextProps.cellEdit && this.isRemoteCellEdit()) { + if (nextProps.cellEdit && isRemoteCellEdit()) { if (nextProps.cellEdit.options.errorMessage) { this.setState(() => ({ - isDataChanged: false, message: nextProps.cellEdit.options.errorMessage })); } else { - this.setState(() => ({ - isDataChanged: true - })); this.escapeEditing(); } - } else { - this.setState(() => ({ - isDataChanged: false - })); } } handleCellUpdate(row, column, newValue) { - const { keyField, cellEdit, store } = this.props; + const { keyField, cellEdit, data } = this.props; const { beforeSaveCell, afterSaveCell } = cellEdit.options; const oldValue = _.get(row, column.dataField); const rowId = _.get(row, keyField); if (_.isFunction(beforeSaveCell)) beforeSaveCell(oldValue, newValue, row, column); - if (this.isRemoteCellEdit()) { - this.handleCellChange(rowId, column.dataField, newValue); + if (isRemoteCellEdit()) { + handleCellChange(rowId, column.dataField, newValue); } else { - store.edit(rowId, column.dataField, newValue); + dataOperator.editCell(data, keyField, rowId, column.dataField, newValue); if (_.isFunction(afterSaveCell)) afterSaveCell(oldValue, newValue, row, column); this.completeEditing(); } @@ -77,8 +74,7 @@ export default ( this.setState(() => ({ ridx: null, cidx: null, - message: null, - isDataChanged: true + message: null })); } @@ -86,8 +82,7 @@ export default ( const editing = () => { this.setState(() => ({ ridx, - cidx, - isDataChanged: false + cidx })); }; @@ -103,18 +98,19 @@ export default ( } render() { - const { isDataChanged, ...stateRest } = this.state; const { cellEdit: { options: { nonEditableRows, errorMessage, ...optionsRest }, editingCellFactory, + createContext, ...cellEditRest } } = this.props; + const newCellEdit = { ...optionsRest, ...cellEditRest, - ...stateRest, + ...this.state, EditingCell, nonEditableRows: _.isDefined(nonEditableRows) ? nonEditableRows() : [], onStart: this.startEditing, @@ -123,13 +119,16 @@ export default ( }; return ( - + + { this.props.children } + ); } + } + return { + Provider: CellEditProvider, + Consumer: CellEditContext.Consumer }; }; diff --git a/packages/react-bootstrap-table2-editor/test/context.test.js b/packages/react-bootstrap-table2-editor/test/context.test.js new file mode 100644 index 0000000..89af0ce --- /dev/null +++ b/packages/react-bootstrap-table2-editor/test/context.test.js @@ -0,0 +1,430 @@ +import 'jsdom-global/register'; +import React from 'react'; +import { shallow } from 'enzyme'; +import _ from 'react-bootstrap-table-next/src/utils'; +import dataOperator from 'react-bootstrap-table-next/src/store/operators'; +import BootstrapTable from 'react-bootstrap-table-next/src/bootstrap-table'; + +import { + CLICK_TO_CELL_EDIT, + DBCLICK_TO_CELL_EDIT, + DELAY_FOR_DBCLICK +} from '../src/const'; +import createCellEditContext from '../src/context'; +import cellEditFactory from '../index'; + +describe('CellEditContext', () => { + let wrapper; + let cellEdit; + let CellEditContext; + + const data = [{ + id: 1, + name: 'A' + }, { + id: 2, + name: 'B' + }]; + + const keyField = 'id'; + + const columns = [{ + dataField: 'id', + text: 'ID' + }, { + dataField: 'name', + text: 'Name' + }]; + + const defaultCellEdit = { + mode: CLICK_TO_CELL_EDIT + }; + + const defaultSelectRow = undefined; + + const mockBase = jest.fn((props => ( + + ))); + + const handleCellChange = jest.fn(); + + function shallowContext( + customCellEdit = defaultCellEdit, + enableRemote = false, + selectRow = defaultSelectRow + ) { + mockBase.mockReset(); + handleCellChange.mockReset(); + CellEditContext = createCellEditContext( + _, + dataOperator, + jest.fn().mockReturnValue(enableRemote), + handleCellChange + ); + cellEdit = cellEditFactory(customCellEdit); + return ( + + + { + cellEditProps => mockBase(cellEditProps) + } + + + ); + } + + describe('default render', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.render(); + }); + + it('should have correct Provider property after calling createCellEditContext', () => { + expect(CellEditContext.Provider).toBeDefined(); + }); + + it('should have correct Consumer property after calling createCellEditContext', () => { + expect(CellEditContext.Consumer).toBeDefined(); + }); + + it('should have correct state.ridx', () => { + expect(wrapper.state().ridx).toBeNull(); + }); + + it('should have correct state.cidx', () => { + expect(wrapper.state().cidx).toBeNull(); + }); + + it('should have correct state.message', () => { + expect(wrapper.state().message).toBeNull(); + }); + + it('should pass correct cell editing props to children element', () => { + expect(wrapper.length).toBe(1); + expect(JSON.stringify(mockBase.mock.calls[0])).toEqual(JSON.stringify([{ + cellEdit: { + ...defaultCellEdit, + CLICK_TO_CELL_EDIT, + DBCLICK_TO_CELL_EDIT, + DELAY_FOR_DBCLICK, + ...wrapper.state(), + nonEditableRows: [] + } + }])); + }); + }); + + describe('componentWillReceiveProps', () => { + const initialState = { ridx: 1, cidx: 1, message: 'test' }; + describe('if nextProps.cellEdit is not existing', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.setState(initialState); + wrapper.render(); + wrapper.instance().componentWillReceiveProps({}); + }); + + it('should not set state.message', () => { + expect(wrapper.state().message).toBe(initialState.message); + }); + + it('should not set state.ridx', () => { + expect(wrapper.state().ridx).toBe(initialState.ridx); + }); + + it('should not set state.cidx', () => { + expect(wrapper.state().cidx).toBe(initialState.cidx); + }); + }); + + describe('if nextProps.cellEdit is existing but remote cell editing is disable', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.setState(initialState); + wrapper.render(); + wrapper.instance().componentWillReceiveProps({ + cellEdit: cellEditFactory(defaultCellEdit) + }); + }); + + it('should not set state.message', () => { + expect(wrapper.state().message).toBe(initialState.message); + }); + + it('should not set state.ridx', () => { + expect(wrapper.state().ridx).toBe(initialState.ridx); + }); + + it('should not set state.cidx', () => { + expect(wrapper.state().cidx).toBe(initialState.cidx); + }); + }); + + describe('if nextProps.cellEdit is existing and remote cell editing is enable', () => { + describe('if nextProps.cellEdit.options.errorMessage is defined', () => { + let message; + beforeEach(() => { + message = 'validation fail'; + wrapper = shallow(shallowContext(defaultCellEdit, true)); + wrapper.setState(initialState); + wrapper.render(); + wrapper.instance().componentWillReceiveProps({ + cellEdit: cellEditFactory({ + ...defaultCellEdit, + errorMessage: message + }) + }); + wrapper.update(); + }); + + it('should set state.message', () => { + expect(wrapper.state('message')).toBe(message); + }); + + it('should not set state.ridx', () => { + expect(wrapper.state().ridx).toBe(initialState.ridx); + }); + + it('should not set state.cidx', () => { + expect(wrapper.state().cidx).toBe(initialState.cidx); + }); + }); + + describe('if nextProps.cellEdit.options.errorMessage is not defined', () => { + beforeEach(() => { + wrapper = shallow(shallowContext(defaultCellEdit, true)); + wrapper.setState(initialState); + wrapper.instance().componentWillReceiveProps({ + cellEdit: cellEditFactory({ ...defaultCellEdit }) + }); + wrapper.update(); + }); + + it('should not set state.message', () => { + expect(wrapper.state('message')).toBe(initialState.message); + }); + + it('should set correct state.ridx', () => { + expect(wrapper.state().ridx).toBeNull(); + }); + + it('should set correct state.cidx', () => { + expect(wrapper.state().cidx).toBeNull(); + }); + }); + }); + }); + + describe('handleCellUpdate', () => { + const row = data[1]; + const column = columns[1]; + const newValue = 'This is new value'; + const oldValue = row[column.dataField]; + + describe('if cellEdit.beforeSaveCell prop is defined', () => { + const beforeSaveCell = jest.fn(); + + beforeEach(() => { + beforeSaveCell.mockReset(); + wrapper = shallow(shallowContext({ + ...defaultCellEdit, + beforeSaveCell + })); + wrapper.instance().handleCellUpdate( + row, + column, + newValue + ); + }); + + it('should call cellEdit.beforeSaveCell correctly', () => { + expect(beforeSaveCell).toHaveBeenCalledTimes(1); + expect(beforeSaveCell).toHaveBeenCalledWith(oldValue, newValue, row, column); + }); + }); + + describe('when remote cell editing is enable', () => { + const afterSaveCell = jest.fn(); + beforeEach(() => { + afterSaveCell.mockReset(); + wrapper = shallow(shallowContext({ + ...defaultCellEdit, + afterSaveCell + }, true)); + wrapper.instance().handleCellUpdate( + row, + column, + newValue + ); + }); + + it('should call handleCellChange correctly', () => { + expect(handleCellChange).toHaveBeenCalledTimes(1); + expect(handleCellChange).toHaveBeenCalledWith(row[keyField], column.dataField, newValue); + }); + + it('should not call cellEdit.afterSaveCell even if it is defined', () => { + expect(afterSaveCell).toHaveBeenCalledTimes(0); + }); + }); + + describe('when remote cell editing is disable', () => { + const afterSaveCell = jest.fn(); + + beforeEach(() => { + afterSaveCell.mockReset(); + wrapper = shallow(shallowContext({ + ...defaultCellEdit, + afterSaveCell + })); + wrapper.setState({ + ridx: 1, + cidx: 1 + }); + wrapper.instance().handleCellUpdate( + row, + column, + newValue + ); + }); + + it('should not call handleCellChange correctly', () => { + expect(handleCellChange).toHaveBeenCalledTimes(0); + }); + + it('should set state correctly', () => { + expect(wrapper.state('ridx')).toBeNull(); + expect(wrapper.state('cidx')).toBeNull(); + expect(wrapper.state('message')).toBeNull(); + }); + + it('should call cellEdit.afterSaveCell if it is defined', () => { + expect(afterSaveCell).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('completeEditing', () => { + const initialState = { ridx: 1, cidx: 1, message: 'test' }; + + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.setState(initialState); + wrapper.render(); + wrapper.instance().completeEditing(); + }); + + it('should set state correctly', () => { + expect(wrapper.state().ridx).toBeNull(); + expect(wrapper.state().cidx).toBeNull(); + expect(wrapper.state().message).toBeNull(); + }); + }); + + describe('startEditing', () => { + const ridx = 0; + const cidx = 1; + + describe('if selectRow prop is not defined', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.render(); + wrapper.instance().startEditing(ridx, cidx); + }); + + it('should set state correctly', () => { + expect(wrapper.state().ridx).toEqual(ridx); + expect(wrapper.state().cidx).toEqual(cidx); + }); + }); + + describe('if selectRow prop is defined', () => { + describe('and selectRow.clickToEdit is enable', () => { + beforeEach(() => { + wrapper = shallow(shallowContext( + defaultCellEdit, + false, + { + ...defaultSelectRow, + clickToEdit: true + } + )); + wrapper.render(); + wrapper.instance().startEditing(ridx, cidx); + }); + + it('should set state correctly', () => { + expect(wrapper.state().ridx).toEqual(ridx); + expect(wrapper.state().cidx).toEqual(cidx); + }); + }); + + describe('and selectRow.clickToSelect is disable', () => { + beforeEach(() => { + wrapper = shallow(shallowContext( + defaultCellEdit, + false, + { + ...defaultSelectRow, + clickToSelect: false + } + )); + wrapper.render(); + wrapper.instance().startEditing(ridx, cidx); + }); + + it('should set state correctly', () => { + expect(wrapper.state().ridx).toEqual(ridx); + expect(wrapper.state().cidx).toEqual(cidx); + }); + }); + + describe('and selectRow.clickToEdit & selectRow.clickToSelect is enable', () => { + beforeEach(() => { + wrapper = shallow(shallowContext( + defaultCellEdit, + false, + { + ...defaultSelectRow, + clickToEdit: false, + clickToSelect: true + } + )); + wrapper.render(); + wrapper.instance().startEditing(ridx, cidx); + }); + + it('should not set state', () => { + expect(wrapper.state().ridx).toBeNull(); + expect(wrapper.state().cidx).toBeNull(); + }); + }); + }); + }); + + describe('escapeEditing', () => { + const initialState = { ridx: 1, cidx: 1 }; + + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.setState(initialState); + wrapper.instance().escapeEditing(); + }); + + it('should set state correctly', () => { + expect(wrapper.state().ridx).toBeNull(); + expect(wrapper.state().cidx).toBeNull(); + }); + }); +}); diff --git a/packages/react-bootstrap-table2-editor/test/wrapper.test.js b/packages/react-bootstrap-table2-editor/test/wrapper.test.js deleted file mode 100644 index 8264ca8..0000000 --- a/packages/react-bootstrap-table2-editor/test/wrapper.test.js +++ /dev/null @@ -1,330 +0,0 @@ -import React from 'react'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; - -import _ from 'react-bootstrap-table-next/src/utils'; -import remoteResolver from 'react-bootstrap-table-next/src/props-resolver/remote-resolver'; -import Store from 'react-bootstrap-table-next/src/store'; -import BootstrapTable from 'react-bootstrap-table-next/src/bootstrap-table'; -import cellEditFactory from '..'; -import * as Const from '../src/const'; -import wrapperFactory from '../src/wrapper'; - -describe('CellEditWrapper', () => { - let wrapper; - let instance; - const onTableChangeCB = sinon.stub(); - const columns = [{ - dataField: 'id', - text: 'ID' - }, { - dataField: 'name', - text: 'Name' - }]; - const data = [{ - id: 1, - name: 'A' - }, { - id: 2, - name: 'B' - }]; - - const createTableProps = (props = {}) => { - const { cellEdit, ...rest } = props; - const tableProps = { - keyField: 'id', - columns, - data, - _, - store: new Store('id'), - cellEdit: cellEditFactory(cellEdit), - onTableChange: onTableChangeCB, - ...rest - }; - tableProps.store.data = data; - return tableProps; - }; - - const CellEditWrapper = wrapperFactory(BootstrapTable, { - _, - remoteResolver - }); - - const createCellEditWrapper = (props, renderFragment = true) => { - wrapper = shallow(); - instance = wrapper.instance(); - if (renderFragment) { - const fragment = instance.render(); - wrapper = shallow(
{ fragment }
); - } - }; - - afterEach(() => { - onTableChangeCB.reset(); - }); - - beforeEach(() => { - const props = createTableProps({ - cellEdit: { mode: Const.CLICK_TO_CELL_EDIT } - }); - createCellEditWrapper(props); - }); - - it('should render CellEditWrapper correctly', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.find(BootstrapTable)).toBeDefined(); - }); - - it('should have correct state', () => { - expect(instance.state.ridx).toBeNull(); - expect(instance.state.cidx).toBeNull(); - expect(instance.state.message).toBeNull(); - expect(instance.state.isDataChanged).toBeFalsy(); - }); - - it('should inject correct props to base component', () => { - const base = wrapper.find(BootstrapTable); - expect(base.props().cellEdit).toBeDefined(); - expect(base.props().cellEdit.onStart).toBeDefined(); - expect(base.props().cellEdit.onEscape).toBeDefined(); - expect(base.props().cellEdit.onUpdate).toBeDefined(); - expect(base.props().cellEdit.EditingCell).toBeDefined(); - expect(base.props().cellEdit.ridx).toBeNull(); - expect(base.props().cellEdit.cidx).toBeNull(); - expect(base.props().cellEdit.message).toBeNull(); - expect(base.props().isDataChanged).toBe(instance.state.isDataChanged); - }); - - describe('when receive new cellEdit prop', () => { - const spy = jest.spyOn(CellEditWrapper.prototype, 'escapeEditing'); - - describe('and cellEdit is not work on remote', () => { - beforeEach(() => { - const props = createTableProps({ - cellEdit: { mode: Const.CLICK_TO_CELL_EDIT } - }); - createCellEditWrapper(props); - wrapper.setProps({ cellEdit: props.cellEdit }); - }); - - it('should always setting state.isDataChanged as false', () => { - expect(instance.state.isDataChanged).toBeFalsy(); - }); - }); - - describe('and cellEdit is work on remote', () => { - let errorMessage; - let props; - beforeEach(() => { - props = createTableProps({ - cellEdit: { mode: Const.CLICK_TO_CELL_EDIT }, - remote: true - }); - }); - - describe('and cellEdit.errorMessage is defined', () => { - beforeEach(() => { - createCellEditWrapper(props, false); - errorMessage = 'test'; - const newCellEdit = { - ...props.cellEdit, - options: { ...props.cellEdit.options, errorMessage } - }; - wrapper.setProps({ cellEdit: newCellEdit }); - }); - - it('should setting correct state', () => { - expect(instance.state.isDataChanged).toBeFalsy(); - expect(instance.state.message).toEqual(errorMessage); - }); - }); - - describe('and cellEdit.errorMessage is undefined', () => { - beforeEach(() => { - errorMessage = null; - createCellEditWrapper(props, false); - const newCellEdit = { - ...props.cellEdit, - options: { ...props.cellEdit.options, errorMessage } - }; - wrapper.setProps({ cellEdit: newCellEdit }); - }); - - it('should setting correct state', () => { - expect(wrapper.state().isDataChanged).toBeTruthy(); - }); - - it('should escape current editing', () => { - expect(spy).toHaveBeenCalled(); - }); - }); - }); - }); - - describe('call escapeEditing function', () => { - it('should set state correctly', () => { - instance.escapeEditing(); - expect(instance.state.ridx).toBeNull(); - expect(instance.state.cidx).toBeNull(); - }); - }); - - describe('call startEditing function', () => { - const ridx = 1; - const cidx = 3; - - it('should set state correctly', () => { - instance.startEditing(ridx, cidx); - expect(instance.state.ridx).toEqual(ridx); - expect(instance.state.cidx).toEqual(cidx); - expect(instance.state.isDataChanged).toBeFalsy(); - }); - - describe('if selectRow.clickToSelect is defined', () => { - beforeEach(() => { - const selectRow = { mode: 'checkbox', clickToSelect: true }; - const props = createTableProps({ - cellEdit: { mode: Const.CLICK_TO_CELL_EDIT }, - selectRow - }); - createCellEditWrapper(props); - }); - - it('should not set state', () => { - instance.startEditing(ridx, cidx); - expect(instance.state.ridx).toBeNull(); - expect(instance.state.cidx).toBeDefined(); - }); - }); - - describe('if selectRow.clickToSelect and selectRow.clickToEdit is defined', () => { - beforeEach(() => { - const selectRow = { mode: 'checkbox', clickToSelect: true, clickToEdit: true }; - const props = createTableProps({ - cellEdit: { mode: Const.CLICK_TO_CELL_EDIT }, - selectRow - }); - createCellEditWrapper(props); - }); - - it('should set state correctly', () => { - instance.startEditing(ridx, cidx); - expect(instance.state.ridx).toEqual(ridx); - expect(instance.state.cidx).toEqual(cidx); - }); - }); - }); - - describe('call completeEditing function', () => { - it('should set state correctly', () => { - instance.completeEditing(); - expect(instance.state.ridx).toBeNull(); - expect(instance.state.cidx).toBeNull(); - expect(instance.state.message).toBeNull(); - expect(instance.state.isDataChanged).toBeTruthy(); - }); - }); - - describe('call handleCellUpdate function', () => { - let props; - const row = data[0]; - const column = columns[1]; - const newValue = 'new name'; - - describe('when cell edit is work on remote', () => { - const spy = jest.spyOn(CellEditWrapper.prototype, 'handleCellChange'); - - beforeEach(() => { - props = createTableProps({ - cellEdit: { mode: Const.CLICK_TO_CELL_EDIT }, - remote: true - }); - createCellEditWrapper(props); - 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.id); - expect(spy.mock.calls[0][1]).toEqual(column.dataField); - expect(spy.mock.calls[0][2]).toEqual(newValue); - }); - }); - - describe('when cell edit is not work on remote', () => { - const spyOnCompleteEditing = jest.spyOn(CellEditWrapper.prototype, 'completeEditing'); - const spyOnStoreEdit = jest.spyOn(Store.prototype, 'edit'); - - beforeEach(() => { - props = createTableProps({ - cellEdit: { mode: Const.CLICK_TO_CELL_EDIT } - }); - createCellEditWrapper(props); - 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.id); - expect(spyOnStoreEdit.mock.calls[0][1]).toEqual(column.dataField); - expect(spyOnStoreEdit.mock.calls[0][2]).toEqual(newValue); - }); - - it('should calling completeEditing function', () => { - expect(spyOnCompleteEditing).toHaveBeenCalled(); - }); - - describe('if cellEdit.afterSaveCell prop defined', () => { - const aftereSaveCellCallBack = sinon.stub(); - - beforeEach(() => { - props = createTableProps({ - cellEdit: { - mode: Const.CLICK_TO_CELL_EDIT, - afterSaveCell: aftereSaveCellCallBack - } - }); - createCellEditWrapper(props); - instance.handleCellUpdate(row, column, newValue); - }); - - it('should calling cellEdit.afterSaveCell correctly', () => { - expect(aftereSaveCellCallBack.callCount).toBe(1); - expect(aftereSaveCellCallBack.calledWith( - row[column.dataField], newValue, row, column) - ).toBe(true); - }); - }); - }); - - describe('if cellEdit.beforeSaveCell prop defined', () => { - const beforeSaveCellCallBack = sinon.stub(); - beforeEach(() => { - props = createTableProps({ - cellEdit: { - mode: Const.CLICK_TO_CELL_EDIT, - beforeSaveCell: beforeSaveCellCallBack - } - }); - createCellEditWrapper(props); - instance.handleCellUpdate(row, column, newValue); - }); - - it('should calling cellEdit.beforeSaveCell correctly', () => { - expect(beforeSaveCellCallBack.callCount).toBe(1); - expect(beforeSaveCellCallBack.calledWith( - row[column.dataField], newValue, row, column) - ).toBe(true); - }); - }); - }); -}); diff --git a/packages/react-bootstrap-table2-example/.storybook/preview-head.html b/packages/react-bootstrap-table2-example/.storybook/preview-head.html index edb9231..4eed7a3 100644 --- a/packages/react-bootstrap-table2-example/.storybook/preview-head.html +++ b/packages/react-bootstrap-table2-example/.storybook/preview-head.html @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/react-bootstrap-table2-example/.storybook/webpack.config.js b/packages/react-bootstrap-table2-example/.storybook/webpack.config.js index a2c12e2..a3995c8 100644 --- a/packages/react-bootstrap-table2-example/.storybook/webpack.config.js +++ b/packages/react-bootstrap-table2-example/.storybook/webpack.config.js @@ -8,6 +8,7 @@ const editorSourcePath = path.join(__dirname, '../../react-bootstrap-table2-edit const sourceStylePath = path.join(__dirname, '../../react-bootstrap-table2/style'); const paginationStylePath = path.join(__dirname, '../../react-bootstrap-table2-paginator/style'); const filterStylePath = path.join(__dirname, '../../react-bootstrap-table2-filter/style'); +const toolkitSourcePath = path.join(__dirname, '../../react-bootstrap-table2-toolkit/index.js'); const storyPath = path.join(__dirname, '../stories'); const examplesPath = path.join(__dirname, '../examples'); const srcPath = path.join(__dirname, '../src'); @@ -23,6 +24,7 @@ const aliasPath = { 'react-bootstrap-table2-filter': filterSourcePath, 'react-bootstrap-table2-overlay': overlaySourcePath, 'react-bootstrap-table2-paginator': paginationSourcePath, + 'react-bootstrap-table2-toolkit': toolkitSourcePath }; const loaders = [{ diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/clear-all-filters.js b/packages/react-bootstrap-table2-example/examples/column-filter/clear-all-filters.js new file mode 100644 index 0000000..d0005fd --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/column-filter/clear-all-filters.js @@ -0,0 +1,86 @@ +import React from 'react'; +import BootstrapTable from 'react-bootstrap-table-next'; +import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const products = productsGenerator(8); + +let nameFilter; +let priceFilter; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name', + filter: textFilter({ + getFilter: (filter) => { + nameFilter = filter; + } + }) +}, { + dataField: 'price', + text: 'Product Price', + filter: textFilter({ + getFilter: (filter) => { + priceFilter = filter; + } + }) +}]; + +const handleClick = () => { + nameFilter(''); + priceFilter(''); +}; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; + +let nameFilter; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name', + filter: textFilter({ + getFilter: (filter) => { + // nameFilter was assigned once the component has been mounted. + nameFilter = filter; + } + }) +}, { + dataField: 'price', + text: 'Product Price', + filter: textFilter() +}]; + +const handleClick = () => { + nameFilter(0); +}; + +export default () => ( +
+ + + +
+); +`; + +export default () => ( +
+ + + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/custom-date-filter.js b/packages/react-bootstrap-table2-example/examples/column-filter/custom-date-filter.js index 226708d..bb04522 100644 --- a/packages/react-bootstrap-table2-example/examples/column-filter/custom-date-filter.js +++ b/packages/react-bootstrap-table2-example/examples/column-filter/custom-date-filter.js @@ -15,6 +15,7 @@ const columns = [{ }, { dataField: 'inStockDate', text: 'InStock Date', + formatter: cell => cell.toString(), filter: dateFilter({ delay: 400, placeholder: 'custom placeholder', diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/custom-filter-value.js b/packages/react-bootstrap-table2-example/examples/column-filter/custom-filter-value.js index 0901eb6..bfc9383 100644 --- a/packages/react-bootstrap-table2-example/examples/column-filter/custom-filter-value.js +++ b/packages/react-bootstrap-table2-example/examples/column-filter/custom-filter-value.js @@ -3,9 +3,9 @@ import React from 'react'; import BootstrapTable from 'react-bootstrap-table-next'; import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; import Code from 'components/common/code-block'; -import { jobsGenerator } from 'utils/common'; +import { jobsGenerator1 } from 'utils/common'; -const jobs = jobsGenerator(5); +const jobs = jobsGenerator1(5); const owners = ['Allen', 'Bob', 'Cat']; const types = ['Cloud Service', 'Message Service', 'Add Service', 'Edit Service', 'Money']; diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/date-filter-default-value.js b/packages/react-bootstrap-table2-example/examples/column-filter/date-filter-default-value.js index c292531..2fcba33 100644 --- a/packages/react-bootstrap-table2-example/examples/column-filter/date-filter-default-value.js +++ b/packages/react-bootstrap-table2-example/examples/column-filter/date-filter-default-value.js @@ -15,6 +15,7 @@ const columns = [{ }, { dataField: 'inStockDate', text: 'InStock Date', + formatter: cell => cell.toString(), filter: dateFilter({ defaultValue: { date: new Date(2018, 0, 1), comparator: Comparator.GT } }) diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/date-filter.js b/packages/react-bootstrap-table2-example/examples/column-filter/date-filter.js index 24cd05b..85bcb89 100644 --- a/packages/react-bootstrap-table2-example/examples/column-filter/date-filter.js +++ b/packages/react-bootstrap-table2-example/examples/column-filter/date-filter.js @@ -15,6 +15,7 @@ const columns = [{ }, { dataField: 'inStockDate', text: 'InStock Date', + formatter: cell => cell.toString(), filter: dateFilter() }]; diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/programmatically-date-filter.js b/packages/react-bootstrap-table2-example/examples/column-filter/programmatically-date-filter.js index d620120..7b25667 100644 --- a/packages/react-bootstrap-table2-example/examples/column-filter/programmatically-date-filter.js +++ b/packages/react-bootstrap-table2-example/examples/column-filter/programmatically-date-filter.js @@ -17,6 +17,7 @@ const columns = [{ }, { dataField: 'inStockDate', text: 'InStock Date', + formatter: cell => cell.toString(), filter: dateFilter({ getFilter: (filter) => { // inStockDateFilter was assigned once the component has been mounted. diff --git a/packages/react-bootstrap-table2-example/examples/csv/csv-column-formatter.js b/packages/react-bootstrap-table2-example/examples/csv/csv-column-formatter.js new file mode 100644 index 0000000..3e4c018 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/csv/csv-column-formatter.js @@ -0,0 +1,80 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +/* eslint no-unused-vars: 0 */ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const { ExportCSVButton } = CSVExport; +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price', + csvFormatter: (cell, row, rowIndex) => `$ ${cell}NTD` +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; + +const { ExportCSVButton } = CSVExport; +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price', + csvFormatter: (cell, row, rowIndex) => \`$ \${cell}NTD\` +}]; + + + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+`; + +export default () => ( +
+ + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/csv/csv-column-type.js b/packages/react-bootstrap-table2-example/examples/csv/csv-column-type.js new file mode 100644 index 0000000..b487bd2 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/csv/csv-column-type.js @@ -0,0 +1,79 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const { ExportCSVButton } = CSVExport; +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price', + csvType: Number +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; + +const { ExportCSVButton } = CSVExport; +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price', + csvType: Number +}]; + + + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+`; + +export default () => ( +
+ + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/csv/custom-csv-button.js b/packages/react-bootstrap-table2-example/examples/csv/custom-csv-button.js new file mode 100644 index 0000000..6977995 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/csv/custom-csv-button.js @@ -0,0 +1,97 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider from 'react-bootstrap-table2-toolkit'; +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 = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider from 'react-bootstrap-table2-toolkit'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const MyExportCSV = (props) => { + const handleClick = () => { + props.onExport(); + }; + return ( +
+ +
+ ); +}; + + + { + props => ( +
+ +
+ +
+ ) + } +
+`; + +const MyExportCSV = (props) => { + const handleClick = () => { + props.onExport(); + }; + return ( +
+ +
+ ); +}; + +export default () => ( +
+ + { + props => ( +
+ +
+ +
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/csv/custom-csv-header.js b/packages/react-bootstrap-table2-example/examples/csv/custom-csv-header.js new file mode 100644 index 0000000..284e7ac --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/csv/custom-csv-header.js @@ -0,0 +1,83 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const { ExportCSVButton } = CSVExport; +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID', + csvText: 'CSV Product ID' +}, { + dataField: 'name', + text: 'Product Name', + csvText: 'CSV Product Name' +}, { + dataField: 'price', + text: 'Product Price', + csvText: 'CSV Product price' +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; + +const { ExportCSVButton } = CSVExport; +const columns = [{ + dataField: 'id', + text: 'Product ID', + csvText: 'CSV Product ID' +}, { + dataField: 'name', + text: 'Product Name', + csvText: 'CSV Product Name' +}, { + dataField: 'price', + text: 'Product Price', + csvText: 'CSV Product price' +}]; + + + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+`; + +export default () => ( +
+ + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/csv/custom-csv.js b/packages/react-bootstrap-table2-example/examples/csv/custom-csv.js new file mode 100644 index 0000000..a78d8b0 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/csv/custom-csv.js @@ -0,0 +1,82 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const { ExportCSVButton } = CSVExport; +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; + +const { ExportCSVButton } = CSVExport; +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + + + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+`; + +export default () => ( +
+ + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/csv/hide-column.js b/packages/react-bootstrap-table2-example/examples/csv/hide-column.js new file mode 100644 index 0000000..4f7d05a --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/csv/hide-column.js @@ -0,0 +1,79 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const { ExportCSVButton } = CSVExport; +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name', + csvExport: false +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; + +const { ExportCSVButton } = CSVExport; +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name', + csvExport: false +}, { + dataField: 'price', + text: 'Product Price' +}]; + + + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+`; + +export default () => ( +
+ + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/csv/index.js b/packages/react-bootstrap-table2-example/examples/csv/index.js new file mode 100644 index 0000000..5c940cc --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/csv/index.js @@ -0,0 +1,77 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const { ExportCSVButton } = CSVExport; +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { CSVExport } from 'react-bootstrap-table2-toolkit'; + +const { ExportCSVButton } = CSVExport; +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + + + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+`; + +export default () => ( +
+ + { + props => ( +
+ Export CSV!! +
+ +
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/remote/remote-all.js b/packages/react-bootstrap-table2-example/examples/remote/remote-all.js index 2720584..1b479f5 100644 --- a/packages/react-bootstrap-table2-example/examples/remote/remote-all.js +++ b/packages/react-bootstrap-table2-example/examples/remote/remote-all.js @@ -4,53 +4,76 @@ import React from 'react'; import PropTypes from 'prop-types'; import BootstrapTable from 'react-bootstrap-table-next'; import paginationFactory from 'react-bootstrap-table2-paginator'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import filterFactory, { textFilter, Comparator } from 'react-bootstrap-table2-filter'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; -const products = productsGenerator(87); +let products = productsGenerator(87); const columns = [{ dataField: 'id', - text: 'Product ID' + text: 'Product ID', + sort: true }, { dataField: 'name', text: 'Product Name', - filter: textFilter() + filter: textFilter({ + defaultValue: '8' + }), + sort: true }, { dataField: 'price', text: 'Product Price', - filter: textFilter() + filter: textFilter(), + sort: true }]; const sourceCode = `\ import BootstrapTable from 'react-bootstrap-table-next'; import paginationFactory from 'react-bootstrap-table2-paginator'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import filterFactory, { textFilter, Comparator } from 'react-bootstrap-table2-filter'; // ... const columns = [{ dataField: 'id', - text: 'Product ID' + text: 'Product ID', + sort: true }, { dataField: 'name', text: 'Product Name', - filter: textFilter() + filter: textFilter({ + defaultValue: '8' + }), + sort: true }, { dataField: 'price', text: 'Product Price', - filter: textFilter() + filter: textFilter(), + sort: true }]; +const defaultSorted = [{ + dataField: 'name', + order: 'desc' +}]; + +const cellEditProps = { + mode: 'click' +}; + const RemoteAll = ({ data, page, sizePerPage, onTableChange, totalSize }) => (
{ sourceCode } @@ -77,10 +100,25 @@ class Container extends React.Component { this.handleTableChange = this.handleTableChange.bind(this); } - handleTableChange = (type, { page, sizePerPage, filters }) => { + handleTableChange = (type, { page, sizePerPage, filters, sortField, sortOrder, cellEdit }) => { const currentIndex = (page - 1) * sizePerPage; setTimeout(() => { - const result = products.filter((row) => { + // Handle cell editing + if (type === 'cellEdit') { + const { rowId, dataField, newValue } = cellEdit; + products = products.map((row) => { + if (row.id === rowId) { + const newRow = { ...row }; + newRow[dataField] = newValue; + return newRow; + } + return row; + }); + } + let result = products; + + // Handle column filters + result = result.filter((row) => { let valid = true; for (const dataField in filters) { const { filterVal, filterType, comparator } = filters[dataField]; @@ -96,6 +134,26 @@ class Container extends React.Component { } return valid; }); + // Handle column sort + if (sortOrder === 'asc') { + result = result.sort((a, b) => { + if (a[sortField] > b[sortField]) { + return 1; + } else if (b[sortField] > a[sortField]) { + return -1; + } + return 0; + }); + } else { + result = result.sort((a, b) => { + if (a[sortField] > b[sortField]) { + return -1; + } else if (b[sortField] > a[sortField]) { + return 1; + } + return 0; + }); + } this.setState(() => ({ page, data: result.slice(currentIndex, currentIndex + sizePerPage), @@ -120,18 +178,29 @@ class Container extends React.Component { } `; +const defaultSorted = [{ + dataField: 'name', + order: 'desc' +}]; + +const cellEditProps = { + mode: 'click' +}; + const RemoteAll = ({ data, page, sizePerPage, onTableChange, totalSize }) => (

When remote.pagination is enabled, the filtering, sorting and searching will also change to remote mode automatically

{ sourceCode }
@@ -157,10 +226,24 @@ class Container extends React.Component { this.handleTableChange = this.handleTableChange.bind(this); } - handleTableChange = (type, { page, sizePerPage, filters }) => { + handleTableChange = (type, { page, sizePerPage, filters, sortField, sortOrder, cellEdit }) => { const currentIndex = (page - 1) * sizePerPage; setTimeout(() => { - const result = products.filter((row) => { + // Handle cell editing + if (type === 'cellEdit') { + const { rowId, dataField, newValue } = cellEdit; + products = products.map((row) => { + if (row.id === rowId) { + const newRow = { ...row }; + newRow[dataField] = newValue; + return newRow; + } + return row; + }); + } + let result = products; + // Handle column filters + result = result.filter((row) => { let valid = true; for (const dataField in filters) { const { filterVal, filterType, comparator } = filters[dataField]; @@ -176,6 +259,26 @@ class Container extends React.Component { } return valid; }); + // Handle column sort + if (sortOrder === 'asc') { + result = result.sort((a, b) => { + if (a[sortField] > b[sortField]) { + return 1; + } else if (b[sortField] > a[sortField]) { + return -1; + } + return 0; + }); + } else { + result = result.sort((a, b) => { + if (a[sortField] > b[sortField]) { + return -1; + } else if (b[sortField] > a[sortField]) { + return 1; + } + return 0; + }); + } this.setState(() => ({ page, data: result.slice(currentIndex, currentIndex + sizePerPage), diff --git a/packages/react-bootstrap-table2-example/examples/remote/remote-search.js b/packages/react-bootstrap-table2-example/examples/remote/remote-search.js new file mode 100644 index 0000000..ad4d5a1 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/remote/remote-search.js @@ -0,0 +1,175 @@ +/* eslint guard-for-in: 0 */ +/* eslint no-restricted-syntax: 0 */ +import React from 'react'; +import PropTypes from 'prop-types'; +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const { SearchBar } = Search; +const products = productsGenerator(17); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit'; +import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; + +const columns = [{ + dataField: 'id', + text: 'Product ID', +}, { + dataField: 'name', + text: 'Product Name', + filter: textFilter() +}, { + dataField: 'price', + text: 'Product Price', + filter: textFilter() +}]; + +const RemoteFilter = props => ( +
+ + { + toolkitprops => [ + , + + ] + } + + { sourceCode } +
+); + +class Container extends React.Component { + constructor(props) { + super(props); + this.state = { + data: products + }; + } + + handleTableChange = (type, { filters }) => { + setTimeout(() => { + const result = products.filter((row) => { + let valid = true; + for (const dataField in filters) { + const { filterVal, filterType, comparator } = filters[dataField]; + + if (filterType === 'TEXT') { + if (comparator === Comparator.LIKE) { + valid = row[dataField].toString().indexOf(filterVal) > -1; + } else { + valid = row[dataField] === filterVal; + } + } + if (!valid) break; + } + return valid; + }); + this.setState(() => ({ + data: result + })); + }, 2000); + } + + render() { + return ( + + ); + } +} +`; + +const RemoteFilter = props => ( +
+ + { + toolkitprops => [ + , + + ] + } + + { sourceCode } +
+); + +RemoteFilter.propTypes = { + data: PropTypes.array.isRequired, + onTableChange: PropTypes.func.isRequired +}; + +class Container extends React.Component { + constructor(props) { + super(props); + this.state = { + data: products + }; + } + + handleTableChange = (type, { searchText }) => { + setTimeout(() => { + const result = products.filter((row) => { + for (let cidx = 0; cidx < columns.length; cidx += 1) { + const column = columns[cidx]; + let targetValue = row[column.dataField]; + if (targetValue !== null && typeof targetValue !== 'undefined') { + targetValue = targetValue.toString().toLowerCase(); + if (targetValue.indexOf(searchText) > -1) { + return true; + } + } + } + return false; + }); + this.setState(() => ({ + data: result + })); + }, 2000); + } + + render() { + return ( + + ); + } +} + +export default Container; diff --git a/packages/react-bootstrap-table2-example/examples/row-expand/custom-expand-column.js b/packages/react-bootstrap-table2-example/examples/row-expand/custom-expand-column.js new file mode 100644 index 0000000..067046d --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/row-expand/custom-expand-column.js @@ -0,0 +1,107 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import Code from 'components/common/code-block'; +import { productsExpandRowsGenerator } from 'utils/common'; + +const products = productsExpandRowsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const expandRow = { + renderer: row => ( +
+

{ `This Expand row is belong to rowKey ${row.id}` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ), + showExpandColumn: true, + expandHeaderColumnRenderer: ({ isAnyExpands }) => { + if (isAnyExpands) { + return -; + } + return +; + }, + expandColumnRenderer: ({ expanded }) => { + if (expanded) { + return ( + - + ); + } + return ( + ... + ); + } +}; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const expandRow = { + renderer: row => ( +
+

{ \`This Expand row is belong to rowKey $\{row.id}\` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ), + showExpandColumn: true, + expandHeaderColumnRenderer: ({ isAnyExpands }) => { + if (isAnyExpands) { + return -; + } + return +; + }, + expandColumnRenderer: ({ expanded }) => { + if (expanded) { + return ( + - + ); + } + return ( + ... + ); + } +}; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/row-expand/expand-column.js b/packages/react-bootstrap-table2-example/examples/row-expand/expand-column.js new file mode 100644 index 0000000..edfff16 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/row-expand/expand-column.js @@ -0,0 +1,74 @@ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import Code from 'components/common/code-block'; +import { productsExpandRowsGenerator } from 'utils/common'; + +const products = productsExpandRowsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const expandRow = { + renderer: row => ( +
+

{ `This Expand row is belong to rowKey ${row.id}` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ), + showExpandColumn: true +}; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const expandRow = { + renderer: row => ( +
+

{ \`This Expand row is belong to rowKey $\{row.id}\` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ), + showExpandColumn: true +}; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/row-expand/expand-hooks.js b/packages/react-bootstrap-table2-example/examples/row-expand/expand-hooks.js new file mode 100644 index 0000000..96fcbfe --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/row-expand/expand-hooks.js @@ -0,0 +1,97 @@ +/* eslint no-console: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import Code from 'components/common/code-block'; +import { productsExpandRowsGenerator } from 'utils/common'; + +const products = productsExpandRowsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const expandRow = { + renderer: row => ( +
+

{ `This Expand row is belong to rowKey ${row.id}` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ), + showExpandColumn: true, + onExpand: (row, isExpand, rowIndex, e) => { + console.log(row.id); + console.log(isExpand); + console.log(rowIndex); + console.log(e); + }, + onExpandAll: (isExpandAll, rows, e) => { + console.log(isExpandAll); + console.log(rows); + console.log(e); + } +}; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const expandRow = { + renderer: row => ( +
+

{ \`This Expand row is belong to rowKey $\{row.id}\` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ), + showExpandColumn: true, + onExpand: (row, isExpand, rowIndex, e) => { + console.log(row.id); + console.log(isExpand); + console.log(rowIndex); + console.log(e); + }, + onExpandAll: (isExpandAll, rows, e) => { + console.log(isExpandAll); + console.log(rows); + console.log(e); + } +}; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/row-expand/expand-management.js b/packages/react-bootstrap-table2-example/examples/row-expand/expand-management.js new file mode 100644 index 0000000..83b1588 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/row-expand/expand-management.js @@ -0,0 +1,138 @@ +/* eslint no-unused-vars: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import Code from 'components/common/code-block'; +import { productsExpandRowsGenerator } from 'utils/common'; + +const products = productsExpandRowsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +class RowExpandManagment extends React.Component { + constructor(props) { + super(props); + this.state = { expanded: [0, 1] }; + } + + handleBtnClick = () => { + if (!this.state.expanded.includes(2)) { + this.setState(() => ({ + expanded: [...this.state.expanded, 2] + })); + } else { + this.setState(() => ({ + expanded: this.state.expanded.filter(x => x !== 2) + })); + } + } + + handleOnExpand = (row, isExpand, rowIndex, e) => { + if (isExpand) { + this.setState(() => ({ + expanded: [...this.state.expanded, row.id] + })); + } else { + this.setState(() => ({ + expanded: this.state.expanded.filter(x => x !== row.id) + })); + } + } + + render() { + const expandRow = { + renderer: row => ( +
+

{ \`This Expand row is belong to rowKey $\{row.id}\` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ), + expanded: this.state.expanded, + onExpand: this.handleOnExpand + }; + return ( +
+ + + { sourceCode } +
+ ); + } +} +`; + +export default class RowExpandManagment extends React.Component { + constructor(props) { + super(props); + this.state = { expanded: [0, 1] }; + } + + handleBtnClick = () => { + if (!this.state.expanded.includes(2)) { + this.setState(() => ({ + expanded: [...this.state.expanded, 2] + })); + } else { + this.setState(() => ({ + expanded: this.state.expanded.filter(x => x !== 2) + })); + } + } + + handleOnExpand = (row, isExpand, rowIndex, e) => { + if (isExpand) { + this.setState(() => ({ + expanded: [...this.state.expanded, row.id] + })); + } else { + this.setState(() => ({ + expanded: this.state.expanded.filter(x => x !== row.id) + })); + } + } + + render() { + const expandRow = { + renderer: row => ( +
+

{ `This Expand row is belong to rowKey ${row.id}` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ), + expanded: this.state.expanded, + onExpand: this.handleOnExpand + }; + return ( +
+ + + { sourceCode } +
+ ); + } +} diff --git a/packages/react-bootstrap-table2-example/examples/row-expand/index.js b/packages/react-bootstrap-table2-example/examples/row-expand/index.js new file mode 100644 index 0000000..0a08904 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/row-expand/index.js @@ -0,0 +1,72 @@ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import Code from 'components/common/code-block'; +import { productsExpandRowsGenerator } from 'utils/common'; + +const products = productsExpandRowsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const expandRow = { + renderer: row => ( +
+

{ `This Expand row is belong to rowKey ${row.id}` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ) +}; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const expandRow = { + renderer: row => ( +
+

{ \`This Expand row is belong to rowKey $\{row.id}\` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ) +}; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/row-expand/non-expandable-rows.js b/packages/react-bootstrap-table2-example/examples/row-expand/non-expandable-rows.js new file mode 100644 index 0000000..6d1ef32 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/row-expand/non-expandable-rows.js @@ -0,0 +1,75 @@ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import Code from 'components/common/code-block'; +import { productsExpandRowsGenerator } from 'utils/common'; + +const products = productsExpandRowsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const expandRow = { + renderer: row => ( +
+

{ `This Expand row is belong to rowKey ${row.id}` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ), + nonExpandable: [1, 3] +}; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const expandRow = { + renderer: row => ( +
+

{ \`This Expand row is belong to rowKey $\{row.id}\` }

+

You can render anything here, also you can add additional data on every row object

+

expandRow.renderer callback will pass the origin row object to you

+
+ ), + nonExpandable: [1, 3] +}; + + +`; + +export default () => ( +
+

The second and fourth row is not expandable

+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/search/custom-search-value.js b/packages/react-bootstrap-table2-example/examples/search/custom-search-value.js new file mode 100644 index 0000000..fcb4d19 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/search/custom-search-value.js @@ -0,0 +1,102 @@ +/* eslint react/prop-types: 0 */ +/* eslint no-unused-vars: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { jobsGenerator1 } from 'utils/common'; + +const { SearchBar } = Search; +const products = jobsGenerator1(5); + +const owners = ['Allen', 'Bob', 'Cat']; +const types = ['Cloud Service', 'Message Service', 'Add Service', 'Edit Service', 'Money']; + +const columns = [{ + dataField: 'id', + text: 'Job ID', + searchable: false, + hidden: true +}, { + dataField: 'owner', + text: 'Job Owner', + formatter: (cell, row) => owners[cell], + filterValue: (cell, row) => owners[cell] // we will search the value after filterValue called +}, { + dataField: 'type', + text: 'Job Type', + formatter: (cell, row) => types[cell], + filterValue: (cell, row) => types[cell] // we will search the value after filterValue called +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit'; + +const { SearchBar } = Search; +const owners = ['Allen', 'Bob', 'Cat']; +const types = ['Cloud Service', 'Message Service', 'Add Service', 'Edit Service', 'Money']; + +const columns = [{ + dataField: 'id', + text: 'Job ID', + searchable: false, + hidden: true +}, { + dataField: 'owner', + text: 'Job Owner', + formatter: (cell, row) => owners[cell], + filterValue: (cell, row) => owners[cell] // we will search the value after filterValue called +}, { + dataField: 'type', + text: 'Job Type', + formatter: (cell, row) => types[cell], + filterValue: (cell, row) => types[cell] // we will search the value after filterValue called +}]; + + + { + props => ( +
+

Try to Search Bob, Cat or Allen instead of 0, 1 or 2

+ +
+ +
+ ) + } +
+`; + +export default () => ( +
+ + { + props => ( +
+

Try to Search Bob, Cat or Allen instead of 0, 1 or 2

+ +
+ +
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/search/default-custom-search.js b/packages/react-bootstrap-table2-example/examples/search/default-custom-search.js new file mode 100644 index 0000000..3e146db --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/search/default-custom-search.js @@ -0,0 +1,95 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const { SearchBar } = Search; +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit'; + +const { SearchBar } = Search; +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + + + { + props => ( +
+

Input something at below input field:

+ +
+ +
+ ) + } +
+`; + +export default () => ( +
+ + { + props => ( +
+

Input something at below input field:

+ +
+ +
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/search/fully-custom-search.js b/packages/react-bootstrap-table2-example/examples/search/fully-custom-search.js new file mode 100644 index 0000000..78bf504 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/search/fully-custom-search.js @@ -0,0 +1,116 @@ +/* eslint react/prop-types: 0 */ +/* eslint no-return-assign: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider from 'react-bootstrap-table2-toolkit'; +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 = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider from 'react-bootstrap-table2-toolkit'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const MySearch = (props) => { + let input; + const handleClick = () => { + props.onSearch(input.value); + }; + return ( +
+ input = n } + type="text" + /> + +
+ ); +}; + + + { + props => ( +
+ + +
+
+ ) + } +
+`; + +const MySearch = (props) => { + let input; + const handleClick = () => { + props.onSearch(input.value); + }; + return ( +
+ input = n } + type="text" + /> + +
+ ); +}; + +export default () => ( +
+ + { + props => ( +
+ + +
+
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/search/index.js b/packages/react-bootstrap-table2-example/examples/search/index.js new file mode 100644 index 0000000..3a8effc --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/search/index.js @@ -0,0 +1,83 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const { SearchBar } = Search; +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit'; + +const { SearchBar } = Search; +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + + + { + props => ( +
+

Input something at below input field:

+ +
+ +
+ ) + } +
+`; + +export default () => ( +
+ + { + props => ( +
+

Input something at below input field:

+ +
+ +
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/search/search-formatted.js b/packages/react-bootstrap-table2-example/examples/search/search-formatted.js new file mode 100644 index 0000000..71575ff --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/search/search-formatted.js @@ -0,0 +1,85 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const { SearchBar } = Search; +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price', + formatter: cell => `USD ${cell}` +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitProvider, { Search } from 'react-bootstrap-table2-toolkit'; + +const { SearchBar } = Search; +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price', + formatter: cell => \`USD \${cell}\` // we will search the data after formatted +}]; + + + { + props => ( +
+

Try to Search USD at below input field:

+ +
+ +
+ ) + } +
+`; + +export default () => ( +
+ + { + props => ( +
+

Try to Search USD at below input field:

+ +
+ +
+ ) + } +
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/package.json b/packages/react-bootstrap-table2-example/package.json index 657a8cd..be6d702 100644 --- a/packages/react-bootstrap-table2-example/package.json +++ b/packages/react-bootstrap-table2-example/package.json @@ -14,8 +14,8 @@ "license": "ISC", "peerDependencies": { "prop-types": "^15.0.0", - "react": "^15.0.0", - "react-dom": "^15.0.0" + "react": "^16.3.0", + "react-dom": "^116.3.0" }, "dependencies": { "bootstrap": "^3.3.7" diff --git a/packages/react-bootstrap-table2-example/src/utils/common.js b/packages/react-bootstrap-table2-example/src/utils/common.js index b8f954e..dbf7b7e 100644 --- a/packages/react-bootstrap-table2-example/src/utils/common.js +++ b/packages/react-bootstrap-table2-example/src/utils/common.js @@ -41,6 +41,14 @@ export const jobsGenerator = (quantity = 5) => type: jobType[Math.floor((Math.random() * 4) + 1)] })); +export const jobsGenerator1 = (quantity = 5) => + Array.from({ length: quantity }, (value, index) => ({ + id: index, + name: `Job name ${index}`, + owner: Math.floor((Math.random() * 2) + 1), + type: Math.floor((Math.random() * 4) + 1) + })); + export const todosGenerator = (quantity = 5) => Array.from({ length: quantity }, (value, index) => ({ id: index, @@ -60,3 +68,17 @@ export const stockGenerator = (quantity = 5) => })); export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +export const productsExpandRowsGenerator = (quantity = 5, callback) => { + if (callback) return Array.from({ length: quantity }, callback); + + // if no given callback, retrun default product format. + return ( + Array.from({ length: quantity }, (value, index) => ({ + id: index, + name: `Item name ${index}`, + price: 2100 + index, + expand: productsQualityGenerator(index) + })) + ); +}; diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js index 0617922..eee2c87 100644 --- a/packages/react-bootstrap-table2-example/stories/index.js +++ b/packages/react-bootstrap-table2-example/stories/index.js @@ -62,6 +62,7 @@ import ProgrammaticallyDateFilter from 'examples/column-filter/programmatically- import ProgrammaticallyMultiSelectFilter from 'examples/column-filter/programmatically-multi-select-filter'; import CustomFilter from 'examples/column-filter/custom-filter'; import AdvanceCustomFilter from 'examples/column-filter/advance-custom-filter'; +import ClearAllFilters from 'examples/column-filter/clear-all-filters'; // work on rows import RowStyleTable from 'examples/rows/row-style'; @@ -112,11 +113,35 @@ import SelectionBgColorTable from 'examples/row-selection/selection-bgcolor'; import SelectionHooks from 'examples/row-selection/selection-hooks'; import HideSelectionColumnTable from 'examples/row-selection/hide-selection-column'; +// work on row expand +import BasicRowExpand from 'examples/row-expand'; +import RowExpandManagement from 'examples/row-expand/expand-management'; +import NonExpandableRows from 'examples/row-expand/non-expandable-rows'; +import ExpandColumn from 'examples/row-expand/expand-column'; +import CustomExpandColumn from 'examples/row-expand/custom-expand-column'; +import ExpandHooks from 'examples/row-expand/expand-hooks'; + // pagination import PaginationTable from 'examples/pagination'; import PaginationHooksTable from 'examples/pagination/pagination-hooks'; import CustomPaginationTable from 'examples/pagination/custom-pagination'; +// search +import SearchTable from 'examples/search'; +import DefaultCustomSearch from 'examples/search/default-custom-search'; +import FullyCustomSearch from 'examples/search/fully-custom-search'; +import SearchFormattedData from 'examples/search/search-formatted'; +import CustomSearchValue from 'examples/search/custom-search-value'; + +// CSV +import ExportCSV from 'examples/csv'; +import CSVFormatter from 'examples/csv/csv-column-formatter'; +import CustomCSVHeader from 'examples/csv/custom-csv-header'; +import HideCSVColumn from 'examples/csv/hide-column'; +import CSVColumnType from 'examples/csv/csv-column-type'; +import CustomCSVButton from 'examples/csv/custom-csv-button'; +import CustomCSV from 'examples/csv/custom-csv'; + // loading overlay import EmptyTableOverlay from 'examples/loading-overlay/empty-table-overlay'; import TableOverlay from 'examples/loading-overlay/table-overlay'; @@ -125,6 +150,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 RemoteSearch from 'examples/remote/remote-search'; import RemoteCellEdit from 'examples/remote/remote-celledit'; import RemoteAll from 'examples/remote/remote-all'; @@ -200,7 +226,8 @@ storiesOf('Column Filter', module) .add('Programmatically Date Filter', () => ) .add('Programmatically Multi Select Filter', () => ) .add('Custom Filter', () => ) - .add('Advance Custom Filter', () => ); + .add('Advance Custom Filter', () => ) + .add('Clear All Filters', () => ); storiesOf('Work on Rows', module) .add('Customize Row Style', () => ) @@ -251,11 +278,35 @@ storiesOf('Row Selection', module) .add('Selection Hooks', () => ) .add('Hide Selection Column', () => ); +storiesOf('Row Expand', module) + .add('Basic Row Expand', () => ) + .add('Expand Management', () => ) + .add('Non Expandabled Rows', () => ) + .add('Expand Indicator', () => ) + .add('Custom Expand Indicator', () => ) + .add('Expand Hooks', () => ); + storiesOf('Pagination', module) .add('Basic Pagination Table', () => ) .add('Pagination Hooks', () => ) .add('Custom Pagination', () => ); +storiesOf('Table Search', module) + .add('Basic Search Table', () => ) + .add('Default Custom Search', () => ) + .add('Fully Custom Search', () => ) + .add('Search Fromatted Value', () => ) + .add('Custom Search Value', () => ); + +storiesOf('Export CSV', module) + .add('Basic Export CSV', () => ) + .add('Format CSV Column', () => ) + .add('Custom CSV Header', () => ) + .add('Hide CSV Column', () => ) + .add('CSV Column Type', () => ) + .add('Custom CSV Button', () => ) + .add('Custom CSV', () => ); + storiesOf('EmptyTableOverlay', module) .add('Empty Table Overlay', () => ) .add('Table Overlay', () => ); @@ -264,5 +315,6 @@ storiesOf('Remote', module) .add('Remote Sort', () => ) .add('Remote Filter', () => ) .add('Remote Pagination', () => ) + .add('Remote Search', () => ) .add('Remote Cell Editing', () => ) .add('Remote All', () => ); diff --git a/packages/react-bootstrap-table2-example/stories/stylesheet/search/_index.scss b/packages/react-bootstrap-table2-example/stories/stylesheet/search/_index.scss new file mode 100644 index 0000000..8c47ed2 --- /dev/null +++ b/packages/react-bootstrap-table2-example/stories/stylesheet/search/_index.scss @@ -0,0 +1,3 @@ +.custome-search-field { + background-color: #c8e6c9; +} \ No newline at end of file diff --git a/packages/react-bootstrap-table2-example/stories/stylesheet/storybook.scss b/packages/react-bootstrap-table2-example/stories/stylesheet/storybook.scss index c38e02c..14959a6 100644 --- a/packages/react-bootstrap-table2-example/stories/stylesheet/storybook.scss +++ b/packages/react-bootstrap-table2-example/stories/stylesheet/storybook.scss @@ -10,4 +10,5 @@ @import "row-selection/index"; @import "rows/index"; @import "sort/index"; +@import "search/index"; @import "loading-overlay/index"; diff --git a/packages/react-bootstrap-table2-filter/README.md b/packages/react-bootstrap-table2-filter/README.md index 38c278e..05c7c9e 100644 --- a/packages/react-bootstrap-table2-filter/README.md +++ b/packages/react-bootstrap-table2-filter/README.md @@ -214,7 +214,7 @@ const columns = [..., { ``` -> **Notes:** date filter accept a Javascript Date object in your raw data. +> **Notes:** date filter accept a Javascript Date object in your raw data and you have to use `column.formatter` to make it as your prefer string result Date filter is same as other filter, you can custom the date filter via `dateFilter` factory function: diff --git a/packages/react-bootstrap-table2-filter/index.js b/packages/react-bootstrap-table2-filter/index.js index 6d9368a..af67dfc 100644 --- a/packages/react-bootstrap-table2-filter/index.js +++ b/packages/react-bootstrap-table2-filter/index.js @@ -3,12 +3,12 @@ import SelectFilter from './src/components/select'; import MultiSelectFilter from './src/components/multiselect'; import NumberFilter from './src/components/number'; import DateFilter from './src/components/date'; -import wrapperFactory from './src/wrapper'; +import createContext from './src/context'; import * as Comparison from './src/comparison'; import { FILTER_TYPE } from './src/const'; export default (options = {}) => ({ - wrapperFactory, + createContext, options }); diff --git a/packages/react-bootstrap-table2-filter/package.json b/packages/react-bootstrap-table2-filter/package.json index e223d02..55bc235 100644 --- a/packages/react-bootstrap-table2-filter/package.json +++ b/packages/react-bootstrap-table2-filter/package.json @@ -38,7 +38,7 @@ ], "peerDependencies": { "prop-types": "^15.0.0", - "react": "^16.0.0", - "react-dom": "^16.0.0" + "react": "^16.3.0", + "react-dom": "^16.3.0" } } diff --git a/packages/react-bootstrap-table2-filter/src/components/date.js b/packages/react-bootstrap-table2-filter/src/components/date.js index 8e31724..be6ecd5 100644 --- a/packages/react-bootstrap-table2-filter/src/components/date.js +++ b/packages/react-bootstrap-table2-filter/src/components/date.js @@ -36,7 +36,7 @@ class DateFilter extends Component { const comparator = this.dateFilterComparator.value; const date = this.inputDate.value; if (comparator && date) { - this.applyFilter(date, comparator); + this.applyFilter(date, comparator, true); } // export onFilter function to allow users to access @@ -92,7 +92,7 @@ class DateFilter extends Component { return defaultDate; } - applyFilter(value, comparator) { + applyFilter(value, comparator, isInitial) { // if (!comparator || !value) { // return; // } @@ -103,7 +103,7 @@ class DateFilter extends Component { // instead of parsing an invalid Date. The filter function will interpret // null as an empty date field const date = value === '' ? null : new Date(value); - onFilter(column, FILTER_TYPE.DATE)({ date, comparator }); + onFilter(column, FILTER_TYPE.DATE, isInitial)({ date, comparator }); }; if (delay) { this.timeout = setTimeout(() => { execute(); }, delay); diff --git a/packages/react-bootstrap-table2-filter/src/components/number.js b/packages/react-bootstrap-table2-filter/src/components/number.js index 960aa5d..447eed2 100644 --- a/packages/react-bootstrap-table2-filter/src/components/number.js +++ b/packages/react-bootstrap-table2-filter/src/components/number.js @@ -36,7 +36,7 @@ class NumberFilter extends Component { const comparator = this.numberFilterComparator.value; const number = this.numberFilter.value; if (comparator && number) { - onFilter(column, FILTER_TYPE.NUMBER)({ number, comparator }); + onFilter(column, FILTER_TYPE.NUMBER, true)({ number, comparator }); } // export onFilter function to allow users to access diff --git a/packages/react-bootstrap-table2-filter/src/components/select.js b/packages/react-bootstrap-table2-filter/src/components/select.js index 7c4d285..e505138 100644 --- a/packages/react-bootstrap-table2-filter/src/components/select.js +++ b/packages/react-bootstrap-table2-filter/src/components/select.js @@ -29,7 +29,7 @@ class SelectFilter extends Component { const value = this.selectInput.value; if (value && value !== '') { - onFilter(column, FILTER_TYPE.SELECT)(value); + onFilter(column, FILTER_TYPE.SELECT, true)(value); } // export onFilter function to allow users to access diff --git a/packages/react-bootstrap-table2-filter/src/components/text.js b/packages/react-bootstrap-table2-filter/src/components/text.js index 3af8ae2..a17c3ea 100644 --- a/packages/react-bootstrap-table2-filter/src/components/text.js +++ b/packages/react-bootstrap-table2-filter/src/components/text.js @@ -23,7 +23,7 @@ class TextFilter extends Component { const defaultValue = this.input.value; if (defaultValue) { - onFilter(this.props.column, FILTER_TYPE.TEXT)(defaultValue); + onFilter(this.props.column, FILTER_TYPE.TEXT, true)(defaultValue); } // export onFilter function to allow users to access diff --git a/packages/react-bootstrap-table2-filter/src/context.js b/packages/react-bootstrap-table2-filter/src/context.js new file mode 100644 index 0000000..790ce00 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/src/context.js @@ -0,0 +1,99 @@ +/* eslint react/prop-types: 0 */ +/* eslint react/require-default-props: 0 */ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { filters } from './filter'; +import { LIKE, EQ } from './comparison'; +import { FILTER_TYPE } from './const'; + +export default ( + _, + isRemoteFiltering, + handleFilterChange +) => { + const FilterContext = React.createContext(); + + class FilterProvider extends React.Component { + static propTypes = { + data: PropTypes.array.isRequired, + columns: PropTypes.array.isRequired + } + + constructor(props) { + super(props); + this.currFilters = {}; + this.onFilter = this.onFilter.bind(this); + this.onExternalFilter = this.onExternalFilter.bind(this); + } + + componentDidMount() { + if (isRemoteFiltering() && Object.keys(this.currFilters).length > 0) { + handleFilterChange(this.currFilters); + } + } + + onFilter(column, filterType, initialize = false) { + return (filterVal) => { + // watch out here if migration to context API, #334 + const currFilters = Object.assign({}, this.currFilters); + const { dataField, filter } = column; + + const needClearFilters = + !_.isDefined(filterVal) || + filterVal === '' || + filterVal.length === 0; + + if (needClearFilters) { + delete currFilters[dataField]; + } else { + // select default comparator is EQ, others are LIKE + const { + comparator = (filterType === FILTER_TYPE.SELECT ? EQ : LIKE), + caseSensitive = false + } = filter.props; + currFilters[dataField] = { filterVal, filterType, comparator, caseSensitive }; + } + + this.currFilters = currFilters; + + if (isRemoteFiltering()) { + if (!initialize) { + handleFilterChange(this.currFilters); + } + return; + } + + this.forceUpdate(); + }; + } + + onExternalFilter(column, filterType) { + return (value) => { + this.onFilter(column, filterType)(value); + }; + } + + render() { + let { data } = this.props; + if (!isRemoteFiltering()) { + data = filters(data, this.props.columns, _)(this.currFilters); + } + return ( + + { this.props.children } + + ); + } + } + + return { + Provider: FilterProvider, + Consumer: FilterContext.Consumer + }; +}; diff --git a/packages/react-bootstrap-table2-filter/src/filter.js b/packages/react-bootstrap-table2-filter/src/filter.js index c1a8666..07454c4 100644 --- a/packages/react-bootstrap-table2-filter/src/filter.js +++ b/packages/react-bootstrap-table2-filter/src/filter.js @@ -229,9 +229,9 @@ export const filterFactory = _ => (filterType) => { return filterFn; }; -export const filters = (store, columns, _) => (currFilters) => { +export const filters = (data, columns, _) => (currFilters) => { const factory = filterFactory(_); - let result = store.getAllData(); + let result = data; let filterFn; Object.keys(currFilters).forEach((dataField) => { const filterObj = currFilters[dataField]; diff --git a/packages/react-bootstrap-table2-filter/src/wrapper.js b/packages/react-bootstrap-table2-filter/src/wrapper.js deleted file mode 100644 index 0602ee8..0000000 --- a/packages/react-bootstrap-table2-filter/src/wrapper.js +++ /dev/null @@ -1,107 +0,0 @@ -/* eslint no-param-reassign: 0 */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { filters } from './filter'; -import { LIKE, EQ } from './comparison'; -import { FILTER_TYPE } from './const'; - -export default (Base, { - _, - remoteResolver -}) => - class FilterWrapper extends remoteResolver(Component) { - static propTypes = { - store: PropTypes.object.isRequired, - columns: PropTypes.array.isRequired - } - - constructor(props) { - super(props); - this.state = { currFilters: {}, isDataChanged: props.isDataChanged || false }; - this.onFilter = this.onFilter.bind(this); - this.onExternalFilter = this.onExternalFilter.bind(this); - } - - componentWillReceiveProps({ isDataChanged, store, columns }) { - // consider to use lodash.isEqual - const isRemoteFilter = this.isRemoteFiltering() || this.isRemotePagination(); - if (isRemoteFilter || - JSON.stringify(this.state.currFilters) !== JSON.stringify(store.filters)) { - // I think this condition only isRemoteFilter is enough - store.filteredData = store.getAllData(); - this.setState(() => ({ isDataChanged: true, currFilters: store.filters })); - } else { - if (Object.keys(this.state.currFilters).length > 0) { - store.filteredData = filters(store, columns, _)(this.state.currFilters); - } - this.setState(() => ({ isDataChanged })); - } - } - - /** - * filter the table like below: - * onFilter(column, filterType)(filterVal) - * @param {Object} column - * @param {String} filterType - * @param {String} filterVal - user input for filtering. - */ - onFilter(column, filterType) { - return (filterVal) => { - const { store, columns } = this.props; - // watch out here if migration to context API, #334 - const currFilters = Object.assign({}, store.filters); - const { dataField, filter } = column; - - const needClearFilters = - !_.isDefined(filterVal) || - filterVal === '' || - filterVal.length === 0; - - if (needClearFilters) { - delete currFilters[dataField]; - } else { - // select default comparator is EQ, others are LIKE - const { - comparator = ( - (filterType === FILTER_TYPE.SELECT) || ( - filterType === FILTER_TYPE.MULTISELECT) ? EQ : LIKE - ), - caseSensitive = false - } = filter.props; - currFilters[dataField] = { filterVal, filterType, comparator, caseSensitive }; - } - - store.filters = currFilters; - - if (this.isRemoteFiltering() || this.isRemotePagination()) { - this.handleRemoteFilterChange(); - // when remote filtering is enable, dont set currFilters state - // in the componentWillReceiveProps, - // it's the key point that we can know the filter is changed - return; - } - - store.filteredData = filters(store, columns, _)(currFilters); - this.setState(() => ({ currFilters, isDataChanged: true })); - }; - } - - onExternalFilter(column, filterType) { - return (value) => { - this.onFilter(column, filterType)(value); - }; - } - - render() { - return ( - - ); - } - }; diff --git a/packages/react-bootstrap-table2-filter/test/components/date.test.js b/packages/react-bootstrap-table2-filter/test/components/date.test.js index 366a675..9fb3776 100644 --- a/packages/react-bootstrap-table2-filter/test/components/date.test.js +++ b/packages/react-bootstrap-table2-filter/test/components/date.test.js @@ -124,7 +124,7 @@ describe('Date Filter', () => { it('should do onFilter correctly when exported function was executed', () => { expect(onFilter).toHaveBeenCalledTimes(1); - expect(onFilter).toHaveBeenCalledWith(column, FILTER_TYPE.DATE); + expect(onFilter).toHaveBeenCalledWith(column, FILTER_TYPE.DATE, undefined); expect(onFilterFirstReturn).toHaveBeenCalledTimes(1); expect(onFilterFirstReturn).toHaveBeenCalledWith({ comparator, date }); }); @@ -148,7 +148,7 @@ describe('Date Filter', () => { it('should calling onFilter on componentDidMount', () => { expect(onFilter).toHaveBeenCalledTimes(1); - expect(onFilter).toHaveBeenCalledWith(column, FILTER_TYPE.DATE); + expect(onFilter).toHaveBeenCalledWith(column, FILTER_TYPE.DATE, true); expect(onFilterFirstReturn).toHaveBeenCalledTimes(1); // expect(onFilterFirstReturn).toHaveBeenCalledWith({ comparator, date }); }); diff --git a/packages/react-bootstrap-table2-filter/test/components/number.test.js b/packages/react-bootstrap-table2-filter/test/components/number.test.js index 3586ec4..1556dac 100644 --- a/packages/react-bootstrap-table2-filter/test/components/number.test.js +++ b/packages/react-bootstrap-table2-filter/test/components/number.test.js @@ -147,7 +147,7 @@ describe('Number Filter', () => { it('should calling onFilter on componentDidMount', () => { expect(onFilter.calledOnce).toBeTruthy(); - expect(onFilter.calledWith(column, FILTER_TYPE.NUMBER)).toBeTruthy(); + expect(onFilter.calledWith(column, FILTER_TYPE.NUMBER, true)).toBeTruthy(); expect(onFilterFirstReturn.calledOnce).toBeTruthy(); expect(onFilterFirstReturn.calledWith({ number: `${number}`, comparator })).toBeTruthy(); }); diff --git a/packages/react-bootstrap-table2-filter/test/components/select.test.js b/packages/react-bootstrap-table2-filter/test/components/select.test.js index 85f9686..80d30d7 100644 --- a/packages/react-bootstrap-table2-filter/test/components/select.test.js +++ b/packages/react-bootstrap-table2-filter/test/components/select.test.js @@ -91,7 +91,7 @@ describe('Select Filter', () => { it('should calling onFilter on componentDidMount', () => { expect(onFilter.calledOnce).toBeTruthy(); - expect(onFilter.calledWith(column, FILTER_TYPE.SELECT)).toBeTruthy(); + expect(onFilter.calledWith(column, FILTER_TYPE.SELECT, true)).toBeTruthy(); expect(onFilterFirstReturn.calledOnce).toBeTruthy(); expect(onFilterFirstReturn.calledWith(defaultValue)).toBeTruthy(); }); diff --git a/packages/react-bootstrap-table2-filter/test/components/text.test.js b/packages/react-bootstrap-table2-filter/test/components/text.test.js index 5a8d297..fbc849f 100644 --- a/packages/react-bootstrap-table2-filter/test/components/text.test.js +++ b/packages/react-bootstrap-table2-filter/test/components/text.test.js @@ -65,7 +65,7 @@ describe('Text Filter', () => { it('should calling onFilter on componentDidMount', () => { expect(onFilter.calledOnce).toBeTruthy(); - expect(onFilter.calledWith(column, FILTER_TYPE.TEXT)).toBeTruthy(); + expect(onFilter.calledWith(column, FILTER_TYPE.TEXT, true)).toBeTruthy(); expect(onFilterFirstReturn.calledOnce).toBeTruthy(); expect(onFilterFirstReturn.calledWith(defaultValue)).toBeTruthy(); }); diff --git a/packages/react-bootstrap-table2-filter/test/context.test.js b/packages/react-bootstrap-table2-filter/test/context.test.js new file mode 100644 index 0000000..f4b97ca --- /dev/null +++ b/packages/react-bootstrap-table2-filter/test/context.test.js @@ -0,0 +1,253 @@ +import 'jsdom-global/register'; +import React from 'react'; +import { shallow } from 'enzyme'; +import _ from 'react-bootstrap-table-next/src/utils'; +import BootstrapTable from 'react-bootstrap-table-next/src/bootstrap-table'; + +import { + FILTER_TYPE +} from '../src/const'; +import createFilterContext from '../src/context'; +import { textFilter } from '../index'; + +describe('FilterContext', () => { + let wrapper; + let FilterContext; + + const data = [{ + id: 1, + name: 'A' + }, { + id: 2, + name: 'B' + }]; + + const columns = [{ + dataField: 'id', + text: 'ID', + filter: textFilter() + }, { + dataField: 'name', + text: 'Name', + filter: textFilter() + }]; + + const mockBase = jest.fn((props => ( + + ))); + + const handleFilterChange = jest.fn(); + + function shallowContext( + enableRemote = false + ) { + mockBase.mockReset(); + handleFilterChange.mockReset(); + FilterContext = createFilterContext( + _, + jest.fn().mockReturnValue(enableRemote), + handleFilterChange + ); + + return ( + + + { + filterProps => mockBase(filterProps) + } + + + ); + } + + describe('default render', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.render(); + }); + + it('should have correct Provider property after calling createFilterContext', () => { + expect(FilterContext.Provider).toBeDefined(); + }); + + it('should have correct Consumer property after calling createFilterContext', () => { + expect(FilterContext.Consumer).toBeDefined(); + }); + + it('should have correct currFilters', () => { + expect(wrapper.instance().currFilters).toEqual({}); + }); + + it('should pass correct cell editing props to children element', () => { + expect(wrapper.length).toBe(1); + expect(mockBase).toHaveBeenCalledWith({ + data, + onFilter: wrapper.instance().onFilter, + onExternalFilter: wrapper.instance().onExternalFilter + }); + }); + }); + + describe('when remote filter is enable', () => { + beforeEach(() => { + wrapper = shallow(shallowContext(true)); + wrapper.render(); + wrapper.instance().currFilters = { price: { filterVal: 20, filterType: FILTER_TYPE.TEXT } }; + }); + + it('should pass original data without internal filtering', () => { + expect(wrapper.length).toBe(1); + expect(mockBase).toHaveBeenCalledWith({ + data, + onFilter: wrapper.instance().onFilter, + onExternalFilter: wrapper.instance().onExternalFilter + }); + }); + }); + + describe('componentDidMount', () => { + describe('when remote filter is disabled', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.render(); + wrapper.instance().componentDidMount(); + }); + + it('should not call handleFilterChange', () => { + expect(handleFilterChange).toHaveBeenCalledTimes(0); + }); + }); + + describe('when remote filter is enable but currFilters is empty', () => { + beforeEach(() => { + wrapper = shallow(shallowContext(true)); + wrapper.render(); + wrapper.instance().componentDidMount(); + }); + + it('should not call handleFilterChange', () => { + expect(handleFilterChange).toHaveBeenCalledTimes(0); + }); + }); + + describe('when remote filter is enable and currFilters is not empty', () => { + beforeEach(() => { + wrapper = shallow(shallowContext(true)); + wrapper.instance().currFilters.price = { filterVal: 40, filterType: FILTER_TYPE.TEXT }; + }); + + it('should not call handleFilterChange', () => { + wrapper.instance().componentDidMount(); + expect(handleFilterChange).toHaveBeenCalledTimes(1); + expect(handleFilterChange).toHaveBeenCalledWith(wrapper.instance().currFilters); + }); + }); + }); + + describe('onFilter', () => { + let instance; + describe('when filterVal is empty or undefined', () => { + const filterVals = ['', undefined, []]; + + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.render(); + instance = wrapper.instance(); + }); + + it('should correct currFilters', () => { + filterVals.forEach((filterVal) => { + instance.onFilter(columns[1], FILTER_TYPE.TEXT)(filterVal); + expect(Object.keys(instance.currFilters)).toHaveLength(0); + }); + }); + }); + + describe('when filterVal is existing', () => { + const filterVal = '3'; + + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.render(); + instance = wrapper.instance(); + }); + + it('should correct currFilters', () => { + instance.onFilter(columns[1], FILTER_TYPE.TEXT)(filterVal); + expect(Object.keys(instance.currFilters)).toHaveLength(1); + }); + }); + + describe('when remote filter is enabled', () => { + const filterVal = '3'; + + beforeEach(() => { + wrapper = shallow(shallowContext(true)); + wrapper.render(); + instance = wrapper.instance(); + instance.onFilter(columns[1], FILTER_TYPE.TEXT)(filterVal); + }); + + it('should correct currFilters', () => { + expect(Object.keys(instance.currFilters)).toHaveLength(1); + }); + + it('should calling handleFilterChange correctly', () => { + expect(handleFilterChange).toHaveBeenCalledTimes(1); + expect(handleFilterChange).toHaveBeenCalledWith(instance.currFilters); + }); + }); + + describe('when remote filter is enabled but initialize argument is true', () => { + const filterVal = '3'; + + beforeEach(() => { + wrapper = shallow(shallowContext(true)); + wrapper.render(); + instance = wrapper.instance(); + instance.onFilter(columns[1], FILTER_TYPE.TEXT, true)(filterVal); + }); + + it('should correct currFilters', () => { + expect(Object.keys(instance.currFilters)).toHaveLength(1); + }); + + it('should not call handleFilterChange correctly', () => { + expect(handleFilterChange).toHaveBeenCalledTimes(0); + }); + }); + + describe('combination', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.render(); + instance = wrapper.instance(); + }); + + it('should set correct currFilters', () => { + instance.onFilter(columns[0], FILTER_TYPE.TEXT)('3'); + expect(Object.keys(instance.currFilters)).toHaveLength(1); + + instance.onFilter(columns[0], FILTER_TYPE.TEXT)('2'); + expect(Object.keys(instance.currFilters)).toHaveLength(1); + + instance.onFilter(columns[1], FILTER_TYPE.TEXT)('2'); + expect(Object.keys(instance.currFilters)).toHaveLength(2); + + instance.onFilter(columns[1], FILTER_TYPE.TEXT)(''); + expect(Object.keys(instance.currFilters)).toHaveLength(1); + + instance.onFilter(columns[0], FILTER_TYPE.TEXT)(''); + expect(Object.keys(instance.currFilters)).toHaveLength(0); + }); + }); + }); +}); diff --git a/packages/react-bootstrap-table2-filter/test/filter.test.js b/packages/react-bootstrap-table2-filter/test/filter.test.js index 79d0407..32956e4 100644 --- a/packages/react-bootstrap-table2-filter/test/filter.test.js +++ b/packages/react-bootstrap-table2-filter/test/filter.test.js @@ -1,5 +1,4 @@ import _ from 'react-bootstrap-table-next/src/utils'; -import Store from 'react-bootstrap-table-next/src/store'; import { filters } from '../src/filter'; import { FILTER_TYPE } from '../src/const'; @@ -16,14 +15,10 @@ for (let i = 0; i < 20; i += 1) { } describe('filter', () => { - let store; - let filterFn; let currFilters; let columns; beforeEach(() => { - store = new Store('id'); - store.data = data; currFilters = {}; columns = [{ dataField: 'id', @@ -41,10 +36,6 @@ describe('filter', () => { }); describe('filterByText', () => { - beforeEach(() => { - filterFn = filters(store, columns, _); - }); - describe('when filter value is not a String', () => { it('should transform to string and do the filter', () => { currFilters.name = { @@ -52,7 +43,7 @@ describe('filter', () => { filterType: FILTER_TYPE.TEXT }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toBeDefined(); expect(result).toHaveLength(2); }); @@ -65,7 +56,7 @@ describe('filter', () => { filterType: FILTER_TYPE.TEXT }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toBeDefined(); expect(result).toHaveLength(2); }); @@ -79,7 +70,7 @@ describe('filter', () => { filterType: FILTER_TYPE.TEXT }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toBeDefined(); expect(result).toHaveLength(0); }); @@ -93,7 +84,7 @@ describe('filter', () => { comparator: EQ }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toBeDefined(); expect(result).toHaveLength(1); }); @@ -102,7 +93,6 @@ describe('filter', () => { describe('column.filterValue is defined', () => { beforeEach(() => { columns[1].filterValue = jest.fn(); - filterFn = filters(store, columns, _); }); it('should calling custom filterValue callback correctly', () => { @@ -111,7 +101,7 @@ describe('filter', () => { filterType: FILTER_TYPE.TEXT }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toBeDefined(); expect(columns[1].filterValue).toHaveBeenCalledTimes(data.length); // const calls = columns[1].filterValue.mock.calls; @@ -124,10 +114,6 @@ describe('filter', () => { }); describe('filterByArray', () => { - beforeEach(() => { - filterFn = filters(store, columns, _); - }); - describe('when filter value is empty array', () => { it('should return original data', () => { currFilters.name = { @@ -135,9 +121,9 @@ describe('filter', () => { filterType: FILTER_TYPE.MULTISELECT }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toBeDefined(); - expect(result).toHaveLength(store.data.length); + expect(result).toHaveLength(data.length); }); }); @@ -150,7 +136,7 @@ describe('filter', () => { comparator: EQ }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toBeDefined(); expect(result).toHaveLength(2); }); @@ -164,7 +150,7 @@ describe('filter', () => { comparator: LIKE }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toBeDefined(); expect(result).toHaveLength(3); }); @@ -173,10 +159,6 @@ describe('filter', () => { }); describe('filterByNumber', () => { - beforeEach(() => { - filterFn = filters(store, columns, _); - }); - describe('when currFilters.filterVal.comparator is empty', () => { it('should returning correct result', () => { currFilters.price = { @@ -184,11 +166,11 @@ describe('filter', () => { filterType: FILTER_TYPE.NUMBER }; - let result = filterFn(currFilters); + let result = filters(data, columns, _)(currFilters); expect(result).toHaveLength(data.length); currFilters.price.filterVal.comparator = undefined; - result = filterFn(currFilters); + result = filters(result, columns, _)(currFilters); expect(result).toHaveLength(data.length); }); }); @@ -200,7 +182,7 @@ describe('filter', () => { filterType: FILTER_TYPE.NUMBER }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toHaveLength(data.length); }); }); @@ -212,11 +194,11 @@ describe('filter', () => { filterType: FILTER_TYPE.NUMBER }; - let result = filterFn(currFilters); + let result = filters(data, columns, _)(currFilters); expect(result).toHaveLength(1); currFilters.price.filterVal.number = '0'; - result = filterFn(currFilters); + result = filters(result, columns, _)(currFilters); expect(result).toHaveLength(0); }); }); @@ -228,7 +210,7 @@ describe('filter', () => { filterType: FILTER_TYPE.NUMBER }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toHaveLength(16); }); }); @@ -240,7 +222,7 @@ describe('filter', () => { filterType: FILTER_TYPE.NUMBER }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toHaveLength(17); }); }); @@ -252,7 +234,7 @@ describe('filter', () => { filterType: FILTER_TYPE.NUMBER }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toHaveLength(3); }); }); @@ -264,7 +246,7 @@ describe('filter', () => { filterType: FILTER_TYPE.NUMBER }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toHaveLength(4); }); }); @@ -276,15 +258,16 @@ describe('filter', () => { filterType: FILTER_TYPE.NUMBER }; - const result = filterFn(currFilters); + const result = filters(data, columns, _)(currFilters); expect(result).toHaveLength(19); }); }); }); describe('filterByDate', () => { + let filterFn; beforeEach(() => { - filterFn = filters(store, columns, _); + filterFn = filters(data, columns, _); }); describe('when currFilters.filterVal.comparator is empty', () => { diff --git a/packages/react-bootstrap-table2-filter/test/wrapper.test.js b/packages/react-bootstrap-table2-filter/test/wrapper.test.js deleted file mode 100644 index 751fc2e..0000000 --- a/packages/react-bootstrap-table2-filter/test/wrapper.test.js +++ /dev/null @@ -1,252 +0,0 @@ -import React from 'react'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; - -import _ from 'react-bootstrap-table-next/src/utils'; -import remoteResolver from 'react-bootstrap-table-next/src/props-resolver/remote-resolver'; -import BootstrapTable from 'react-bootstrap-table-next/src/bootstrap-table'; -import Store from 'react-bootstrap-table-next/src/store'; -import filter, { textFilter } from '..'; -import wrapperFactory from '../src/wrapper'; -import { FILTER_TYPE } from '../src/const'; - -const data = []; -for (let i = 0; i < 20; i += 1) { - data.push({ - id: i, - name: `itme name ${i}`, - price: 200 + i - }); -} - -describe('Wrapper', () => { - let wrapper; - let instance; - const onTableChangeCB = sinon.stub(); - - afterEach(() => { - onTableChangeCB.reset(); - }); - - const createTableProps = (props) => { - const tableProps = { - keyField: 'id', - columns: [{ - dataField: 'id', - text: 'ID' - }, { - dataField: 'name', - text: 'Name', - filter: textFilter() - }, { - dataField: 'price', - text: 'Price', - filter: textFilter() - }], - data, - filter: filter(), - _, - store: new Store('id'), - onTableChange: onTableChangeCB, - ...props - }; - tableProps.store.data = data; - return tableProps; - }; - - const FilterWrapper = wrapperFactory(BootstrapTable, { - _, - remoteResolver - }); - - const createFilterWrapper = (props, renderFragment = true) => { - wrapper = shallow(); - instance = wrapper.instance(); - if (renderFragment) { - const fragment = instance.render(); - wrapper = shallow(
{ fragment }
); - } - }; - - describe('default filter wrapper', () => { - const props = createTableProps(); - - beforeEach(() => { - createFilterWrapper(props); - }); - - it('should rendering correctly', () => { - expect(wrapper.length).toBe(1); - }); - - it('should initializing state correctly', () => { - expect(instance.state.isDataChanged).toBeFalsy(); - expect(instance.state.currFilters).toEqual({}); - }); - - it('should rendering BootstraTable correctly', () => { - const table = wrapper.find(BootstrapTable); - expect(table.length).toBe(1); - expect(table.prop('onFilter')).toBeDefined(); - expect(table.prop('isDataChanged')).toEqual(instance.state.isDataChanged); - }); - }); - - describe('componentWillReceiveProps', () => { - let nextProps; - - describe('when props.store.filters is same as current state.currFilters', () => { - beforeEach(() => { - nextProps = createTableProps(); - instance.componentWillReceiveProps(nextProps); - }); - - it('should setting isDataChanged as false (Temporary solution)', () => { - expect(instance.state.isDataChanged).toBeFalsy(); - }); - }); - - describe('when props.isDataChanged is true', () => { - 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', () => { - const nextData = []; - - beforeEach(() => { - nextProps = createTableProps(); - nextProps.store.filters = { price: { filterVal: 20, filterType: FILTER_TYPE.TEXT } }; - nextProps.store.setAllData(nextData); - instance.componentWillReceiveProps(nextProps); - }); - - it('should setting states correctly', () => { - expect(nextProps.store.filteredData).toEqual(nextData); - expect(instance.state.isDataChanged).toBeTruthy(); - expect(instance.state.currFilters).toBe(nextProps.store.filters); - }); - }); - - describe('when remote filter is enabled', () => { - let props; - const nextData = []; - - beforeEach(() => { - props = createTableProps({ remote: { filter: true } }); - createFilterWrapper(props); - nextProps = createTableProps({ remote: { filter: true } }); - nextProps.store.setAllData(nextData); - instance.componentWillReceiveProps(nextProps); - }); - - it('should setting states correctly', () => { - expect(nextProps.store.filteredData).toEqual(nextData); - expect(instance.state.isDataChanged).toBeTruthy(); - expect(instance.state.currFilters).toBe(nextProps.store.filters); - }); - }); - }); - - describe('onFilter', () => { - let props; - - beforeEach(() => { - props = createTableProps(); - createFilterWrapper(props); - }); - - describe('when filterVal is empty or undefined', () => { - const filterVals = ['', undefined, []]; - - it('should setting store object correctly', () => { - filterVals.forEach((filterVal) => { - instance.onFilter(props.columns[1], FILTER_TYPE.TEXT)(filterVal); - expect(props.store.filtering).toBeFalsy(); - }); - }); - - it('should setting state correctly', () => { - filterVals.forEach((filterVal) => { - instance.onFilter(props.columns[1], FILTER_TYPE.TEXT)(filterVal); - expect(instance.state.isDataChanged).toBeTruthy(); - expect(Object.keys(instance.state.currFilters)).toHaveLength(0); - }); - }); - }); - - describe('when filterVal is existing', () => { - const filterVal = '3'; - - it('should setting store object correctly', () => { - instance.onFilter(props.columns[1], FILTER_TYPE.TEXT)(filterVal); - expect(props.store.filters).toEqual(instance.state.currFilters); - }); - - it('should setting state correctly', () => { - instance.onFilter(props.columns[1], FILTER_TYPE.TEXT)(filterVal); - expect(instance.state.isDataChanged).toBeTruthy(); - expect(Object.keys(instance.state.currFilters)).toHaveLength(1); - }); - }); - - describe('when remote filter is enabled', () => { - const filterVal = '3'; - - beforeEach(() => { - props = createTableProps(); - props.remote = { filter: true }; - createFilterWrapper(props); - instance.onFilter(props.columns[1], FILTER_TYPE.TEXT)(filterVal); - }); - - it('should not setting store object correctly', () => { - expect(props.store.filters).not.toEqual(instance.state.currFilters); - }); - - it('should not setting state', () => { - expect(instance.state.isDataChanged).toBeFalsy(); - expect(Object.keys(instance.state.currFilters)).toHaveLength(0); - }); - - it('should calling props.onRemoteFilterChange correctly', () => { - expect(onTableChangeCB.calledOnce).toBeTruthy(); - }); - }); - - describe('combination', () => { - it('should setting store object correctly', () => { - instance.onFilter(props.columns[1], FILTER_TYPE.TEXT)('3'); - expect(props.store.filters).toEqual(instance.state.currFilters); - expect(instance.state.isDataChanged).toBeTruthy(); - expect(Object.keys(instance.state.currFilters)).toHaveLength(1); - - instance.onFilter(props.columns[1], FILTER_TYPE.TEXT)('2'); - expect(props.store.filters).toEqual(instance.state.currFilters); - expect(instance.state.isDataChanged).toBeTruthy(); - expect(Object.keys(instance.state.currFilters)).toHaveLength(1); - - instance.onFilter(props.columns[2], FILTER_TYPE.TEXT)('2'); - expect(props.store.filters).toEqual(instance.state.currFilters); - expect(instance.state.isDataChanged).toBeTruthy(); - expect(Object.keys(instance.state.currFilters)).toHaveLength(2); - - instance.onFilter(props.columns[2], FILTER_TYPE.TEXT)(''); - expect(props.store.filters).toEqual(instance.state.currFilters); - expect(instance.state.isDataChanged).toBeTruthy(); - expect(Object.keys(instance.state.currFilters)).toHaveLength(1); - - instance.onFilter(props.columns[1], FILTER_TYPE.TEXT)(''); - expect(props.store.filters).toEqual(instance.state.currFilters); - expect(instance.state.isDataChanged).toBeTruthy(); - expect(Object.keys(instance.state.currFilters)).toHaveLength(0); - }); - }); - }); -}); diff --git a/packages/react-bootstrap-table2-overlay/package.json b/packages/react-bootstrap-table2-overlay/package.json index cc8a25d..66fa40e 100644 --- a/packages/react-bootstrap-table2-overlay/package.json +++ b/packages/react-bootstrap-table2-overlay/package.json @@ -41,7 +41,7 @@ }, "peerDependencies": { "prop-types": "^15.0.0", - "react": "^16.0.0", - "react-dom": "^16.0.0" + "react": "^16.3.0", + "react-dom": "^16.3.0" } } diff --git a/packages/react-bootstrap-table2-paginator/index.js b/packages/react-bootstrap-table2-paginator/index.js index edd0067..8750183 100644 --- a/packages/react-bootstrap-table2-paginator/index.js +++ b/packages/react-bootstrap-table2-paginator/index.js @@ -1,6 +1,6 @@ -import wrapperFactory from './src/wrapper'; +import createContext from './src/context'; export default (options = {}) => ({ - wrapperFactory, + createContext, options }); diff --git a/packages/react-bootstrap-table2-paginator/package.json b/packages/react-bootstrap-table2-paginator/package.json index 6beb303..cf56c83 100644 --- a/packages/react-bootstrap-table2-paginator/package.json +++ b/packages/react-bootstrap-table2-paginator/package.json @@ -38,7 +38,7 @@ ], "peerDependencies": { "prop-types": "^15.0.0", - "react": "^16.0.0", - "react-dom": "^16.0.0" + "react": "^16.3.0", + "react-dom": "^16.3.0" } } diff --git a/packages/react-bootstrap-table2-paginator/src/bootstrap.js b/packages/react-bootstrap-table2-paginator/src/bootstrap.js new file mode 100644 index 0000000..1ee0db1 --- /dev/null +++ b/packages/react-bootstrap-table2-paginator/src/bootstrap.js @@ -0,0 +1,6 @@ +import React from 'react'; + +// consider to have a common lib?1 +export const BootstrapContext = React.createContext({ + bootstrap4: false +}); diff --git a/packages/react-bootstrap-table2-paginator/src/context.js b/packages/react-bootstrap-table2-paginator/src/context.js new file mode 100644 index 0000000..10d72ff --- /dev/null +++ b/packages/react-bootstrap-table2-paginator/src/context.js @@ -0,0 +1,188 @@ +/* eslint react/prop-types: 0 */ +/* eslint react/require-default-props: 0 */ +/* eslint no-lonely-if: 0 */ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Const from './const'; +import { BootstrapContext } from './bootstrap'; +import Pagination from './pagination'; +import { getByCurrPage, alignPage } from './page'; + +export default ( + isRemotePagination, + handleRemotePageChange +) => { + const PaginationContext = React.createContext(); + + class PaginationProvider extends React.Component { + static propTypes = { + data: PropTypes.array.isRequired + } + + constructor(props) { + super(props); + this.handleChangePage = this.handleChangePage.bind(this); + this.handleChangeSizePerPage = this.handleChangeSizePerPage.bind(this); + + let currPage; + let currSizePerPage; + const { options } = props.pagination; + const sizePerPageList = options.sizePerPageList || Const.SIZE_PER_PAGE_LIST; + + // initialize current page + if (typeof options.page !== 'undefined') { + currPage = options.page; + } else if (typeof options.pageStartIndex !== 'undefined') { + currPage = options.pageStartIndex; + } else { + currPage = Const.PAGE_START_INDEX; + } + + // initialize current sizePerPage + if (typeof options.sizePerPage !== 'undefined') { + currSizePerPage = options.sizePerPage; + } else if (typeof sizePerPageList[0] === 'object') { + currSizePerPage = sizePerPageList[0].value; + } else { + currSizePerPage = sizePerPageList[0]; + } + + this.currPage = currPage; + this.currSizePerPage = currSizePerPage; + } + + componentWillReceiveProps(nextProps) { + let needNewState = false; + let { currPage, currSizePerPage } = this; + const { page, sizePerPage, onPageChange } = nextProps.pagination.options; + + const pageStartIndex = typeof nextProps.pagination.options.pageStartIndex !== 'undefined' ? + nextProps.pagination.options.pageStartIndex : Const.PAGE_START_INDEX; + + if (typeof page !== 'undefined' && currPage !== page) { // user defined page + currPage = page; + needNewState = true; + } else { + // user should align the page when the page is not fit to the data size when remote enable + if (!isRemotePagination()) { + const newPage = alignPage(nextProps.data, currPage, currSizePerPage, pageStartIndex); + if (currPage !== newPage) { + currPage = newPage; + needNewState = true; + } + } + } + + if (typeof sizePerPage !== 'undefined' && currSizePerPage !== sizePerPage) { + currSizePerPage = sizePerPage; + needNewState = true; + } + + if (needNewState) { + if (onPageChange) { + onPageChange(currPage, currSizePerPage); + } + this.currPage = currPage; + this.currSizePerPage = currSizePerPage; + } + } + + handleChangePage(currPage) { + const { currSizePerPage } = this; + const { pagination: { options } } = this.props; + + if (options.onPageChange) { + options.onPageChange(currPage, currSizePerPage); + } + + this.currPage = currPage; + + if (isRemotePagination()) { + handleRemotePageChange(currPage, currSizePerPage); + return; + } + this.forceUpdate(); + } + + handleChangeSizePerPage(currSizePerPage, currPage) { + const { pagination: { options } } = this.props; + + if (options.onSizePerPageChange) { + options.onSizePerPageChange(currSizePerPage, currPage); + } + + this.currPage = currPage; + this.currSizePerPage = currSizePerPage; + + if (isRemotePagination()) { + handleRemotePageChange(currPage, currSizePerPage); + return; + } + this.forceUpdate(); + } + + render() { + let { data } = this.props; + const { pagination: { options }, bootstrap4 } = this.props; + const { currPage, currSizePerPage } = this; + const withFirstAndLast = typeof options.withFirstAndLast === 'undefined' ? + Const.With_FIRST_AND_LAST : options.withFirstAndLast; + const alwaysShowAllBtns = typeof options.alwaysShowAllBtns === 'undefined' ? + Const.SHOW_ALL_PAGE_BTNS : options.alwaysShowAllBtns; + const hideSizePerPage = typeof options.hideSizePerPage === 'undefined' ? + Const.HIDE_SIZE_PER_PAGE : options.hideSizePerPage; + const hidePageListOnlyOnePage = typeof options.hidePageListOnlyOnePage === 'undefined' ? + Const.HIDE_PAGE_LIST_ONLY_ONE_PAGE : options.hidePageListOnlyOnePage; + const pageStartIndex = typeof options.pageStartIndex === 'undefined' ? + Const.PAGE_START_INDEX : options.pageStartIndex; + + data = isRemotePagination() ? + data : + getByCurrPage( + data, + currPage, + currSizePerPage, + pageStartIndex + ); + + return ( + + { this.props.children } + + + + + ); + } + } + + return { + Provider: PaginationProvider, + Consumer: PaginationContext.Consumer + }; +}; diff --git a/packages/react-bootstrap-table2-paginator/src/page.js b/packages/react-bootstrap-table2-paginator/src/page.js index 3a9b863..40d2a8a 100644 --- a/packages/react-bootstrap-table2-paginator/src/page.js +++ b/packages/react-bootstrap-table2-paginator/src/page.js @@ -1,5 +1,3 @@ -/* eslint no-param-reassign: 0 */ - const getNormalizedPage = ( page, pageStartIndex @@ -19,25 +17,36 @@ const startIndex = ( sizePerPage, ) => end - (sizePerPage - 1); -export const alignPage = (store, pageStartIndex, sizePerPage) => { - const end = endIndex(store.page, sizePerPage, pageStartIndex); - const dataSize = store.data.length; +export const alignPage = ( + data, + page, + sizePerPage, + pageStartIndex +) => { + const end = endIndex(page, sizePerPage, pageStartIndex); + const dataSize = data.length; if (end - 1 > dataSize) { return pageStartIndex; } - return store.page; + return page; }; -export const getByCurrPage = (store, pageStartIndex) => { - const dataSize = store.data.length; +export const getByCurrPage = ( + data, + page, + sizePerPage, + pageStartIndex +) => { + const dataSize = data.length; if (!dataSize) return []; - const end = endIndex(store.page, store.sizePerPage, pageStartIndex); - const start = startIndex(end, store.sizePerPage); + + const end = endIndex(page, sizePerPage, pageStartIndex); + const start = startIndex(end, sizePerPage); const result = []; for (let i = start; i <= end; i += 1) { - result.push(store.data[i]); + result.push(data[i]); if (i + 1 === dataSize) break; } return result; diff --git a/packages/react-bootstrap-table2-paginator/src/size-per-page-dropdown.js b/packages/react-bootstrap-table2-paginator/src/size-per-page-dropdown.js index 5ae4298..2d8fcc4 100644 --- a/packages/react-bootstrap-table2-paginator/src/size-per-page-dropdown.js +++ b/packages/react-bootstrap-table2-paginator/src/size-per-page-dropdown.js @@ -1,6 +1,7 @@ import React from 'react'; import cs from 'classnames'; import PropTypes from 'prop-types'; +import { BootstrapContext } from './bootstrap'; import SizePerPageOption from './size-per-page-option'; const sizePerPageDefaultClass = 'react-bs-table-sizePerPage-dropdown'; @@ -20,44 +21,60 @@ const SizePerPageDropDown = (props) => { } = props; const dropDownStyle = { visibility: hidden ? 'hidden' : 'visible' }; + const openClass = open ? 'open show' : ''; const dropdownClasses = cs( - open ? 'open show' : '', + openClass, sizePerPageDefaultClass, variation, className, ); return ( - - -
    - { - options.map(option => ( - - )) - } -
-
+ + { + ({ bootstrap4 }) => ( + + +
    + { + options.map(option => ( + + )) + } +
+
+ ) + } +
); }; diff --git a/packages/react-bootstrap-table2-paginator/src/size-per-page-option.js b/packages/react-bootstrap-table2-paginator/src/size-per-page-option.js index eba6ca6..a01a360 100644 --- a/packages/react-bootstrap-table2-paginator/src/size-per-page-option.js +++ b/packages/react-bootstrap-table2-paginator/src/size-per-page-option.js @@ -5,9 +5,28 @@ import PropTypes from 'prop-types'; const SizePerPageOption = ({ text, page, - onSizePerPageChange -}) => ( -
  • + onSizePerPageChange, + bootstrap4 +}) => (bootstrap4 ? ( + { + e.preventDefault(); + onSizePerPageChange(page); + } } + > + { text } + +) : ( +
  • -); +)); SizePerPageOption.propTypes = { text: PropTypes.string.isRequired, page: PropTypes.number.isRequired, - onSizePerPageChange: PropTypes.func.isRequired + onSizePerPageChange: PropTypes.func.isRequired, + bootstrap4: PropTypes.bool +}; + +SizePerPageOption.defaultProps = { + bootstrap4: false }; export default SizePerPageOption; diff --git a/packages/react-bootstrap-table2-paginator/src/wrapper.js b/packages/react-bootstrap-table2-paginator/src/wrapper.js deleted file mode 100644 index 784687a..0000000 --- a/packages/react-bootstrap-table2-paginator/src/wrapper.js +++ /dev/null @@ -1,168 +0,0 @@ -/* eslint react/prop-types: 0 */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import Const from './const'; -import Pagination from './pagination'; -import { getByCurrPage, alignPage } from './page'; - -export default (Base, { - remoteResolver -}) => - class PaginationWrapper extends remoteResolver(Component) { - static propTypes = { - store: PropTypes.object.isRequired - } - - constructor(props) { - super(props); - this.handleChangePage = this.handleChangePage.bind(this); - this.handleChangeSizePerPage = this.handleChangeSizePerPage.bind(this); - - let currPage; - let currSizePerPage; - const { options } = props.pagination; - const sizePerPageList = options.sizePerPageList || Const.SIZE_PER_PAGE_LIST; - - // initialize current page - if (typeof options.page !== 'undefined') { - currPage = options.page; - } else if (typeof options.pageStartIndex !== 'undefined') { - currPage = options.pageStartIndex; - } else { - currPage = Const.PAGE_START_INDEX; - } - - // initialize current sizePerPage - if (typeof options.sizePerPage !== 'undefined') { - currSizePerPage = options.sizePerPage; - } else if (typeof sizePerPageList[0] === 'object') { - currSizePerPage = sizePerPageList[0].value; - } else { - currSizePerPage = sizePerPageList[0]; - } - - this.state = { currPage, currSizePerPage }; - this.saveToStore(currPage, currSizePerPage); - } - - componentWillReceiveProps(nextProps) { - let needNewState = false; - let { currPage, currSizePerPage } = this.state; - const { page, sizePerPage, onPageChange } = nextProps.pagination.options; - - const pageStartIndex = typeof nextProps.pagination.options.pageStartIndex !== 'undefined' ? - nextProps.pagination.options.pageStartIndex : Const.PAGE_START_INDEX; - - if (typeof page !== 'undefined' && currPage !== page) { // user defined page - currPage = page; - needNewState = true; - } else if (nextProps.isDataChanged) { - currPage = alignPage(this.props.store, pageStartIndex, currSizePerPage); - needNewState = true; - } - - if (typeof currPage === 'undefined') { - currPage = pageStartIndex; - } - - if (typeof sizePerPage !== 'undefined') { - currSizePerPage = sizePerPage; - needNewState = true; - } - - this.saveToStore(currPage, currSizePerPage); - - if (needNewState) { - if (onPageChange) { - onPageChange(currPage, currSizePerPage); - } - this.setState(() => ({ currPage, currSizePerPage })); - } - } - - saveToStore(page, sizePerPage) { - this.props.store.page = page; - this.props.store.sizePerPage = sizePerPage; - } - - handleChangePage(currPage) { - const { currSizePerPage } = this.state; - const { pagination: { options } } = this.props; - this.saveToStore(currPage, currSizePerPage); - - if (options.onPageChange) { - options.onPageChange(currPage, currSizePerPage); - } - if (this.isRemotePagination()) { - this.handleRemotePageChange(); - return; - } - this.setState(() => ({ currPage })); - } - - handleChangeSizePerPage(currSizePerPage, currPage) { - const { pagination: { options } } = this.props; - this.saveToStore(currPage, currSizePerPage); - - if (options.onSizePerPageChange) { - options.onSizePerPageChange(currSizePerPage, currPage); - } - if (this.isRemotePagination()) { - this.handleRemotePageChange(); - return; - } - this.setState(() => ({ - currPage, - currSizePerPage - })); - } - - render() { - const { pagination: { options }, store } = this.props; - const { currPage, currSizePerPage } = this.state; - const withFirstAndLast = typeof options.withFirstAndLast === 'undefined' ? - Const.With_FIRST_AND_LAST : options.withFirstAndLast; - const alwaysShowAllBtns = typeof options.alwaysShowAllBtns === 'undefined' ? - Const.SHOW_ALL_PAGE_BTNS : options.alwaysShowAllBtns; - const hideSizePerPage = typeof options.hideSizePerPage === 'undefined' ? - Const.HIDE_SIZE_PER_PAGE : options.hideSizePerPage; - const hidePageListOnlyOnePage = typeof options.hidePageListOnlyOnePage === 'undefined' ? - Const.HIDE_PAGE_LIST_ONLY_ONE_PAGE : options.hidePageListOnlyOnePage; - const pageStartIndex = typeof options.pageStartIndex === 'undefined' ? - Const.PAGE_START_INDEX : options.pageStartIndex; - - const data = this.isRemotePagination() ? - this.props.data : - getByCurrPage(store, pageStartIndex); - - return [ - , - - ]; - } - }; diff --git a/packages/react-bootstrap-table2-paginator/test/context.test.js b/packages/react-bootstrap-table2-paginator/test/context.test.js new file mode 100644 index 0000000..4b9c30d --- /dev/null +++ b/packages/react-bootstrap-table2-paginator/test/context.test.js @@ -0,0 +1,773 @@ +import 'jsdom-global/register'; +import React from 'react'; +import { shallow } from 'enzyme'; +import BootstrapTable from 'react-bootstrap-table-next/src/bootstrap-table'; + +import Pagination from '../src/pagination'; +import Const from '../src/const'; +import createPaginationContext from '../src/context'; +import paginationFactory from '../index'; + +const data = []; +for (let i = 0; i < 100; i += 1) { + data.push({ + id: i, + name: `itme name ${i}` + }); +} + +describe('PaginationContext', () => { + let wrapper; + let PaginationContext; + + const columns = [{ + dataField: 'id', + text: 'ID' + }, { + dataField: 'name', + text: 'Name' + }]; + + const defaultPagination = { options: {} }; + + const mockBase = jest.fn((props => ( + + ))); + + const handleRemotePaginationChange = jest.fn(); + + function shallowContext( + customPagination = defaultPagination, + enableRemote = false + ) { + mockBase.mockReset(); + handleRemotePaginationChange.mockReset(); + PaginationContext = createPaginationContext( + jest.fn().mockReturnValue(enableRemote), + handleRemotePaginationChange + ); + + return ( + + + { + paginationProps => mockBase(paginationProps) + } + + + ); + } + + describe('default render', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + wrapper.render(); + }); + + it('should have correct Provider property after calling createPaginationContext', () => { + expect(PaginationContext.Provider).toBeDefined(); + }); + + it('should have correct Consumer property after calling createPaginationContext', () => { + expect(PaginationContext.Consumer).toBeDefined(); + }); + + it('should have correct currPage', () => { + expect(wrapper.instance().currPage).toEqual(Const.PAGE_START_INDEX); + }); + + it('should have correct currSizePerPage', () => { + expect(wrapper.instance().currSizePerPage).toEqual(Const.SIZE_PER_PAGE_LIST[0]); + }); + + it('should render Pagination component correctly', () => { + expect(wrapper.length).toBe(1); + const instance = wrapper.instance(); + const pagination = wrapper.find(Pagination); + expect(pagination).toHaveLength(1); + expect(pagination.prop('dataSize')).toEqual(data.length); + expect(pagination.prop('currPage')).toEqual(instance.currPage); + expect(pagination.prop('currSizePerPage')).toEqual(instance.currSizePerPage); + expect(pagination.prop('onPageChange')).toEqual(instance.handleChangePage); + expect(pagination.prop('onSizePerPageChange')).toEqual(instance.handleChangeSizePerPage); + expect(pagination.prop('sizePerPageList')).toEqual(Const.SIZE_PER_PAGE_LIST); + expect(pagination.prop('paginationSize')).toEqual(Const.PAGINATION_SIZE); + expect(pagination.prop('pageStartIndex')).toEqual(Const.PAGE_START_INDEX); + expect(pagination.prop('withFirstAndLast')).toEqual(Const.With_FIRST_AND_LAST); + expect(pagination.prop('alwaysShowAllBtns')).toEqual(Const.SHOW_ALL_PAGE_BTNS); + expect(pagination.prop('firstPageText')).toEqual(Const.FIRST_PAGE_TEXT); + expect(pagination.prop('prePageText')).toEqual(Const.PRE_PAGE_TEXT); + expect(pagination.prop('nextPageText')).toEqual(Const.NEXT_PAGE_TEXT); + expect(pagination.prop('lastPageText')).toEqual(Const.LAST_PAGE_TEXT); + expect(pagination.prop('firstPageTitle')).toEqual(Const.FIRST_PAGE_TITLE); + expect(pagination.prop('prePageTitle')).toEqual(Const.PRE_PAGE_TITLE); + expect(pagination.prop('nextPageTitle')).toEqual(Const.NEXT_PAGE_TITLE); + expect(pagination.prop('lastPageTitle')).toEqual(Const.LAST_PAGE_TITLE); + expect(pagination.prop('hideSizePerPage')).toEqual(Const.HIDE_SIZE_PER_PAGE); + expect(pagination.prop('hideSizePerPage')).toEqual(Const.HIDE_SIZE_PER_PAGE); + expect(pagination.prop('paginationTotalRenderer')).toBeNull(); + }); + + it('should pass correct cell editing props to children element', () => { + expect(mockBase.mock.calls[0][0].data).toHaveLength(Const.SIZE_PER_PAGE_LIST[0]); + }); + }); + + describe('componentWillReceiveProps', () => { + let instance; + let nextProps; + + describe('when nextProps.pagination.options.page is existing', () => { + const onPageChange = jest.fn(); + afterEach(() => { + onPageChange.mockReset(); + }); + + describe('and if it is different with currPage', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + instance = wrapper.instance(); + wrapper.render(); + nextProps = { + data, + pagination: { + options: { + page: 2, + onPageChange + } + } + }; + instance.componentWillReceiveProps(nextProps); + }); + + it('should call options.onPageChange', () => { + expect(onPageChange).toHaveBeenCalledTimes(1); + expect(onPageChange).toHaveBeenCalledWith( + instance.currPage, + instance.currSizePerPage + ); + }); + + it('should set correct currPage', () => { + expect(instance.currPage).toEqual(nextProps.pagination.options.page); + }); + }); + + describe('and if it is same as currPage', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + instance = wrapper.instance(); + wrapper.render(); + nextProps = { + data, + pagination: { + options: { + page: 1, + onPageChange + } + } + }; + instance.componentWillReceiveProps(nextProps); + }); + + it('shouldn\'t call options.onPageChange', () => { + expect(onPageChange).toHaveBeenCalledTimes(0); + }); + + it('should have correct currPage', () => { + expect(instance.currPage).toEqual(nextProps.pagination.options.page); + }); + }); + }); + + describe('when nextProps.pagination.options.page is not existing', () => { + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + page: 3 + })); + instance = wrapper.instance(); + wrapper.render(); + nextProps = { data, pagination: defaultPagination }; + instance.componentWillReceiveProps(nextProps); + }); + + it('should not set currPage', () => { + expect(instance.currPage).toEqual(3); + }); + }); + + describe('when nextProps.pagination.options.sizePerPage is existing', () => { + const onPageChange = jest.fn(); + afterEach(() => { + onPageChange.mockReset(); + }); + + describe('and if it is different with currSizePerPage', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + instance = wrapper.instance(); + wrapper.render(); + nextProps = { + data, + pagination: { + options: { + sizePerPage: Const.SIZE_PER_PAGE_LIST[2], + onPageChange + } + } + }; + instance.componentWillReceiveProps(nextProps); + }); + + it('should call options.onPageChange', () => { + expect(onPageChange).toHaveBeenCalledTimes(1); + expect(onPageChange).toHaveBeenCalledWith( + instance.currPage, + instance.currSizePerPage + ); + }); + + it('should set correct currSizePerPage', () => { + expect(instance.currSizePerPage).toEqual(nextProps.pagination.options.sizePerPage); + }); + }); + + describe('and if it is same as currSizePerPage', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + instance = wrapper.instance(); + wrapper.render(); + nextProps = { + data, + pagination: { + options: { + sizePerPage: Const.SIZE_PER_PAGE_LIST[0], + onPageChange + } + } + }; + instance.componentWillReceiveProps(nextProps); + }); + + it('shouldn\'t call options.onPageChange', () => { + expect(onPageChange).toHaveBeenCalledTimes(0); + }); + + it('should have correct currSizePerPage', () => { + expect(instance.currSizePerPage).toEqual(nextProps.pagination.options.sizePerPage); + }); + }); + }); + + describe('when nextProps.pagination.options.sizePerPage is not existing', () => { + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + sizePerPage: Const.SIZE_PER_PAGE_LIST[2] + })); + instance = wrapper.instance(); + wrapper.render(); + nextProps = { data, pagination: defaultPagination }; + instance.componentWillReceiveProps(nextProps); + }); + + it('should not set currPage', () => { + expect(instance.currSizePerPage).toEqual(Const.SIZE_PER_PAGE_LIST[2]); + }); + }); + }); + + describe('handleChangePage', () => { + let instance; + const newPage = 3; + + describe('should update component correctly', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + instance = wrapper.instance(); + jest.spyOn(instance, 'forceUpdate'); + instance.handleChangePage(newPage); + }); + + it('', () => { + expect(instance.currPage).toEqual(newPage); + expect(instance.forceUpdate).toHaveBeenCalledTimes(1); + }); + }); + + describe('if options.onPageChange is defined', () => { + const onPageChange = jest.fn(); + beforeEach(() => { + onPageChange.mockClear(); + wrapper = shallow(shallowContext({ + ...defaultPagination, + onPageChange + })); + instance = wrapper.instance(); + jest.spyOn(instance, 'forceUpdate'); + instance.handleChangePage(newPage); + }); + + it('should still update component correctly', () => { + expect(instance.currPage).toEqual(newPage); + expect(instance.forceUpdate).toHaveBeenCalledTimes(1); + }); + + it('should call options.onPageChange correctly', () => { + expect(onPageChange).toHaveBeenCalledTimes(1); + expect(onPageChange).toHaveBeenCalledWith(newPage, instance.currSizePerPage); + }); + }); + + describe('if remote pagination is enable', () => { + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination + }, true)); + instance = wrapper.instance(); + jest.spyOn(instance, 'forceUpdate'); + instance.handleChangePage(newPage); + }); + + it('should still update component correctly', () => { + expect(instance.currPage).toEqual(newPage); + expect(instance.forceUpdate).toHaveBeenCalledTimes(0); + }); + + it('should call handleRemotePageChange correctly', () => { + expect(handleRemotePaginationChange).toHaveBeenCalledTimes(1); + expect(handleRemotePaginationChange) + .toHaveBeenCalledWith(newPage, instance.currSizePerPage); + }); + }); + }); + + describe('handleChangeSizePerPage', () => { + let instance; + const newPage = 2; + const newSizePerPage = 15; + + describe('should update component correctly', () => { + beforeEach(() => { + wrapper = shallow(shallowContext()); + instance = wrapper.instance(); + jest.spyOn(instance, 'forceUpdate'); + instance.handleChangeSizePerPage(newSizePerPage, newPage); + }); + + it('', () => { + expect(instance.currPage).toEqual(newPage); + expect(instance.currSizePerPage).toEqual(newSizePerPage); + expect(instance.forceUpdate).toHaveBeenCalledTimes(1); + }); + }); + + describe('if options.onSizePerPageChange is defined', () => { + const onSizePerPageChange = jest.fn(); + beforeEach(() => { + onSizePerPageChange.mockClear(); + wrapper = shallow(shallowContext({ + ...defaultPagination, + onSizePerPageChange + })); + instance = wrapper.instance(); + jest.spyOn(instance, 'forceUpdate'); + instance.handleChangeSizePerPage(newSizePerPage, newPage); + }); + + it('should still update component correctly', () => { + expect(instance.currPage).toEqual(newPage); + expect(instance.currSizePerPage).toEqual(newSizePerPage); + expect(instance.forceUpdate).toHaveBeenCalledTimes(1); + }); + + it('should call options.onSizePerPageChange correctly', () => { + expect(onSizePerPageChange).toHaveBeenCalledTimes(1); + expect(onSizePerPageChange).toHaveBeenCalledWith(newSizePerPage, newPage); + }); + }); + + describe('if remote pagination is enable', () => { + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination + }, true)); + instance = wrapper.instance(); + jest.spyOn(instance, 'forceUpdate'); + instance.handleChangeSizePerPage(newSizePerPage, newPage); + }); + + it('should still update component correctly', () => { + expect(instance.currPage).toEqual(newPage); + expect(instance.currSizePerPage).toEqual(newSizePerPage); + expect(instance.forceUpdate).toHaveBeenCalledTimes(0); + }); + + it('should call handleRemotePageChange correctly', () => { + expect(handleRemotePaginationChange).toHaveBeenCalledTimes(1); + expect(handleRemotePaginationChange) + .toHaveBeenCalledWith(newPage, newSizePerPage); + }); + }); + }); + + describe('when options.page is defined', () => { + const page = 3; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + page + })); + wrapper.render(); + }); + + it('should set correct currPage', () => { + expect(wrapper.instance().currPage).toEqual(page); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('currPage')).toEqual(page); + }); + }); + + describe('when options.sizePerPage is defined', () => { + const sizePerPage = Const.SIZE_PER_PAGE_LIST[2]; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + sizePerPage + })); + wrapper.render(); + }); + + it('should set correct currSizePerPage', () => { + expect(wrapper.instance().currSizePerPage).toEqual(sizePerPage); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('currSizePerPage')).toEqual(sizePerPage); + }); + }); + + describe('when options.totalSize is defined', () => { + const totalSize = 100; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + totalSize + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('dataSize')).toEqual(totalSize); + }); + }); + + describe('when options.showTotal is defined', () => { + const showTotal = true; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + showTotal + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('showTotal')).toEqual(showTotal); + }); + }); + + describe('when options.pageStartIndex is defined', () => { + const pageStartIndex = -1; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + pageStartIndex + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('pageStartIndex')).toEqual(pageStartIndex); + }); + }); + + describe('when options.sizePerPageList is defined', () => { + const sizePerPageList = [10, 40]; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + sizePerPageList + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('sizePerPageList')).toEqual(sizePerPageList); + }); + }); + + describe('when options.paginationSize is defined', () => { + const paginationSize = 10; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + paginationSize + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('paginationSize')).toEqual(paginationSize); + }); + }); + + describe('when options.withFirstAndLast is defined', () => { + const withFirstAndLast = false; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + withFirstAndLast + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('withFirstAndLast')).toEqual(withFirstAndLast); + }); + }); + + describe('when options.alwaysShowAllBtns is defined', () => { + const alwaysShowAllBtns = true; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + alwaysShowAllBtns + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('alwaysShowAllBtns')).toEqual(alwaysShowAllBtns); + }); + }); + + describe('when options.firstPageText is defined', () => { + const firstPageText = '1st'; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + firstPageText + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('firstPageText')).toEqual(firstPageText); + }); + }); + + describe('when options.prePageText is defined', () => { + const prePageText = 'PRE'; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + prePageText + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('prePageText')).toEqual(prePageText); + }); + }); + + describe('when options.nextPageText is defined', () => { + const nextPageText = 'NEXT'; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + nextPageText + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('nextPageText')).toEqual(nextPageText); + }); + }); + + describe('when options.lastPageText is defined', () => { + const lastPageText = 'LAST'; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + lastPageText + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('lastPageText')).toEqual(lastPageText); + }); + }); + + describe('when options.firstPageTitle is defined', () => { + const firstPageTitle = '1st'; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + firstPageTitle + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('firstPageTitle')).toEqual(firstPageTitle); + }); + }); + + describe('when options.prePageTitle is defined', () => { + const prePageTitle = 'PRE'; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + prePageTitle + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('prePageTitle')).toEqual(prePageTitle); + }); + }); + + describe('when options.nextPageTitle is defined', () => { + const nextPageTitle = 'NEXT'; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + nextPageTitle + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('nextPageTitle')).toEqual(nextPageTitle); + }); + }); + + describe('when options.lastPageTitle is defined', () => { + const lastPageTitle = 'nth'; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + lastPageTitle + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('lastPageTitle')).toEqual(lastPageTitle); + }); + }); + + describe('when options.hideSizePerPage is defined', () => { + const hideSizePerPage = true; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + hideSizePerPage + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('hideSizePerPage')).toEqual(hideSizePerPage); + }); + }); + + describe('when options.hidePageListOnlyOnePage is defined', () => { + const hidePageListOnlyOnePage = true; + + beforeEach(() => { + wrapper = shallow(shallowContext({ + ...defaultPagination, + hidePageListOnlyOnePage + })); + wrapper.render(); + }); + + it('should rendering Pagination correctly', () => { + const pagination = wrapper.find(Pagination); + expect(pagination.length).toBe(1); + expect(pagination.prop('hidePageListOnlyOnePage')).toEqual(hidePageListOnlyOnePage); + }); + }); +}); diff --git a/packages/react-bootstrap-table2-paginator/test/page.test.js b/packages/react-bootstrap-table2-paginator/test/page.test.js index bd63574..28ab1e1 100644 --- a/packages/react-bootstrap-table2-paginator/test/page.test.js +++ b/packages/react-bootstrap-table2-paginator/test/page.test.js @@ -1,9 +1,8 @@ -import Store from 'react-bootstrap-table-next/src/store'; + import { getByCurrPage, alignPage } from '../src/page'; describe('Page Functions', () => { let data; - let store; const params = [ // [page, sizePerPage, pageStartIndex] [1, 10, 1], @@ -23,27 +22,21 @@ describe('Page Functions', () => { for (let i = 0; i < 100; i += 1) { data.push({ id: i, name: `test_name${i}` }); } - store = new Store('id'); - store.data = data; }); it('should always return correct data', () => { params.forEach(([page, sizePerPage, pageStartIndex]) => { - store.page = page; - store.sizePerPage = sizePerPage; - const rows = getByCurrPage(store, pageStartIndex); + const rows = getByCurrPage(data, page, sizePerPage, pageStartIndex); expect(rows).toBeDefined(); expect(Array.isArray(rows)).toBeTruthy(); expect(rows.every(row => !!row)).toBeTruthy(); }); }); - it('should return empty array when store.data is empty', () => { - store.data = []; + it('should return empty array when data is empty', () => { + data = []; params.forEach(([page, sizePerPage, pageStartIndex]) => { - store.page = page; - store.sizePerPage = sizePerPage; - const rows = getByCurrPage(store, pageStartIndex); + const rows = getByCurrPage(data, page, sizePerPage, pageStartIndex); expect(rows).toHaveLength(0); }); }); @@ -52,19 +45,17 @@ describe('Page Functions', () => { describe('alignPage', () => { const pageStartIndex = 1; const sizePerPage = 10; + const page = 2; describe('if the length of store.data is less than the end page index', () => { beforeEach(() => { data = []; for (let i = 0; i < 15; i += 1) { data.push({ id: i, name: `test_name${i}` }); } - store = new Store('id'); - store.data = data; - store.page = 2; }); it('should return pageStartIndex argument', () => { - expect(alignPage(store, pageStartIndex, sizePerPage)).toEqual(pageStartIndex); + expect(alignPage(data, page, sizePerPage, pageStartIndex)).toEqual(pageStartIndex); }); }); @@ -74,13 +65,10 @@ describe('Page Functions', () => { for (let i = 0; i < 30; i += 1) { data.push({ id: i, name: `test_name${i}` }); } - store = new Store('id'); - store.data = data; - store.page = 2; }); it('should return current page', () => { - expect(alignPage(store, pageStartIndex, sizePerPage)).toEqual(store.page); + expect(alignPage(data, page, sizePerPage, pageStartIndex)).toEqual(page); }); }); }); diff --git a/packages/react-bootstrap-table2-paginator/test/size-per-page-dropdown.test.js b/packages/react-bootstrap-table2-paginator/test/size-per-page-dropdown.test.js index 21e11cc..fdf55bd 100644 --- a/packages/react-bootstrap-table2-paginator/test/size-per-page-dropdown.test.js +++ b/packages/react-bootstrap-table2-paginator/test/size-per-page-dropdown.test.js @@ -5,6 +5,12 @@ import { shallow } from 'enzyme'; import SizePerPageOption from '../src/size-per-page-option'; import SizePerPageDropDown from '../src/size-per-page-dropdown'; +const shallowWithContext = (elem, context = {}) => { + const wrapper = shallow(elem); + const Children = wrapper.props().children(context); + return shallow(Children); +}; + describe('SizePerPageDropDown', () => { let wrapper; const currSizePerPage = '25'; @@ -28,8 +34,9 @@ describe('SizePerPageDropDown', () => { describe('default SizePerPageDropDown component', () => { beforeEach(() => { - wrapper = shallow( - + wrapper = shallowWithContext( + , + { bootstrap4: false } ); }); @@ -47,6 +54,7 @@ describe('SizePerPageDropDown', () => { const option = options[i]; expect(sizePerPage.prop('text')).toEqual(option.text); expect(sizePerPage.prop('page')).toEqual(option.page); + expect(sizePerPage.prop('bootstrap4')).toBeFalsy(); expect(sizePerPage.prop('onSizePerPageChange')).toEqual(onSizePerPageChange); }); }); @@ -61,10 +69,52 @@ describe('SizePerPageDropDown', () => { }); }); + describe('when bootstrap4 context is true', () => { + beforeEach(() => { + wrapper = shallowWithContext( + , + { bootstrap4: true } + ); + }); + + it('should rendering SizePerPageDropDown correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find('button').length).toBe(1); + expect(wrapper.find('button').text()).toEqual(`${currSizePerPage} `); + }); + + it('should rendering SizePerPageOption successfully', () => { + expect(wrapper.find('ul.dropdown-menu').length).toBe(1); + const sizePerPageOptions = wrapper.find(SizePerPageOption); + expect(sizePerPageOptions.length).toBe(options.length); + sizePerPageOptions.forEach((sizePerPage, i) => { + const option = options[i]; + expect(sizePerPage.prop('text')).toEqual(option.text); + expect(sizePerPage.prop('page')).toEqual(option.page); + expect(sizePerPage.prop('bootstrap4')).toBeTruthy(); + expect(sizePerPage.prop('onSizePerPageChange')).toEqual(onSizePerPageChange); + }); + }); + + it('no need to render caret', () => { + expect(wrapper.find('.caret')).toHaveLength(0); + }); + + it('default variation is dropdown', () => { + expect(wrapper.hasClass('dropdown')).toBeTruthy(); + }); + + it('default dropdown is not open', () => { + expect(wrapper.hasClass('open show')).toBeFalsy(); + expect(wrapper.find('[aria-expanded=false]').length).toBe(1); + }); + }); + describe('when open prop is true', () => { beforeEach(() => { - wrapper = shallow( - + wrapper = shallowWithContext( + , + { bootstrap4: false } ); }); @@ -76,8 +126,9 @@ describe('SizePerPageDropDown', () => { describe('when hidden prop is true', () => { beforeEach(() => { - wrapper = shallow( -