diff --git a/docs/README.md b/docs/README.md index 828cae1..096ca24 100644 --- a/docs/README.md +++ b/docs/README.md @@ -145,7 +145,7 @@ Custom the events on row: ```js const rowEvents = { - onClick: (e) => { + onClick: (e, row, rowIndex) => { .... } }; diff --git a/docs/columns.md b/docs/columns.md index cf546a8..b80c48a 100644 --- a/docs/columns.md +++ b/docs/columns.md @@ -12,6 +12,7 @@ Available properties in a column object: * [formatExtraData](#formatExtraData) * [sort](#sort) * [sortFunc](#sortFunc) +* [onSort](#onSort) * [classes](#classes) * [style](#style) * [title](#title) @@ -122,6 +123,19 @@ Enable the column sort via a `true` value given. ``` > The possible value of `order` argument is **`asc`** and **`desc`**. +## column.onSort - [Function] +`column.onSort` is an event listener for sort change event: + +```js +{ + // omit... + sort: true, + onSort: (field, order) => { + // .... + } +} +``` + ## column.classes - [String | Function] It's availabe to have custom class on table column: diff --git a/docs/migration.md b/docs/migration.md index fb294c3..ae009e4 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -60,6 +60,7 @@ Please see [Work with table sort](https://react-bootstrap-table.github.io/react- - [x] Default Sort - [x] Remote mode - [x] Custom the sorting header +- [x] Sort event listener - [ ] Custom the sort caret - [ ] Sort management - [ ] Multi sort @@ -85,7 +86,7 @@ Please see [available filter configuration](https://react-bootstrap-table.github - [ ] Regex Filter - [x] Select Filter - [x] Custom Select Filter -- [ ] Number Filter +- [X] Number Filter - [ ] Date Filter - [ ] Array Filter - [ ] Programmatically Filter diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/custom-number-filter.js b/packages/react-bootstrap-table2-example/examples/column-filter/custom-number-filter.js new file mode 100644 index 0000000..29c361d --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/column-filter/custom-number-filter.js @@ -0,0 +1,74 @@ +import React from 'react'; +import BootstrapTable from 'react-bootstrap-table-next'; +import filterFactory, { numberFilter, 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' +}, { + dataField: 'price', + text: 'Product Price', + filter: numberFilter({ + options: [2100, 2103, 2105], + delay: 600, + placeholder: 'custom placeholder', + withoutEmptyComparatorOption: true, + comparators: [Comparator.EQ, Comparator.GT, Comparator.LT], + style: { display: 'inline-grid' }, + className: 'custom-numberfilter-class', + comparatorStyle: { backgroundColor: 'antiquewhite' }, + comparatorClassName: 'custom-comparator-class', + numberStyle: { backgroundColor: 'cadetblue', margin: '0px' }, + numberClassName: 'custom-number-class' + }) +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import filterFactory, { numberFilter, Comparator } from 'react-bootstrap-table2-filter'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price', + filter: numberFilter({ + options: [2100, 2103, 2105], + delay: 600, + placeholder: 'custom placeholder', + withoutEmptyComparatorOption: true, + comparators: [Comparator.EQ, Comparator.GT, Comparator.LT], + style: { display: 'inline-grid' }, + className: 'custom-numberfilter-class', + comparatorStyle: { backgroundColor: 'antiquewhite' }, + comparatorClassName: 'custom-comparator-class', + numberStyle: { backgroundColor: 'cadetblue', margin: '0px' }, + numberClassName: 'custom-number-class' + }) +}]; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/number-filter-default-value.js b/packages/react-bootstrap-table2-example/examples/column-filter/number-filter-default-value.js new file mode 100644 index 0000000..463b472 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/column-filter/number-filter-default-value.js @@ -0,0 +1,54 @@ +import React from 'react'; +import BootstrapTable from 'react-bootstrap-table-next'; +import filterFactory, { numberFilter, 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' +}, { + dataField: 'price', + text: 'Product Price', + filter: numberFilter({ + defaultValue: { number: 2103, comparator: Comparator.GT } + }) +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import filterFactory, { numberFilter, Comparator } from 'react-bootstrap-table2-filter'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price', + filter: numberFilter({ + defaultValue: { number: 2103, comparator: Comparator.GT } + }) +}]; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/number-filter.js b/packages/react-bootstrap-table2-example/examples/column-filter/number-filter.js new file mode 100644 index 0000000..b01167f --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/column-filter/number-filter.js @@ -0,0 +1,50 @@ +import React from 'react'; +import BootstrapTable from 'react-bootstrap-table-next'; +import filterFactory, { numberFilter } 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' +}, { + dataField: 'price', + text: 'Product Price', + filter: numberFilter() +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import filterFactory, { numberFilter } from 'react-bootstrap-table2-filter'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price', + filter: numberFilter() +}]; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/column-filter/text-filter-caseSensitive.js b/packages/react-bootstrap-table2-example/examples/column-filter/text-filter-caseSensitive.js new file mode 100644 index 0000000..aa1f421 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/column-filter/text-filter-caseSensitive.js @@ -0,0 +1,51 @@ +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); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name', + filter: textFilter({ caseSensitive: true }) +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import filterFactory, { textFilter } from 'react-bootstrap-table2-filter'; + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name', + filter: textFilter({ caseSensitive: true }) +}, { + dataField: 'price', + text: 'Product Price' +}]; + + +`; + +export default () => ( +
+

Product Name is case sensitive

+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/examples/rows/row-event.js b/packages/react-bootstrap-table2-example/examples/rows/row-event.js index 915f9af..1a57fb6 100644 --- a/packages/react-bootstrap-table2-example/examples/rows/row-event.js +++ b/packages/react-bootstrap-table2-example/examples/rows/row-event.js @@ -20,8 +20,8 @@ const columns = [{ }]; const rowEvents = { - onClick: (e) => { - alert('click on row'); + onClick: (e, row, rowIndex) => { + alert(`clicked on row with index: ${rowIndex}`); } }; @@ -40,8 +40,8 @@ const columns = [{ }]; const rowEvents = { - onClick: (e) => { - alert('click on row'); + onClick: (e, row, rowIndex) => { + alert(\`clicked on row with index: \${rowIndex}\`); } }; diff --git a/packages/react-bootstrap-table2-example/examples/sort/sort-events.js b/packages/react-bootstrap-table2-example/examples/sort/sort-events.js new file mode 100644 index 0000000..06b406f --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/sort/sort-events.js @@ -0,0 +1,58 @@ +/* eslint no-console: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID', + sort: true +}, { + dataField: 'name', + text: 'Product Name', + sort: true, + onSort: (field, order) => { + console.log(`Sort Field: ${field}, Sort Order: ${order}`); + } +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const defaultSorted = [{ + dataField: 'name', + order: 'desc' +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; + +const columns = [{ + dataField: 'id', + text: 'Product ID', + sort: true +}, { + dataField: 'name', + text: 'Product Name', + sort: true, + onSort: (field, order) => { + console.log(....); + } +}, { + dataField: 'price', + text: 'Product Price' +}]; + + +`; + +export default () => ( +
+ + { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js index f5e3db7..09bce47 100644 --- a/packages/react-bootstrap-table2-example/stories/index.js +++ b/packages/react-bootstrap-table2-example/stories/index.js @@ -37,12 +37,16 @@ import HeaderColumnAttrsTable from 'examples/header-columns/column-attrs-table'; 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 TextFilterCaseSensitive from 'examples/column-filter/text-filter-caseSensitive'; import CustomTextFilter from 'examples/column-filter/custom-text-filter'; import CustomFilterValue from 'examples/column-filter/custom-filter-value'; import SelectFilter from 'examples/column-filter/select-filter'; import SelectFilterWithDefaultValue from 'examples/column-filter/select-filter-default-value'; import SelectFilterComparator from 'examples/column-filter/select-filter-like-comparator'; import CustomSelectFilter from 'examples/column-filter/custom-select-filter'; +import NumberFilter from 'examples/column-filter/number-filter'; +import NumberFilterWithDefaultValue from 'examples/column-filter/number-filter-default-value'; +import CustomNumberFilter from 'examples/column-filter/custom-number-filter'; // work on rows import RowStyleTable from 'examples/rows/row-style'; @@ -52,6 +56,7 @@ import RowEventTable from 'examples/rows/row-event'; // table sort import EnableSortTable from 'examples/sort/enable-sort-table'; import DefaultSortTable from 'examples/sort/default-sort-table'; +import SortEvents from 'examples/sort/sort-events'; import CustomSortTable from 'examples/sort/custom-sort-table'; import HeaderSortingClassesTable from 'examples/sort/header-sorting-classes'; import HeaderSortingStyleTable from 'examples/sort/header-sorting-style'; @@ -143,12 +148,16 @@ storiesOf('Column Filter', module) .add('Text Filter', () => ) .add('Text Filter with Default Value', () => ) .add('Text Filter with Comparator', () => ) - .add('Custom Text Filter', () => ) + .add('Text Filter with Case Sensitive', () => ) // add another filter type example right here. .add('Select Filter', () => ) .add('Select Filter with Default Value', () => ) .add('Select Filter with Comparator', () => ) + .add('Number Filter', () => ) + .add('Number Filter with Default Value', () => ) + .add('Custom Text Filter', () => ) .add('Custom Select Filter', () => ) + .add('Custom Number Filter', () => ) .add('Custom Filter Value', () => ); storiesOf('Work on Rows', module) @@ -159,6 +168,7 @@ storiesOf('Work on Rows', module) storiesOf('Sort Table', module) .add('Enable Sort', () => ) .add('Default Sort Table', () => ) + .add('Sort Events', () => ) .add('Custom Sort Fuction', () => ) .add('Custom Classes on Sorting Header Column', () => ) .add('Custom Style on Sorting Header Column', () => ); diff --git a/packages/react-bootstrap-table2-filter/README.md b/packages/react-bootstrap-table2-filter/README.md index 7557419..07561bf 100644 --- a/packages/react-bootstrap-table2-filter/README.md +++ b/packages/react-bootstrap-table2-filter/README.md @@ -18,6 +18,7 @@ You can get all types of filters via import and these filters are a factory func * TextFilter * SelectFilter +* NumberFilter * **Coming soon!** ## Add CSS @@ -58,6 +59,7 @@ const priceFilter = textFilter({ className: 'my-custom-text-filter', // custom classname on input defaultValue: 'test', // default filtering value comparator: Comparator.EQ, // default is Comparator.LIKE + caseSensitive: true, // default is false, and true will only work when comparator is LIKE style: { ... }, // your custom styles on input delay: 1000 // how long will trigger filtering after user typing, default is 500 ms }); @@ -107,5 +109,44 @@ const qualityFilter = selectFilter({ withoutEmptyOption: true // hide the default select option }); +// omit... +``` + +## Number Filter + +```js +import filterFactory, { numberFilter } from 'react-bootstrap-table2-filter'; + +const columns = [..., { + dataField: 'price', + text: 'Product Price', + filter: numberFilter() +}]; + + +``` + +Numner filter is same as other filter, you can custom the number filter via `numberFilter` factory function: + +```js +import filterFactory, { selectFilter, Comparator } from 'react-bootstrap-table2-filter'; +// omit... + +const numberFilter = numberFilter({ + options: [2100, 2103, 2105], // if options defined, will render number select instead of number input + delay: 600, // how long will trigger filtering after user typing, default is 500 ms + placeholder: 'custom placeholder', // placeholder for number input + withoutEmptyComparatorOption: true, // dont render empty option for comparator + withoutEmptyNumberOption: true, // dont render empty option for numner select if it is defined + comparators: [Comparator.EQ, Comparator.GT, Comparator.LT], // Custom the comparators + style: { display: 'inline-grid' }, // custom the style on number filter + className: 'custom-numberfilter-class', // custom the class on number filter + comparatorStyle: { backgroundColor: 'antiquewhite' }, // custom the style on comparator select + comparatorClassName: 'custom-comparator-class', // custom the class on comparator select + numberStyle: { backgroundColor: 'cadetblue', margin: '0px' }, // custom the style on number input/select + numberClassName: 'custom-number-class', // custom the class on ber input/select + defaultValue: { number: 2103, comparator: Comparator.GT } // default value +}) + // omit... ``` \ No newline at end of file diff --git a/packages/react-bootstrap-table2-filter/index.js b/packages/react-bootstrap-table2-filter/index.js index 068b508..a2bc666 100644 --- a/packages/react-bootstrap-table2-filter/index.js +++ b/packages/react-bootstrap-table2-filter/index.js @@ -1,5 +1,6 @@ import TextFilter from './src/components/text'; import SelectFilter from './src/components/select'; +import NumberFilter from './src/components/number'; import wrapperFactory from './src/wrapper'; import * as Comparison from './src/comparison'; @@ -19,3 +20,8 @@ export const selectFilter = (props = {}) => ({ Filter: SelectFilter, props }); + +export const numberFilter = (props = {}) => ({ + Filter: NumberFilter, + props +}); diff --git a/packages/react-bootstrap-table2-filter/src/comparison.js b/packages/react-bootstrap-table2-filter/src/comparison.js index cc24214..7e599e7 100644 --- a/packages/react-bootstrap-table2-filter/src/comparison.js +++ b/packages/react-bootstrap-table2-filter/src/comparison.js @@ -1,2 +1,7 @@ export const LIKE = 'LIKE'; export const EQ = '='; +export const NE = '!='; +export const GT = '>'; +export const GE = '>='; +export const LT = '<'; +export const LE = '<='; diff --git a/packages/react-bootstrap-table2-filter/src/components/number.js b/packages/react-bootstrap-table2-filter/src/components/number.js new file mode 100644 index 0000000..3c2b612 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/src/components/number.js @@ -0,0 +1,249 @@ +/* eslint no-return-assign: 0 */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import * as Comparator from '../comparison'; +import { FILTER_TYPE, FILTER_DELAY } from '../const'; + +const legalComparators = [ + Comparator.EQ, + Comparator.NE, + Comparator.GT, + Comparator.GE, + Comparator.LT, + Comparator.LE +]; + +class NumberFilter extends Component { + constructor(props) { + super(props); + this.comparators = props.comparators || legalComparators; + this.timeout = null; + let isSelected = props.defaultValue !== undefined && props.defaultValue.number !== undefined; + if (props.options && isSelected) { + isSelected = props.options.indexOf(props.defaultValue.number) > -1; + } + this.state = { isSelected }; + this.onChangeNumber = this.onChangeNumber.bind(this); + this.onChangeNumberSet = this.onChangeNumberSet.bind(this); + this.onChangeComparator = this.onChangeComparator.bind(this); + } + + componentDidMount() { + const { column, onFilter } = this.props; + const comparator = this.numberFilterComparator.value; + const number = this.numberFilter.value; + if (comparator && number) { + onFilter(column, { number, comparator }, FILTER_TYPE.NUMBER); + } + } + + componentWillUnmount() { + clearTimeout(this.timeout); + } + + onChangeNumber(e) { + const { delay, column, onFilter } = this.props; + const comparator = this.numberFilterComparator.value; + if (comparator === '') { + return; + } + if (this.timeout) { + clearTimeout(this.timeout); + } + const filterValue = e.target.value; + this.timeout = setTimeout(() => { + onFilter(column, { number: filterValue, comparator }, FILTER_TYPE.NUMBER); + }, delay); + } + + onChangeNumberSet(e) { + const { column, onFilter } = this.props; + const comparator = this.numberFilterComparator.value; + const { value } = e.target; + this.setState(() => ({ isSelected: (value !== '') })); + // if (comparator === '') { + // return; + // } + onFilter(column, { number: value, comparator }, FILTER_TYPE.NUMBER); + } + + onChangeComparator(e) { + const { column, onFilter } = this.props; + const value = this.numberFilter.value; + const comparator = e.target.value; + // if (value === '') { + // return; + // } + onFilter(column, { number: value, comparator }, FILTER_TYPE.NUMBER); + } + + getComparatorOptions() { + const optionTags = []; + const { withoutEmptyComparatorOption } = this.props; + if (!withoutEmptyComparatorOption) { + optionTags.push( + ); + } + return optionTags; + } + + getNumberOptions() { + const optionTags = []; + const { options, column, withoutEmptyNumberOption } = this.props; + if (!withoutEmptyNumberOption) { + optionTags.push( + + ); + } + for (let i = 0; i < options.length; i += 1) { + optionTags.push(); + } + return optionTags; + } + + applyFilter(filterObj) { + const { column, onFilter } = this.props; + const { number, comparator } = filterObj; + this.setState(() => ({ isSelected: (number !== '') })); + this.numberFilterComparator.value = comparator; + this.numberFilter.value = number; + onFilter(column, { number, comparator }, FILTER_TYPE.NUMBER); + } + + cleanFiltered() { + const { column, onFilter, defaultValue } = this.props; + const value = defaultValue ? defaultValue.number : ''; + const comparator = defaultValue ? defaultValue.comparator : ''; + this.setState(() => ({ isSelected: (value !== '') })); + this.numberFilterComparator.value = comparator; + this.numberFilter.value = value; + onFilter(column, { number: value, comparator }, FILTER_TYPE.NUMBER); + } + + render() { + const { isSelected } = this.state; + const { + defaultValue, + column, + options, + style, + className, + numberStyle, + numberClassName, + comparatorStyle, + comparatorClassName, + placeholder + } = this.props; + const selectClass = ` + select-filter + number-filter-input + form-control + ${numberClassName} + ${!isSelected ? 'placeholder-selected' : ''} + `; + + return ( +
+ + { + options ? + : + this.numberFilter = n } + type="number" + style={ numberStyle } + className={ `number-filter-input form-control ${numberClassName}` } + placeholder={ placeholder || `Enter ${column.text}...` } + onChange={ this.onChangeNumber } + defaultValue={ defaultValue ? defaultValue.number : '' } + /> + } +
+ ); + } +} + +NumberFilter.propTypes = { + onFilter: PropTypes.func.isRequired, + column: PropTypes.object.isRequired, + options: PropTypes.arrayOf(PropTypes.number), + defaultValue: PropTypes.shape({ + number: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + comparator: PropTypes.oneOf([...legalComparators, '']) + }), + delay: PropTypes.number, + /* eslint consistent-return: 0 */ + comparators: (props, propName) => { + if (!props[propName]) { + return; + } + for (let i = 0; i < props[propName].length; i += 1) { + let comparatorIsValid = false; + for (let j = 0; j < legalComparators.length; j += 1) { + if (legalComparators[j] === props[propName][i] || props[propName][i] === '') { + comparatorIsValid = true; + break; + } + } + if (!comparatorIsValid) { + return new Error(`Number comparator provided is not supported. + Use only ${legalComparators}`); + } + } + }, + placeholder: PropTypes.string, + withoutEmptyComparatorOption: PropTypes.bool, + withoutEmptyNumberOption: PropTypes.bool, + style: PropTypes.object, + className: PropTypes.string, + comparatorStyle: PropTypes.object, + comparatorClassName: PropTypes.string, + numberStyle: PropTypes.object, + numberClassName: PropTypes.string +}; + +NumberFilter.defaultProps = { + delay: FILTER_DELAY, + options: undefined, + defaultValue: { + number: undefined, + comparator: '' + }, + withoutEmptyComparatorOption: false, + withoutEmptyNumberOption: false, + comparators: legalComparators, + placeholder: undefined, + style: undefined, + className: '', + comparatorStyle: undefined, + comparatorClassName: '', + numberStyle: undefined, + numberClassName: '' +}; + +export default NumberFilter; diff --git a/packages/react-bootstrap-table2-filter/src/components/select.js b/packages/react-bootstrap-table2-filter/src/components/select.js index 0374280..7e17afd 100644 --- a/packages/react-bootstrap-table2-filter/src/components/select.js +++ b/packages/react-bootstrap-table2-filter/src/components/select.js @@ -89,6 +89,7 @@ class SelectFilter extends Component { options, comparator, withoutEmptyOption, + caseSensitive, ...rest } = this.props; @@ -119,14 +120,16 @@ SelectFilter.propTypes = { style: PropTypes.object, className: PropTypes.string, withoutEmptyOption: PropTypes.bool, - defaultValue: PropTypes.any + defaultValue: PropTypes.any, + caseSensitive: PropTypes.bool }; SelectFilter.defaultProps = { defaultValue: '', className: '', withoutEmptyOption: false, - comparator: EQ + comparator: EQ, + caseSensitive: true }; export default SelectFilter; diff --git a/packages/react-bootstrap-table2-filter/src/components/text.js b/packages/react-bootstrap-table2-filter/src/components/text.js index 1c102c3..ffbe702 100644 --- a/packages/react-bootstrap-table2-filter/src/components/text.js +++ b/packages/react-bootstrap-table2-filter/src/components/text.js @@ -69,7 +69,16 @@ class TextFilter extends Component { } render() { - const { placeholder, column: { text }, style, className, onFilter, ...rest } = this.props; + const { + placeholder, + column: { text }, + style, + className, + onFilter, + caseSensitive, + defaultValue, + ...rest + } = this.props; // stopPropagation for onClick event is try to prevent sort was triggered. return ( ( data, dataField, - { filterVal, comparator = LIKE }, + { filterVal = '', comparator = LIKE, caseSensitive }, customFilterValue ) => data.filter((row) => { @@ -16,15 +18,81 @@ export const filterByText = _ => ( if (comparator === EQ) { return cellStr === filterVal; } - return cellStr.indexOf(filterVal) > -1; + if (caseSensitive) { + return cellStr.includes(filterVal); + } + return cellStr.toLocaleUpperCase().includes(filterVal.toLocaleUpperCase()); + }); + +export const filterByNumber = _ => ( + data, + dataField, + { filterVal: { comparator, number } }, + customFilterValue +) => + data.filter((row) => { + if (number === '' || !comparator) return true; + let valid = true; + let cell = _.get(row, dataField); + if (customFilterValue) { + cell = customFilterValue(cell, row); + } + + switch (comparator) { + case EQ: { + if (cell != number) { + valid = false; + } + break; + } + case GT: { + if (cell <= number) { + valid = false; + } + break; + } + case GE: { + if (cell < number) { + valid = false; + } + break; + } + case LT: { + if (cell >= number) { + valid = false; + } + break; + } + case LE: { + if (cell > number) { + valid = false; + } + break; + } + case NE: { + if (cell == number) { + valid = false; + } + break; + } + default: { + console.error('Number comparator provided is not supported'); + break; + } + } + return valid; }); export const filterFactory = _ => (filterType) => { let filterFn; switch (filterType) { case FILTER_TYPE.TEXT: + case FILTER_TYPE.SELECT: filterFn = filterByText(_); break; + case FILTER_TYPE.NUMBER: + filterFn = filterByNumber(_); + break; default: filterFn = filterByText(_); } diff --git a/packages/react-bootstrap-table2-filter/src/wrapper.js b/packages/react-bootstrap-table2-filter/src/wrapper.js index 3e6822a..f48e031 100644 --- a/packages/react-bootstrap-table2-filter/src/wrapper.js +++ b/packages/react-bootstrap-table2-filter/src/wrapper.js @@ -49,8 +49,11 @@ export default (Base, { delete currFilters[dataField]; } else { // select default comparator is EQ, others are LIKE - const { comparator = (filterType === FILTER_TYPE.SELECT ? EQ : LIKE) } = filter.props; - currFilters[dataField] = { filterVal, filterType, comparator }; + const { + comparator = (filterType === FILTER_TYPE.SELECT ? EQ : LIKE), + caseSensitive = false + } = filter.props; + currFilters[dataField] = { filterVal, filterType, comparator, caseSensitive }; } store.filters = currFilters; diff --git a/packages/react-bootstrap-table2-filter/style/react-bootstrap-table2-filter.scss b/packages/react-bootstrap-table2-filter/style/react-bootstrap-table2-filter.scss index 16aa6dd..70d4a03 100644 --- a/packages/react-bootstrap-table2-filter/style/react-bootstrap-table2-filter.scss +++ b/packages/react-bootstrap-table2-filter/style/react-bootstrap-table2-filter.scss @@ -3,7 +3,9 @@ } .react-bootstrap-table > table > thead > tr > th .select-filter option[value=''], -.react-bootstrap-table > table > thead > tr > th .select-filter.placeholder-selected { +.react-bootstrap-table > table > thead > tr > th .select-filter.placeholder-selected, +.react-bootstrap-table > table > thead > tr > th .filter::-webkit-input-placeholder, +.react-bootstrap-table > table > thead > tr > th .number-filter-input::-webkit-input-placeholder { color: lightgrey; font-style: italic; } @@ -11,4 +13,19 @@ .react-bootstrap-table > table > thead > tr > th .select-filter.placeholder-selected option:not([value='']) { color: initial; font-style: initial; +} + +.react-bootstrap-table > table > thead > tr > th .number-filter { + display: flex; +} + +.react-bootstrap-table > table > thead > tr > th .number-filter-input { + margin-left: 5px; + float: left; + width: calc(100% - 67px - 5px); +} + +.react-bootstrap-table > table > thead > tr > th .number-filter-comparator { + width: 67px; + float: left; } \ No newline at end of file diff --git a/packages/react-bootstrap-table2-filter/test/components/number.test.js b/packages/react-bootstrap-table2-filter/test/components/number.test.js new file mode 100644 index 0000000..6ba66f7 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/test/components/number.test.js @@ -0,0 +1,310 @@ +import 'jsdom-global/register'; +import React from 'react'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; +import NumberFilter from '../../src/components/number'; +import { FILTER_TYPE } from '../../src/const'; +import * as Comparator from '../../src/comparison'; + + +describe('Number Filter', () => { + let wrapper; + const onFilter = sinon.stub(); + const column = { + dataField: 'price', + text: 'Product Price' + }; + + afterEach(() => { + onFilter.reset(); + }); + + describe('initialization', () => { + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should have correct state', () => { + expect(wrapper.state().isSelected).toBeFalsy(); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(wrapper.find('select')).toHaveLength(1); + expect(wrapper.find('input[type="number"]')).toHaveLength(1); + expect(wrapper.find('.number-filter')).toHaveLength(1); + }); + + it('should rendering comparator options correctly', () => { + const select = wrapper.find('select'); + expect(select.find('option')).toHaveLength(wrapper.prop('comparators').length + 1); + }); + }); + + describe('when withoutEmptyComparatorOption prop is true', () => { + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering comparator options correctly', () => { + const select = wrapper.find('select'); + expect(select.find('option')).toHaveLength(wrapper.prop('comparators').length); + }); + }); + + describe('when defaultValue.number props is defined', () => { + const number = 203; + + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering input successfully', () => { + expect(wrapper).toHaveLength(1); + const input = wrapper.find('input[type="number"]'); + expect(input).toHaveLength(1); + expect(input.props().defaultValue).toEqual(number); + }); + }); + + describe('when defaultValue.comparator props is defined', () => { + const comparator = Comparator.EQ; + + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering comparator select successfully', () => { + expect(wrapper).toHaveLength(1); + const select = wrapper.find('.number-filter-comparator'); + expect(select).toHaveLength(1); + expect(select.props().defaultValue).toEqual(comparator); + }); + }); + + describe('when defaultValue.number and defaultValue.comparator props is defined', () => { + const number = 203; + const comparator = Comparator.EQ; + + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should have correct state', () => { + expect(wrapper.state().isSelected).toBeTruthy(); + }); + + it('should calling onFilter on componentDidMount', () => { + expect(onFilter.calledOnce).toBeTruthy(); + expect(onFilter.calledWith( + column, { number: `${number}`, comparator }, FILTER_TYPE.NUMBER)).toBeTruthy(); + }); + }); + + describe('when options props is defined', () => { + const options = [2100, 2103, 2105]; + + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering number options instead of number input', () => { + expect(wrapper).toHaveLength(1); + const select = wrapper.find('.select-filter.placeholder-selected'); + expect(select).toHaveLength(1); + expect(select.find('option')).toHaveLength(options.length + 1); + }); + + describe('when withoutEmptyNumberOption props is defined', () => { + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering number options instead of number input', () => { + const select = wrapper.find('.select-filter.placeholder-selected'); + expect(select).toHaveLength(1); + expect(select.find('option')).toHaveLength(options.length); + }); + }); + + describe('when defaultValue.number props is defined', () => { + const number = 203; + + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering number options successfully', () => { + const select = wrapper.find('.select-filter.placeholder-selected'); + expect(select).toHaveLength(1); + expect(select.props().defaultValue).toEqual(number); + }); + }); + + describe('when defaultValue.number and defaultValue.comparator props is defined', () => { + const number = options[1]; + const comparator = Comparator.EQ; + + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering number options successfully', () => { + let select = wrapper.find('.placeholder-selected'); + expect(select).toHaveLength(0); + + select = wrapper.find('.select-filter'); + expect(select).toHaveLength(1); + }); + }); + }); + + describe('when style props is defined', () => { + const style = { backgroundColor: 'red' }; + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.number-filter').prop('style')).toEqual(style); + }); + }); + + describe('when numberStyle props is defined', () => { + const numberStyle = { backgroundColor: 'red' }; + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.number-filter-input').prop('style')).toEqual(numberStyle); + }); + }); + + describe('when comparatorStyle props is defined', () => { + const comparatorStyle = { backgroundColor: 'red' }; + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(wrapper.find('select').prop('style')).toEqual(comparatorStyle); + }); + }); + + describe('when className props is defined', () => { + const className = 'test'; + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(wrapper.hasClass(className)).toBeTruthy(); + }); + }); + + describe('when numberClassName props is defined', () => { + const className = 'test'; + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(wrapper.find('.number-filter-input').prop('className').indexOf(className) > -1).toBeTruthy(); + }); + }); + + describe('when comparatorClassName props is defined', () => { + const className = 'test'; + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(wrapper.find('select').prop('className').indexOf(className) > -1).toBeTruthy(); + }); + }); +}); diff --git a/packages/react-bootstrap-table2-filter/test/filter.test.js b/packages/react-bootstrap-table2-filter/test/filter.test.js index e834f17..ae66a38 100644 --- a/packages/react-bootstrap-table2-filter/test/filter.test.js +++ b/packages/react-bootstrap-table2-filter/test/filter.test.js @@ -4,7 +4,7 @@ import Store from 'react-bootstrap-table-next/src/store'; import { filters } from '../src/filter'; import { FILTER_TYPE } from '../src/const'; -import { LIKE, EQ } from '../src/comparison'; +import { LIKE, EQ, GT, GE, LT, LE, NE } from '../src/comparison'; const data = []; for (let i = 0; i < 20; i += 1) { @@ -37,7 +37,7 @@ describe('filter', () => { }]; }); - describe('text filter', () => { + describe('filterByText', () => { beforeEach(() => { filterFn = filters(store, columns, _); }); @@ -55,6 +55,20 @@ describe('filter', () => { }); }); + describe('when caseSensitive is true', () => { + it('should returning correct result', () => { + currFilters.name = { + filterVal: 'NAME', + caseSensitive: true, + filterType: FILTER_TYPE.TEXT + }; + + const result = filterFn(currFilters); + expect(result).toBeDefined(); + expect(result).toHaveLength(0); + }); + }); + describe(`when default comparator is ${EQ}`, () => { it('should returning correct result', () => { currFilters.name = { @@ -91,4 +105,114 @@ describe('filter', () => { }); }); }); + + describe('filterByNumber', () => { + beforeEach(() => { + filterFn = filters(store, columns, _); + }); + + describe('when currFilters.filterVal.comparator is empty', () => { + it('should returning correct result', () => { + currFilters.price = { + filterVal: { comparator: '', number: '203' }, + filterType: FILTER_TYPE.NUMBER + }; + + let result = filterFn(currFilters); + expect(result).toHaveLength(data.length); + + currFilters.price.filterVal.comparator = undefined; + result = filterFn(currFilters); + expect(result).toHaveLength(data.length); + }); + }); + + describe('when currFilters.filterVal.number is empty', () => { + it('should returning correct result', () => { + currFilters.price = { + filterVal: { comparator: EQ, number: '' }, + filterType: FILTER_TYPE.NUMBER + }; + + const result = filterFn(currFilters); + expect(result).toHaveLength(data.length); + }); + }); + + describe(`when currFilters.filterVal.comparator is ${EQ}`, () => { + it('should returning correct result', () => { + currFilters.price = { + filterVal: { comparator: EQ, number: '203' }, + filterType: FILTER_TYPE.NUMBER + }; + + let result = filterFn(currFilters); + expect(result).toHaveLength(1); + + currFilters.price.filterVal.number = '0'; + result = filterFn(currFilters); + expect(result).toHaveLength(0); + }); + }); + + describe(`when currFilters.filterVal.comparator is ${GT}`, () => { + it('should returning correct result', () => { + currFilters.price = { + filterVal: { comparator: GT, number: '203' }, + filterType: FILTER_TYPE.NUMBER + }; + + const result = filterFn(currFilters); + expect(result).toHaveLength(16); + }); + }); + + describe(`when currFilters.filterVal.comparator is ${GE}`, () => { + it('should returning correct result', () => { + currFilters.price = { + filterVal: { comparator: GE, number: '203' }, + filterType: FILTER_TYPE.NUMBER + }; + + const result = filterFn(currFilters); + expect(result).toHaveLength(17); + }); + }); + + describe(`when currFilters.filterVal.comparator is ${LT}`, () => { + it('should returning correct result', () => { + currFilters.price = { + filterVal: { comparator: LT, number: '203' }, + filterType: FILTER_TYPE.NUMBER + }; + + const result = filterFn(currFilters); + expect(result).toHaveLength(3); + }); + }); + + describe(`when currFilters.filterVal.comparator is ${LE}`, () => { + it('should returning correct result', () => { + currFilters.price = { + filterVal: { comparator: LE, number: '203' }, + filterType: FILTER_TYPE.NUMBER + }; + + const result = filterFn(currFilters); + expect(result).toHaveLength(4); + }); + }); + + describe(`when currFilters.filterVal.comparator is ${NE}`, () => { + it('should returning correct result', () => { + currFilters.price = { + filterVal: { comparator: NE, number: '203' }, + filterType: FILTER_TYPE.NUMBER + }; + + const result = filterFn(currFilters); + expect(result).toHaveLength(19); + }); + }); + }); }); diff --git a/packages/react-bootstrap-table2/src/header-cell.js b/packages/react-bootstrap-table2/src/header-cell.js index d5dd47b..b6af21e 100644 --- a/packages/react-bootstrap-table2/src/header-cell.js +++ b/packages/react-bootstrap-table2/src/header-cell.js @@ -131,6 +131,7 @@ HeaderCell.propTypes = { attrs: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), sort: PropTypes.bool, sortFunc: PropTypes.func, + onSort: PropTypes.func, editable: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), editCellStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), editCellClasses: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), diff --git a/packages/react-bootstrap-table2/src/row.js b/packages/react-bootstrap-table2/src/row.js index fa723af..1b55628 100644 --- a/packages/react-bootstrap-table2/src/row.js +++ b/packages/react-bootstrap-table2/src/row.js @@ -13,6 +13,7 @@ class Row extends Component { super(props); this.clickNum = 0; this.handleRowClick = this.handleRowClick.bind(this); + this.handleSimpleRowClick = this.handleSimpleRowClick.bind(this); } handleRowClick(e) { @@ -36,7 +37,7 @@ class Row extends Component { const clickFn = () => { if (attrs.onClick) { - attrs.onClick(e); + attrs.onClick(e, row, rowIndex); } if (selectable) { const key = _.get(row, keyField); @@ -57,6 +58,16 @@ class Row extends Component { } } + handleSimpleRowClick(e) { + const { + row, + rowIndex, + attrs + } = this.props; + + attrs.onClick(e, row, rowIndex); + } + render() { const { row, @@ -90,6 +101,8 @@ class Row extends Component { const trAttrs = { ...attrs }; if (clickToSelect) { trAttrs.onClick = this.handleRowClick; + } else if (attrs.onClick) { + trAttrs.onClick = this.handleSimpleRowClick; } return ( diff --git a/packages/react-bootstrap-table2/src/sort/wrapper.js b/packages/react-bootstrap-table2/src/sort/wrapper.js index 45414e3..90a9bb9 100644 --- a/packages/react-bootstrap-table2/src/sort/wrapper.js +++ b/packages/react-bootstrap-table2/src/sort/wrapper.js @@ -25,6 +25,10 @@ export default Base => if (column.length > 0) { store.setSort(column[0], order); + if (column[0].onSort) { + column[0].onSort(store.sortField, store.sortOrder); + } + if (this.isRemoteSort() || this.isRemotePagination()) { this.handleSortChange(); } else { @@ -48,6 +52,10 @@ export default Base => const { store } = this.props; store.setSort(column); + if (column.onSort) { + column.onSort(store.sortField, store.sortOrder); + } + if (this.isRemoteSort() || this.isRemotePagination()) { this.handleSortChange(); } else { diff --git a/packages/react-bootstrap-table2/test/sort/wrapper.test.js b/packages/react-bootstrap-table2/test/sort/wrapper.test.js index d8a3ce7..1c28b8c 100644 --- a/packages/react-bootstrap-table2/test/sort/wrapper.test.js +++ b/packages/react-bootstrap-table2/test/sort/wrapper.test.js @@ -10,16 +10,7 @@ import wrapperFactory from '../../src/sort/wrapper'; describe('SortWrapper', () => { let wrapper; - - const columns = [{ - dataField: 'id', - text: 'ID', - sort: true - }, { - dataField: 'name', - text: 'Name', - sort: true - }]; + let columns; const data = [{ id: 1, @@ -37,6 +28,15 @@ describe('SortWrapper', () => { const SortWrapper = wrapperFactory(BootstrapTable); beforeEach(() => { + columns = [{ + dataField: 'id', + text: 'ID', + sort: true + }, { + dataField: 'name', + text: 'Name', + sort: true + }]; wrapper = shallow( { describe('call handleSort function', () => { let sortBySpy; - const sortColumn = columns[0]; + let sortColumn; beforeEach(() => { + sortColumn = columns[0]; store = new Store(keyField); store.data = data; sortBySpy = sinon.spy(store, 'sortBy'); @@ -130,6 +131,32 @@ describe('SortWrapper', () => { expect(onTableChangeCB.calledOnce).toBeTruthy(); }); }); + + describe('when column.onSort prop is defined', () => { + const onSortCB = jest.fn(); + + beforeEach(() => { + columns[0].onSort = onSortCB; + wrapper = shallow( + + ); + wrapper.instance().handleSort(sortColumn); + }); + + it('should calling column.onSort function correctly', () => { + expect(onSortCB).toHaveBeenCalledTimes(1); + expect(onSortCB).toHaveBeenCalledWith(columns[0].dataField, Const.SORT_DESC); + + wrapper.instance().handleSort(sortColumn); + expect(onSortCB).toHaveBeenCalledTimes(2); + expect(onSortCB).toHaveBeenCalledWith(columns[0].dataField, Const.SORT_ASC); + }); + }); }); describe('when defaultSorted prop is defined', () => { @@ -161,6 +188,28 @@ describe('SortWrapper', () => { it('should update store.sortOrder correctly', () => { expect(store.sortOrder).toEqual(defaultSorted[0].order); }); + + describe('when column.onSort prop is defined', () => { + const onSortCB = jest.fn(); + + beforeEach(() => { + columns[1].onSort = onSortCB; + wrapper = shallow( + + ); + }); + + it('should calling column.onSort function correctly', () => { + expect(onSortCB).toHaveBeenCalledTimes(1); + expect(onSortCB).toHaveBeenCalledWith(defaultSorted[0].dataField, defaultSorted[0].order); + }); + }); }); describe('componentWillReceiveProps', () => {