diff --git a/docs/README.md b/docs/README.md index 2a334c1..0766df6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,6 +22,7 @@ * [rowEvents](#rowEvents) * [defaultSorted](#defaultSorted) * [pagination](#pagination) +* [filter](#filter) * [onTableChange](#onTableChange) ### keyField(**required**) - [String] @@ -198,6 +199,33 @@ paginator({ }) ``` +### filter - [Object] +`filter` allow user to filter data by column. However, filter funcitonality is separated from core of `react-bootstrap-table2` so that you are suppose to install `react-bootstrap-table2-filter` firstly. + +```sh +$ npm install react-bootstrap-table2-filter --save +``` + +After installation of `react-bootstrap-table2-filter`, you can configure filter on `react-bootstrap-table2` easily: + +```js +import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; + +// omit... +const columns = [ { + dataField: 'id', + text: 'Production ID' +}, { + dataField: 'name', + text: 'Production Name', + filter: textFilter() // apply text filter +}, { + dataField: 'price', + text: 'Production Price' +} ]; + +``` + ### onTableChange - [Function] This callback function will be called when [`remote`](#remote) enabled only. diff --git a/docs/columns.md b/docs/columns.md index 52aaa17..0f13a82 100644 --- a/docs/columns.md +++ b/docs/columns.md @@ -31,20 +31,22 @@ Available properties in a column object: * [validator](#validator) * [editCellStyle](#editCellStyle) * [editCellClasses](#editCellClasses) +* [filter](#filter) +* [filterValue](#filterValue) Following is a most simplest and basic usage: ```js const rows = [ { id: 1, name: '...', price: '102' } ]; const columns = [ { - dataField: id, - text: Production ID + dataField: 'id', + text: 'Production ID' }, { - dataField: name, - text: Production Name + dataField: 'name', + text: 'Production Name' }, { - dataField: price, - text: Production Price + dataField: 'price', + text: 'Production Price' } ]; ``` @@ -525,4 +527,24 @@ Or take a callback function // it is suppose to return a string } } -``` \ No newline at end of file +``` + +## column.filter - [Object] +Configure `column.filter` will able to setup a column level filter on the header column. Currently, `react-bootstrap-table2` support following filters: + +* Text(`textFilter`) + +We have a quick example to show you how to use `column.filter`: + +``` +import { textFilter } from 'react-bootstrap-table2-filter'; + +// omit... +{ + dataField: 'price', + text: 'Product Price', + filter: textFilter() +} +``` + +For some reason of simple customization, `react-bootstrap-table2` allow you to pass some props to filter factory function. Please check [here](https://github.com/react-bootstrap-table/react-bootstrap-table2/tree/master/packages/react-bootstrap-table2-filter/README.md) for more detail tutorial. \ 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 8ebc72a..25f09d8 100644 --- a/packages/react-bootstrap-table2-example/.storybook/webpack.config.js +++ b/packages/react-bootstrap-table2-example/.storybook/webpack.config.js @@ -3,6 +3,7 @@ const path = require('path'); const sourcePath = path.join(__dirname, '../../react-bootstrap-table2/src'); const paginationSourcePath = path.join(__dirname, '../../react-bootstrap-table2-paginator/src'); const overlaySourcePath = path.join(__dirname, '../../react-bootstrap-table2-overlay/src'); +const filterSourcePath = path.join(__dirname, '../../react-bootstrap-table2-filter/src'); const sourceStylePath = path.join(__dirname, '../../react-bootstrap-table2/style'); const paginationStylePath = path.join(__dirname, '../../react-bootstrap-table2-paginator/style'); const storyPath = path.join(__dirname, '../stories'); @@ -26,7 +27,7 @@ const loaders = [{ test: /\.js?$/, use: ['babel-loader'], exclude: /node_modules/, - include: [sourcePath, paginationSourcePath, overlaySourcePath, storyPath], + include: [sourcePath, paginationSourcePath, overlaySourcePath, filterSourcePath, storyPath], }, { test: /\.css$/, use: ['style-loader', 'css-loader'], 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 new file mode 100644 index 0000000..55158e0 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/column-filter/custom-filter-value.js @@ -0,0 +1,74 @@ +/* eslint no-unused-vars: 0 */ +import React from 'react'; +import BootstrapTable from 'react-bootstrap-table2'; +import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; +import Code from 'components/common/code-block'; +import { jobsGenerator } from 'utils/common'; + +const jobs = jobsGenerator(5); + +const owners = ['Allen', 'Bob', 'Cat']; +const types = ['Cloud Service', 'Message Service', 'Add Service', 'Edit Service', 'Money']; + +const columns = [{ + dataField: 'id', + text: 'Job ID' +}, { + dataField: 'name', + text: 'Job Name', + filter: textFilter() +}, { + dataField: 'owner', + text: 'Job Owner', + filter: textFilter(), + formatter: (cell, row) => owners[cell], + filterValue: (cell, row) => owners[cell] +}, { + dataField: 'type', + text: 'Job Type', + filter: textFilter(), + formatter: (cell, row) => types[cell], + filterValue: (cell, row) => types[cell] +}]; + +const sourceCode = `\ +import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; + +const owners = ['Allen', 'Bob', 'Cat']; +const types = ['Cloud Service', 'Message Service', 'Add Service', 'Edit Service', 'Money']; +const columns = [{ + dataField: 'id', + text: 'Job ID' +}, { + dataField: 'name', + text: 'Job Name', + filter: textFilter() +}, { + dataField: 'owner', + text: 'Job Owner', + filter: textFilter(), + formatter: (cell, row) => owners[cell], + filterValue: (cell, row) => owners[cell] +}, { + dataField: 'type', + text: 'Job Type', + filter: textFilter(), + filterValue: (cell, row) => types[cell] +}]; + +// shape of job: { id: 0, name: 'Job name 0', owner: 1, type: 3 } + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/custom-text-filter.js b/packages/react-bootstrap-table2-example/examples/column-filter/custom-text-filter.js new file mode 100644 index 0000000..84edd0b --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/column-filter/custom-text-filter.js @@ -0,0 +1,68 @@ +/* eslint no-console: 0 */ +import React from 'react'; +import BootstrapTable from 'react-bootstrap-table2'; +import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const products = productsGenerator(8); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name', + filter: textFilter() +}, { + dataField: 'price', + text: 'Product Price', + filter: textFilter({ + delay: 1000, // default is 500ms + style: { + backgroundColor: 'yellow' + }, + className: 'test-classname', + placeholder: 'Custom PlaceHolder', + onClick: e => console.log(e) + }) +}]; + +const sourceCode = `\ +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({ + delay: 1000, // default is 500ms + style: { + backgroundColor: 'yellow' + }, + className: 'test-classname', + placeholder: 'Custom PlaceHolder', + onClick: e => console.log(e) + }) +}]; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/text-filter-default-value.js b/packages/react-bootstrap-table2-example/examples/column-filter/text-filter-default-value.js new file mode 100644 index 0000000..edbf0b7 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/column-filter/text-filter-default-value.js @@ -0,0 +1,55 @@ +import React from 'react'; +import BootstrapTable from 'react-bootstrap-table2'; +import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const products = productsGenerator(8); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name', + filter: textFilter() +}, { + dataField: 'price', + text: 'Product Price', + filter: textFilter({ + defaultValue: '2103' + }) +}]; + +const sourceCode = `\ +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({ + defaultValue: '2103' + }) +}]; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/text-filter-eq-comparator.js b/packages/react-bootstrap-table2-example/examples/column-filter/text-filter-eq-comparator.js new file mode 100644 index 0000000..d062178 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/column-filter/text-filter-eq-comparator.js @@ -0,0 +1,56 @@ +import React from 'react'; +import BootstrapTable from 'react-bootstrap-table2'; +import filterFactory, { textFilter, Comparator } from 'react-bootstrap-table2-filter'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const products = productsGenerator(8); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name', + filter: textFilter({ + comparator: Comparator.EQ // default is Comparator.LIKE + }) +}, { + dataField: 'price', + text: 'Product Price', + filter: textFilter() +}]; + +const sourceCode = `\ +import filterFactory, { textFilter, Comparator } from 'react-bootstrap-table2-filter'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name', + filter: textFilter({ + comparator: Comparator.EQ + }) +}, { + dataField: 'price', + text: 'Product Price', + filter: textFilter() +}]; + + +`; + +export default () => ( +
+

Product Name filter apply Equal Comparator

+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/text-filter.js b/packages/react-bootstrap-table2-example/examples/column-filter/text-filter.js new file mode 100644 index 0000000..03de391 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/column-filter/text-filter.js @@ -0,0 +1,51 @@ +import React from 'react'; +import BootstrapTable from 'react-bootstrap-table2'; +import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const products = productsGenerator(8); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name', + filter: textFilter() +}, { + dataField: 'price', + text: 'Product Price', + filter: textFilter() +}]; + +const sourceCode = `\ +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() +}]; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/package.json b/packages/react-bootstrap-table2-example/package.json index a726ef5..419f884 100644 --- a/packages/react-bootstrap-table2-example/package.json +++ b/packages/react-bootstrap-table2-example/package.json @@ -19,7 +19,8 @@ "bootstrap": "^3.3.7", "react-bootstrap-table2": "0.0.1", "react-bootstrap-table2-paginator": "0.0.1", - "react-bootstrap-table2-overlay": "0.0.1" + "react-bootstrap-table2-overlay": "0.0.1", + "react-bootstrap-table2-filter": "0.0.1" }, "devDependencies": { "@storybook/addon-console": "^1.0.0", diff --git a/packages/react-bootstrap-table2-example/src/utils/common.js b/packages/react-bootstrap-table2-example/src/utils/common.js index 724e042..0d5ad84 100644 --- a/packages/react-bootstrap-table2-example/src/utils/common.js +++ b/packages/react-bootstrap-table2-example/src/utils/common.js @@ -20,4 +20,12 @@ export const productsGenerator = (quantity = 5, callback) => { ); }; +export const jobsGenerator = (quantity = 5) => + Array.from({ length: quantity }, (value, index) => ({ + id: index, + name: `Job name ${index}`, + owner: Math.floor(Math.random() * 3), + type: Math.floor(Math.random() * 5) + })); + export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js index 738cfc6..b2f0f15 100644 --- a/packages/react-bootstrap-table2-example/stories/index.js +++ b/packages/react-bootstrap-table2-example/stories/index.js @@ -32,6 +32,13 @@ import HeaderColumnClassTable from 'examples/header-columns/column-class-table'; import HeaderColumnStyleTable from 'examples/header-columns/column-style-table'; import HeaderColumnAttrsTable from 'examples/header-columns/column-attrs-table'; +// column filter +import TextFilter from 'examples/column-filter/text-filter'; +import TextFilterWithDefaultValue from 'examples/column-filter/text-filter-default-value'; +import TextFilterComparator from 'examples/column-filter/text-filter-eq-comparator'; +import CustomTextFilter from 'examples/column-filter/custom-text-filter'; +import CustomFilterValue from 'examples/column-filter/custom-filter-value'; + // work on rows import RowStyleTable from 'examples/rows/row-style'; import RowClassTable from 'examples/rows/row-class'; @@ -121,6 +128,14 @@ storiesOf('Work on Header Columns', module) .add('Customize Column Style', () => ) .add('Customize Column HTML attribute', () => ); +storiesOf('Column Filter', module) + .add('Text Filter', () => ) + .add('Text Filter with Default Value', () => ) + .add('Text Filter with Comparator', () => ) + .add('Custom Text Filter', () => ) + // add another filter type example right here. + .add('Custom Filter Value', () => ); + storiesOf('Work on Rows', module) .add('Customize Row Style', () => ) .add('Customize Row Class', () => ) diff --git a/packages/react-bootstrap-table2-filter/README.md b/packages/react-bootstrap-table2-filter/README.md new file mode 100644 index 0000000..8cef968 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/README.md @@ -0,0 +1,40 @@ +# react-bootstrap-table2-filter + +## Filters + +* Text (`textFilter`) + +You can get all of above filters via import and these filters are a factory function to create a individual filter instance. +In addition, for some simple customization reasons, these factory function allow to pass some props. + +### Text Filter + +```js +import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; + +// omit... +const columns = [ + ..., { + dataField: 'price', + text: 'Product Price', + filter: textFilter() +}]; + + +``` + +Following we list all the availabe props for `textFilter` function: + +```js +import { Comparator } from 'react-bootstrap-table2-filter'; +// omit... + +const customTextFilter = textFilter({ + placeholder: 'My Custom PlaceHolder', // custom the input placeholder + style: { ... }, // your custom styles on input + className: 'my-custom-text-filter', // custom classname on input + defaultValue: 'test', // default filtering value + delay: 1000, // how long will trigger filtering after user typing, default is 500 ms + comparator: Comparator.EQ // default is Comparator.LIKE +}); +``` \ No newline at end of file diff --git a/packages/react-bootstrap-table2-filter/package.json b/packages/react-bootstrap-table2-filter/package.json new file mode 100644 index 0000000..826efa7 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/package.json @@ -0,0 +1,11 @@ +{ + "name": "react-bootstrap-table2-filter", + "version": "0.0.1", + "description": "it's the column filter addon for react-bootstrap-table2", + "main": "src/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/packages/react-bootstrap-table2-filter/src/comparison.js b/packages/react-bootstrap-table2-filter/src/comparison.js new file mode 100644 index 0000000..cc24214 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/src/comparison.js @@ -0,0 +1,2 @@ +export const LIKE = 'LIKE'; +export const EQ = '='; diff --git a/packages/react-bootstrap-table2-filter/src/components/text.js b/packages/react-bootstrap-table2-filter/src/components/text.js new file mode 100644 index 0000000..1c102c3 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/src/components/text.js @@ -0,0 +1,107 @@ +/* eslint react/require-default-props: 0 */ +/* eslint react/prop-types: 0 */ +/* eslint no-return-assign: 0 */ +import React, { Component } from 'react'; +import { PropTypes } from 'prop-types'; + +import { LIKE, EQ } from '../comparison'; +import { FILTER_TYPE, FILTER_DELAY } from '../const'; + +class TextFilter extends Component { + constructor(props) { + super(props); + this.filter = this.filter.bind(this); + this.handleClick = this.handleClick.bind(this); + this.timeout = null; + this.state = { + value: props.defaultValue + }; + } + componentDidMount() { + const defaultValue = this.input.value; + if (defaultValue) { + this.props.onFilter(this.props.column, defaultValue, FILTER_TYPE.TEXT); + } + } + + componentWillReceiveProps(nextProps) { + if (nextProps.defaultValue !== this.props.defaultValue) { + this.applyFilter(nextProps.defaultValue); + } + } + + componentWillUnmount() { + this.cleanTimer(); + } + + filter(e) { + e.stopPropagation(); + this.cleanTimer(); + const filterValue = e.target.value; + this.setState(() => ({ value: filterValue })); + this.timeout = setTimeout(() => { + this.props.onFilter(this.props.column, filterValue, FILTER_TYPE.TEXT); + }, this.props.delay); + } + + cleanTimer() { + if (this.timeout) { + clearTimeout(this.timeout); + } + } + + cleanFiltered() { + const value = this.props.defaultValue; + this.setState(() => ({ value })); + this.props.onFilter(this.props.column, value, FILTER_TYPE.TEXT); + } + + applyFilter(filterText) { + this.setState(() => ({ value: filterText })); + this.props.onFilter(this.props.column, filterText, FILTER_TYPE.TEXT); + } + + handleClick(e) { + e.stopPropagation(); + if (this.props.onClick) { + this.props.onClick(e); + } + } + + render() { + const { placeholder, column: { text }, style, className, onFilter, ...rest } = this.props; + // stopPropagation for onClick event is try to prevent sort was triggered. + return ( + this.input = n } + type="text" + className={ `filter text-filter form-control ${className}` } + style={ style } + onChange={ this.filter } + onClick={ this.handleClick } + placeholder={ placeholder || `Enter ${text}...` } + value={ this.state.value } + /> + ); + } +} + +TextFilter.propTypes = { + onFilter: PropTypes.func.isRequired, + column: PropTypes.object.isRequired, + comparator: PropTypes.oneOf([LIKE, EQ]), + defaultValue: PropTypes.string, + delay: PropTypes.number, + placeholder: PropTypes.string, + style: PropTypes.object, + className: PropTypes.string +}; + +TextFilter.defaultProps = { + delay: FILTER_DELAY, + defaultValue: '' +}; + + +export default TextFilter; diff --git a/packages/react-bootstrap-table2-filter/src/const.js b/packages/react-bootstrap-table2-filter/src/const.js new file mode 100644 index 0000000..25faae0 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/src/const.js @@ -0,0 +1,5 @@ +export const FILTER_TYPE = { + TEXT: 'TEXT' +}; + +export const FILTER_DELAY = 500; diff --git a/packages/react-bootstrap-table2-filter/src/filter.js b/packages/react-bootstrap-table2-filter/src/filter.js new file mode 100644 index 0000000..03914a8 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/src/filter.js @@ -0,0 +1,45 @@ +import { FILTER_TYPE } from './const'; +import { LIKE, EQ } from './comparison'; + +export const filterByText = _ => ( + data, + dataField, + { filterVal, comparator = LIKE }, + customFilterValue +) => + data.filter((row) => { + let cell = _.get(row, dataField); + if (customFilterValue) { + cell = customFilterValue(cell, row); + } + const cellStr = _.isDefined(cell) ? cell.toString() : ''; + if (comparator === EQ) { + return cellStr === filterVal; + } + return cellStr.indexOf(filterVal) > -1; + }); + +export const filterFactory = _ => (filterType) => { + let filterFn; + switch (filterType) { + case FILTER_TYPE.TEXT: + filterFn = filterByText(_); + break; + default: + filterFn = filterByText(_); + } + return filterFn; +}; + +export const filters = (store, columns, _) => (currFilters) => { + const factory = filterFactory(_); + let result = store.getAllData(); + let filterFn; + Object.keys(currFilters).forEach((dataField) => { + const filterObj = currFilters[dataField]; + filterFn = factory(filterObj.filterType); + const { filterValue } = columns.find(col => col.dataField === dataField); + result = filterFn(result, dataField, filterObj, filterValue); + }); + return result; +}; diff --git a/packages/react-bootstrap-table2-filter/src/index.js b/packages/react-bootstrap-table2-filter/src/index.js new file mode 100644 index 0000000..4bba49d --- /dev/null +++ b/packages/react-bootstrap-table2-filter/src/index.js @@ -0,0 +1,15 @@ +import TextFilter from './components/text'; +import FilterWrapper from './wrapper'; +import * as Comparison from './comparison'; + +export default (options = {}) => ({ + FilterWrapper, + options +}); + +export const Comparator = Comparison; + +export const textFilter = (props = {}) => ({ + Filter: TextFilter, + props +}); diff --git a/packages/react-bootstrap-table2-filter/src/wrapper.js b/packages/react-bootstrap-table2-filter/src/wrapper.js new file mode 100644 index 0000000..0cd8f44 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/src/wrapper.js @@ -0,0 +1,49 @@ +import { Component } from 'react'; +import PropTypes from 'prop-types'; +import { filters } from './filter'; + +export default class FilterWrapper extends Component { + static propTypes = { + store: PropTypes.object.isRequired, + columns: PropTypes.array.isRequired, + baseElement: PropTypes.func.isRequired, + _: PropTypes.object.isRequired + } + + constructor(props) { + super(props); + this.state = { currFilters: {}, isDataChanged: false }; + this.onFilter = this.onFilter.bind(this); + } + + componentWillReceiveProps() { + this.setState(() => ({ isDataChanged: false })); + } + + onFilter(column, filterVal, filterType) { + const { store, columns, _ } = this.props; + const { currFilters } = this.state; + const { dataField, filter } = column; + + if (!_.isDefined(filterVal) || filterVal === '') { + delete currFilters[dataField]; + } else { + const { comparator } = filter.props; + currFilters[dataField] = { filterVal, filterType, comparator }; + } + + store.filteredData = filters(store, columns, _)(currFilters); + store.filtering = Object.keys(currFilters).length > 0; + + this.setState(() => ({ currFilters, isDataChanged: true })); + } + + render() { + return this.props.baseElement({ + ...this.props, + key: 'table', + onFilter: this.onFilter, + isDataChanged: this.state.isDataChanged + }); + } +} diff --git a/packages/react-bootstrap-table2-filter/test/components/text.test.js b/packages/react-bootstrap-table2-filter/test/components/text.test.js new file mode 100644 index 0000000..4a9c282 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/test/components/text.test.js @@ -0,0 +1,190 @@ +import 'jsdom-global/register'; +import React from 'react'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; +import TextFilter from '../../src/components/text'; +import { FILTER_TYPE } from '../../src/const'; + +jest.useFakeTimers(); +describe('Text Filter', () => { + let wrapper; + let instance; + const onFilter = sinon.stub(); + const column = { + dataField: 'price', + text: 'Price' + }; + + afterEach(() => { + onFilter.reset(); + }); + + describe('initialization', () => { + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + }); + + it('should have correct state', () => { + expect(instance.state.value).toEqual(instance.props.defaultValue); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(wrapper.find('input[type="text"]')).toHaveLength(1); + expect(instance.input.getAttribute('placeholder')).toEqual(`Enter ${column.text}...`); + }); + }); + + describe('when defaultValue is defined', () => { + const defaultValue = '123'; + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + }); + + it('should have correct state', () => { + expect(instance.state.value).toEqual(defaultValue); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(instance.input.value).toEqual(defaultValue); + }); + + it('should calling onFilter on componentDidMount', () => { + expect(onFilter.calledOnce).toBeTruthy(); + expect(onFilter.calledWith(column, defaultValue, FILTER_TYPE.TEXT)).toBeTruthy(); + }); + }); + + describe('when placeholder is defined', () => { + const placeholder = 'test'; + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(instance.input.getAttribute('placeholder')).toEqual(placeholder); + }); + }); + + describe('when style is defined', () => { + const style = { backgroundColor: 'red' }; + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(wrapper.find('input').prop('style')).toEqual(style); + }); + }); + + describe('componentWillReceiveProps', () => { + const nextDefaultValue = 'tester'; + const nextProps = { + onFilter, + column, + defaultValue: nextDefaultValue + }; + + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + instance.componentWillReceiveProps(nextProps); + }); + + it('should setting state correctly when props.defaultValue is changed', () => { + expect(instance.state.value).toEqual(nextDefaultValue); + }); + + it('should calling onFilter correctly when props.defaultValue is changed', () => { + expect(onFilter.calledOnce).toBeTruthy(); + expect(onFilter.calledWith(column, nextDefaultValue, FILTER_TYPE.TEXT)).toBeTruthy(); + }); + }); + + describe('cleanFiltered', () => { + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + instance.cleanFiltered(); + }); + + it('should setting state correctly', () => { + expect(instance.state.value).toEqual(instance.props.defaultValue); + }); + + it('should calling onFilter correctly', () => { + expect(onFilter.calledOnce).toBeTruthy(); + expect(onFilter.calledWith( + column, instance.props.defaultValue, FILTER_TYPE.TEXT)).toBeTruthy(); + }); + }); + + describe('applyFilter', () => { + const filterText = 'test'; + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + instance.applyFilter(filterText); + }); + + it('should setting state correctly', () => { + expect(instance.state.value).toEqual(filterText); + }); + + it('should calling onFilter correctly', () => { + expect(onFilter.calledOnce).toBeTruthy(); + expect(onFilter.calledWith(column, filterText, FILTER_TYPE.TEXT)).toBeTruthy(); + }); + }); + + describe('filter', () => { + const event = { stopPropagation: sinon.stub(), target: { value: 'tester' } }; + + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + instance.filter(event); + }); + + afterEach(() => { + setTimeout.mockClear(); + }); + + it('should calling e.stopPropagation', () => { + expect(event.stopPropagation.calledOnce).toBeTruthy(); + }); + + it('should setting state correctly', () => { + expect(instance.state.value).toEqual(event.target.value); + }); + + it('should calling setTimeout correctly', () => { + expect(setTimeout.mock.calls).toHaveLength(1); + expect(setTimeout.mock.calls[0]).toHaveLength(2); + expect(setTimeout.mock.calls[0][1]).toEqual(instance.props.delay); + }); + }); +}); diff --git a/packages/react-bootstrap-table2-filter/test/filter.test.js b/packages/react-bootstrap-table2-filter/test/filter.test.js new file mode 100644 index 0000000..508c25f --- /dev/null +++ b/packages/react-bootstrap-table2-filter/test/filter.test.js @@ -0,0 +1,94 @@ +import sinon from 'sinon'; +import _ from 'react-bootstrap-table2/src/utils'; +import Store from 'react-bootstrap-table2/src/store'; + +import { filters } from '../src/filter'; +import { FILTER_TYPE } from '../src/const'; +import { LIKE, EQ } from '../src/comparison'; + +const data = []; +for (let i = 0; i < 20; i += 1) { + data.push({ + id: i, + name: `itme name ${i}`, + price: 200 + i + }); +} + +describe('filter', () => { + let store; + let filterFn; + let currFilters; + let columns; + + beforeEach(() => { + store = new Store('id'); + store.data = data; + currFilters = {}; + columns = [{ + dataField: 'id', + text: 'ID' + }, { + dataField: 'name', + text: 'Name' + }, { + dataField: 'price', + text: 'Price' + }]; + }); + + describe('text filter', () => { + beforeEach(() => { + filterFn = filters(store, columns, _); + }); + + describe(`when default comparator is ${LIKE}`, () => { + it('should returning correct result', () => { + currFilters.name = { + filterVal: '3', + filterType: FILTER_TYPE.TEXT + }; + + const result = filterFn(currFilters); + expect(result).toBeDefined(); + expect(result).toHaveLength(2); + }); + }); + + describe(`when default comparator is ${EQ}`, () => { + it('should returning correct result', () => { + currFilters.name = { + filterVal: 'itme name 3', + filterType: FILTER_TYPE.TEXT, + comparator: EQ + }; + + const result = filterFn(currFilters); + expect(result).toBeDefined(); + expect(result).toHaveLength(1); + }); + }); + + describe('column.filterValue is defined', () => { + beforeEach(() => { + columns[1].filterValue = sinon.stub(); + filterFn = filters(store, columns, _); + }); + + it('should calling custom filterValue callback correctly', () => { + currFilters.name = { + filterVal: '3', + filterType: FILTER_TYPE.TEXT + }; + + const result = filterFn(currFilters); + expect(result).toBeDefined(); + expect(columns[1].filterValue.callCount).toBe(data.length); + const calls = columns[1].filterValue.getCalls(); + calls.forEach((call, i) => { + expect(call.calledWith(data[i].name, data[i])).toBeTruthy(); + }); + }); + }); + }); +}); diff --git a/packages/react-bootstrap-table2-filter/test/wrapper.test.js b/packages/react-bootstrap-table2-filter/test/wrapper.test.js new file mode 100644 index 0000000..c877c62 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/test/wrapper.test.js @@ -0,0 +1,168 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import _ from 'react-bootstrap-table2/src/utils'; +import BootstrapTable from 'react-bootstrap-table2/src/bootstrap-table'; +import Store from 'react-bootstrap-table2/src/store'; +import filter, { textFilter } from '../src'; +import FilterWrapper 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 createTableProps = () => { + 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') + }; + tableProps.store.data = data; + return tableProps; + }; + + const pureTable = props => (); + + 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; + + beforeEach(() => { + nextProps = createTableProps(); + instance.componentWillReceiveProps(nextProps); + }); + + it('should setting isDataChanged as false always(Temporary solution)', () => { + expect(instance.state.isDataChanged).toBeFalsy(); + }); + }); + + 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], filterVal, FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeFalsy(); + }); + }); + + it('should setting state correctly', () => { + filterVals.forEach((filterVal) => { + instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT); + 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], filterVal, FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeTruthy(); + }); + + it('should setting state correctly', () => { + instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(1); + }); + }); + + describe('combination', () => { + it('should setting store object correctly', () => { + instance.onFilter(props.columns[1], '3', FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeTruthy(); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(1); + + instance.onFilter(props.columns[1], '2', FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeTruthy(); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(1); + + instance.onFilter(props.columns[2], '2', FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeTruthy(); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(2); + + instance.onFilter(props.columns[2], '', FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeTruthy(); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(1); + + instance.onFilter(props.columns[1], '', FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeFalsy(); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(0); + }); + }); + }); +}); diff --git a/packages/react-bootstrap-table2-paginator/src/page-resolver.js b/packages/react-bootstrap-table2-paginator/src/page-resolver.js index ef2e6d2..931595f 100644 --- a/packages/react-bootstrap-table2-paginator/src/page-resolver.js +++ b/packages/react-bootstrap-table2-paginator/src/page-resolver.js @@ -18,8 +18,7 @@ export default ExtendBase => return { totalPages, lastPage, dropdownOpen: false }; } - calculateTotalPage(sizePerPage = this.props.currSizePerPage) { - const { dataSize } = this.props; + calculateTotalPage(sizePerPage = this.props.currSizePerPage, dataSize = this.props.dataSize) { return Math.ceil(dataSize / sizePerPage); } diff --git a/packages/react-bootstrap-table2-paginator/src/page.js b/packages/react-bootstrap-table2-paginator/src/page.js index c18ede9..1ea54e5 100644 --- a/packages/react-bootstrap-table2-paginator/src/page.js +++ b/packages/react-bootstrap-table2-paginator/src/page.js @@ -1,11 +1,12 @@ export const getByCurrPage = store => (page, sizePerPage, pageStartIndex) => { + const dataSize = store.data.length; + if (!dataSize) return []; const getNormalizedPage = () => { const offset = Math.abs(1 - pageStartIndex); return page + offset; }; const end = (getNormalizedPage() * sizePerPage) - 1; const start = end - (sizePerPage - 1); - const dataSize = store.data.length; const result = []; for (let i = start; i <= end; i += 1) { diff --git a/packages/react-bootstrap-table2-paginator/src/pagination.js b/packages/react-bootstrap-table2-paginator/src/pagination.js index ee685c7..d103e4b 100644 --- a/packages/react-bootstrap-table2-paginator/src/pagination.js +++ b/packages/react-bootstrap-table2-paginator/src/pagination.js @@ -20,9 +20,8 @@ class Pagination extends pageResolver(Component) { componentWillReceiveProps(nextProps) { const { dataSize, currSizePerPage } = nextProps; - if (currSizePerPage !== this.props.currSizePerPage || dataSize !== this.props.dataSize) { - const totalPages = this.calculateTotalPage(currSizePerPage); + const totalPages = this.calculateTotalPage(currSizePerPage, dataSize); const lastPage = this.calculateLastPage(totalPages); this.setState({ totalPages, lastPage }); } diff --git a/packages/react-bootstrap-table2-paginator/src/wrapper.js b/packages/react-bootstrap-table2-paginator/src/wrapper.js index 94995cd..f54a8d9 100644 --- a/packages/react-bootstrap-table2-paginator/src/wrapper.js +++ b/packages/react-bootstrap-table2-paginator/src/wrapper.js @@ -48,11 +48,16 @@ class PaginationWrapper extends Component { componentWillReceiveProps(nextProps) { let needNewState = false; let { currPage, currSizePerPage } = this.state; - const { page, sizePerPage } = nextProps.pagination.options; - if (typeof page !== 'undefined') { + const { page, sizePerPage, pageStartIndex } = nextProps.pagination.options; + + if (typeof page !== 'undefined') { // user defined page currPage = page; needNewState = true; + } else if (nextProps.isDataChanged) { // user didn't defined page but data change + currPage = typeof pageStartIndex !== 'undefined' ? pageStartIndex : Const.PAGE_START_INDEX; + needNewState = true; } + if (typeof sizePerPage !== 'undefined') { currSizePerPage = sizePerPage; needNewState = true; diff --git a/packages/react-bootstrap-table2-paginator/test/page.test.js b/packages/react-bootstrap-table2-paginator/test/page.test.js index e8ccafa..f073c86 100644 --- a/packages/react-bootstrap-table2-paginator/test/page.test.js +++ b/packages/react-bootstrap-table2-paginator/test/page.test.js @@ -4,6 +4,18 @@ import { getByCurrPage } from '../src/page'; describe('Page Functions', () => { let data; let store; + const params = [ + // [page, sizePerPage, pageStartIndex] + [1, 10, 1], + [1, 25, 1], + [1, 30, 1], + [3, 30, 1], + [4, 30, 1], + [10, 10, 1], + [0, 10, 0], + [1, 10, 0], + [9, 10, 0] + ]; describe('getByCurrPage', () => { beforeEach(() => { @@ -16,23 +28,20 @@ describe('Page Functions', () => { }); it('should always return correct data', () => { - [ - // [page, sizePerPage, pageStartIndex] - [1, 10, 1], - [1, 25, 1], - [1, 30, 1], - [3, 30, 1], - [4, 30, 1], - [10, 10, 1], - [0, 10, 0], - [1, 10, 0], - [9, 10, 0] - ].forEach(([page, sizePerPage, pageStartIndex]) => { + params.forEach(([page, sizePerPage, pageStartIndex]) => { const rows = getByCurrPage(store)(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 = []; + params.forEach(([page, sizePerPage, pageStartIndex]) => { + const rows = getByCurrPage(store)(page, sizePerPage, pageStartIndex); + expect(rows).toHaveLength(0); + }); + }); }); }); diff --git a/packages/react-bootstrap-table2-paginator/test/pagination.test.js b/packages/react-bootstrap-table2-paginator/test/pagination.test.js index cf8e44d..a9e5d35 100644 --- a/packages/react-bootstrap-table2-paginator/test/pagination.test.js +++ b/packages/react-bootstrap-table2-paginator/test/pagination.test.js @@ -143,12 +143,13 @@ describe('Pagination', () => { it('should setting correct state.totalPages', () => { instance.componentWillReceiveProps(nextProps); expect(instance.state.totalPages).toEqual( - instance.calculateTotalPage(nextProps.currSizePerPage)); + instance.calculateTotalPage(nextProps.currSizePerPage, nextProps.dataSize)); }); it('should setting correct state.lastPage', () => { instance.componentWillReceiveProps(nextProps); - const totalPages = instance.calculateTotalPage(nextProps.currSizePerPage); + const totalPages = instance.calculateTotalPage( + nextProps.currSizePerPage, nextProps.dataSize); expect(instance.state.lastPage).toEqual( instance.calculateLastPage(totalPages)); }); diff --git a/packages/react-bootstrap-table2-paginator/test/wrapper.test.js b/packages/react-bootstrap-table2-paginator/test/wrapper.test.js index 16852d0..7254e2a 100644 --- a/packages/react-bootstrap-table2-paginator/test/wrapper.test.js +++ b/packages/react-bootstrap-table2-paginator/test/wrapper.test.js @@ -100,29 +100,47 @@ describe('Wrapper', () => { }); describe('componentWillReceiveProps', () => { - it('should setting currPage state correclt by options.page', () => { - props.pagination.options.page = 2; - instance.componentWillReceiveProps(props); - expect(instance.state.currPage).toEqual(props.pagination.options.page); + let nextProps; + beforeEach(() => { + nextProps = createTableProps(); + }); + + it('should setting currPage state correctly by options.page', () => { + nextProps.pagination.options.page = 2; + instance.componentWillReceiveProps(nextProps); + expect(instance.state.currPage).toEqual(nextProps.pagination.options.page); }); it('should not setting currPage state if options.page not existing', () => { const { currPage } = instance.state; - instance.componentWillReceiveProps(props); + instance.componentWillReceiveProps(nextProps); expect(instance.state.currPage).toBe(currPage); }); - it('should setting currSizePerPage state correclt by options.sizePerPage', () => { - props.pagination.options.sizePerPage = 20; - instance.componentWillReceiveProps(props); - expect(instance.state.currSizePerPage).toEqual(props.pagination.options.sizePerPage); + it('should setting currSizePerPage state correctly by options.sizePerPage', () => { + nextProps.pagination.options.sizePerPage = 20; + instance.componentWillReceiveProps(nextProps); + expect(instance.state.currSizePerPage).toEqual(nextProps.pagination.options.sizePerPage); }); it('should not setting currSizePerPage state if options.sizePerPage not existing', () => { const { currSizePerPage } = instance.state; - instance.componentWillReceiveProps(props); + instance.componentWillReceiveProps(nextProps); expect(instance.state.currSizePerPage).toBe(currSizePerPage); }); + + it('should setting currPage state when nextProps.isDataChanged is true', () => { + nextProps.isDataChanged = true; + instance.componentWillReceiveProps(nextProps); + expect(instance.state.currPage).toBe(Const.PAGE_START_INDEX); + }); + + it('should setting currPage state when nextProps.isDataChanged is true and options.pageStartIndex is existing', () => { + nextProps.isDataChanged = true; + nextProps.pagination.options.pageStartIndex = 0; + instance.componentWillReceiveProps(nextProps); + expect(instance.state.currPage).toBe(nextProps.pagination.options.pageStartIndex); + }); }); }); diff --git a/packages/react-bootstrap-table2/src/bootstrap-table.js b/packages/react-bootstrap-table2/src/bootstrap-table.js index ca7000c..19e7afa 100644 --- a/packages/react-bootstrap-table2/src/bootstrap-table.js +++ b/packages/react-bootstrap-table2/src/bootstrap-table.js @@ -86,6 +86,7 @@ class BootstrapTable extends PropsBaseResolver(Component) { sortField={ store.sortField } sortOrder={ store.sortOrder } onSort={ this.props.onSort } + onFilter={ this.props.onFilter } selectRow={ headerCellSelectionInfo } /> }); } else if (this.props.selectRow) { return wrapWithSelection(baseProps); + } else if (this.props.filter) { + return wrapWithFilter(baseProps); } else if (this.props.columns.filter(col => col.sort).length > 0) { return wrapWithSort(baseProps); } else if (this.props.pagination) { diff --git a/packages/react-bootstrap-table2/src/header-cell.js b/packages/react-bootstrap-table2/src/header-cell.js index fb57ade..80f819c 100644 --- a/packages/react-bootstrap-table2/src/header-cell.js +++ b/packages/react-bootstrap-table2/src/header-cell.js @@ -16,12 +16,14 @@ const HeaderCell = (props) => { onSort, sorting, sortOrder, - isLastSorting + isLastSorting, + onFilter } = props; const { text, sort, + filter, hidden, headerTitle, headerAlign, @@ -38,10 +40,13 @@ const HeaderCell = (props) => { ..._.isFunction(headerAttrs) ? headerAttrs(column, index) : headerAttrs, ...headerEvents }; + // we are suppose to pass sortSymbol and filerElm + // the headerFormatter is not only header text but also the all of header cell customization const children = headerFormatter ? headerFormatter(column, index) : text; - let cellStyle = {}; let sortSymbol; + let filterElm; + let cellStyle = {}; let cellClasses = _.isFunction(headerClasses) ? headerClasses(column, index) : headerClasses; if (headerStyle) { @@ -91,12 +96,14 @@ const HeaderCell = (props) => { } if (cellClasses) cellAttrs.className = cs(cellAttrs.className, cellClasses); - if (!_.isEmptyObject(cellStyle)) cellAttrs.style = cellStyle; + if (filter) { + filterElm = ; + } return ( - { children }{ sortSymbol } + { children }{ sortSymbol }{ filterElm } ); }; @@ -126,13 +133,16 @@ HeaderCell.propTypes = { editable: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), editCellStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), editCellClasses: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - validator: PropTypes.func + validator: PropTypes.func, + filter: PropTypes.object, + filterValue: PropTypes.func }).isRequired, index: PropTypes.number.isRequired, onSort: PropTypes.func, sorting: PropTypes.bool, sortOrder: PropTypes.oneOf([Const.SORT_ASC, Const.SORT_DESC]), - isLastSorting: PropTypes.bool + isLastSorting: PropTypes.bool, + onFilter: PropTypes.func }; export default HeaderCell; diff --git a/packages/react-bootstrap-table2/src/header.js b/packages/react-bootstrap-table2/src/header.js index da77cb9..ddbc7bf 100644 --- a/packages/react-bootstrap-table2/src/header.js +++ b/packages/react-bootstrap-table2/src/header.js @@ -12,6 +12,7 @@ const Header = (props) => { const { columns, onSort, + onFilter, sortField, sortOrder, selectRow @@ -36,6 +37,7 @@ const Header = (props) => { column={ column } onSort={ onSort } sorting={ currSort } + onFilter={ onFilter } sortOrder={ sortOrder } isLastSorting={ isLastSorting } />); @@ -49,6 +51,7 @@ const Header = (props) => { Header.propTypes = { columns: PropTypes.array.isRequired, onSort: PropTypes.func, + onFilter: PropTypes.func, sortField: PropTypes.string, sortOrder: PropTypes.string, selectRow: PropTypes.object diff --git a/packages/react-bootstrap-table2/src/sort/wrapper.js b/packages/react-bootstrap-table2/src/sort/wrapper.js index fcb644c..82f00b2 100644 --- a/packages/react-bootstrap-table2/src/sort/wrapper.js +++ b/packages/react-bootstrap-table2/src/sort/wrapper.js @@ -26,6 +26,16 @@ class SortWrapper extends Component { } } + componentWillReceiveProps(nextProps) { + if (nextProps.isDataChanged) { + const sortedColumn = nextProps.columns.find( + column => column.dataField === nextProps.store.sortField); + if (sortedColumn) { + nextProps.store.sortBy(sortedColumn, nextProps.store.sortOrder); + } + } + } + handleSort(column) { const { store } = this.props; store.sortBy(column); diff --git a/packages/react-bootstrap-table2/src/store/index.js b/packages/react-bootstrap-table2/src/store/index.js index c39e47d..97d1aba 100644 --- a/packages/react-bootstrap-table2/src/store/index.js +++ b/packages/react-bootstrap-table2/src/store/index.js @@ -6,11 +6,12 @@ import { getRowByRowId } from './rows'; export default class Store { constructor(keyField) { this._data = []; + this._filteredData = []; this._keyField = keyField; - this._sortOrder = undefined; this._sortField = undefined; this._selected = []; + this._filtering = false; } edit(rowId, dataField, newValue) { @@ -24,8 +25,26 @@ export default class Store { this.data = sort(this)(sortFunc); } - get data() { return this._data; } - set data(data) { this._data = (data ? JSON.parse(JSON.stringify(data)) : []); } + getAllData() { + return this._data; + } + + get data() { + if (this._filtering) { + return this._filteredData; + } + return this._data; + } + set data(data) { + if (this._filtering) { + this._filteredData = data; + } else { + this._data = (data ? JSON.parse(JSON.stringify(data)) : []); + } + } + + get filteredData() { return this._filteredData; } + set filteredData(filteredData) { this._filteredData = filteredData; } get keyField() { return this._keyField; } set keyField(keyField) { this._keyField = keyField; } @@ -38,4 +57,7 @@ export default class Store { get selected() { return this._selected; } set selected(selected) { this._selected = selected; } + + get filtering() { return this._filtering; } + set filtering(filtering) { this._filtering = filtering; } } diff --git a/packages/react-bootstrap-table2/src/table-factory.js b/packages/react-bootstrap-table2/src/table-factory.js index d90453d..10ff86e 100644 --- a/packages/react-bootstrap-table2/src/table-factory.js +++ b/packages/react-bootstrap-table2/src/table-factory.js @@ -1,6 +1,7 @@ /* eslint react/prop-types: 0 */ import React from 'react'; +import _ from './utils'; import BootstrapTable from './bootstrap-table'; import SortWrapper from './sort/wrapper'; import RowSelectionWrapper from './row-selection/wrapper'; @@ -19,6 +20,14 @@ export const wrapWithSort = props => export const pureTable = props => React.createElement(BootstrapTable, { ...props }); +export const wrapWithFilter = (props) => { + if (props.filter) { + const { FilterWrapper } = props.filter; + return React.createElement(FilterWrapper, { ...props, baseElement: wrapWithSort, _ }); + } + return wrapWithSort(props); +}; + export const wrapWithPagination = (props) => { if (props.pagination) { const { PaginationWrapper } = props.pagination; @@ -29,6 +38,6 @@ export const wrapWithPagination = (props) => { export const sortableElement = props => wrapWithPagination(props); -export const selectionElement = props => wrapWithSort(props); +export const selectionElement = props => wrapWithFilter(props); export const cellEditElement = props => wrapWithSelection(props); diff --git a/packages/react-bootstrap-table2/test/sort/wrapper.test.js b/packages/react-bootstrap-table2/test/sort/wrapper.test.js index 6618192..733f67d 100644 --- a/packages/react-bootstrap-table2/test/sort/wrapper.test.js +++ b/packages/react-bootstrap-table2/test/sort/wrapper.test.js @@ -1,5 +1,6 @@ import 'jsdom-global/register'; import React from 'react'; +import sinon from 'sinon'; import { shallow, mount } from 'enzyme'; import Const from '../../src/const'; @@ -114,4 +115,38 @@ describe('SortWrapper', () => { expect(store.sortOrder).toEqual(defaultSorted[0].order); }); }); + + describe('componentWillReceiveProps', () => { + let nextProps; + + beforeEach(() => { + nextProps = { columns, store }; + store.sortField = columns[1].dataField; + store.sortOrder = Const.SORT_DESC; + }); + + describe('if nextProps.isDataChanged is true', () => { + beforeEach(() => { + nextProps.isDataChanged = true; + store.sortBy = sinon.stub(); + }); + + it('should sorting again', () => { + wrapper.instance().componentWillReceiveProps(nextProps); + expect(store.sortBy.calledOnce).toBeTruthy(); + }); + }); + + describe('if nextProps.isDataChanged is false', () => { + beforeEach(() => { + nextProps.isDataChanged = false; + store.sortBy = sinon.stub(); + }); + + it('should not sorting', () => { + wrapper.instance().componentWillReceiveProps(nextProps); + expect(store.sortBy.calledOnce).toBeFalsy(); + }); + }); + }); });