diff --git a/README.md b/README.md index 1b07596..a28599c 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ The component accepts the following props: |**`expandableRowsOnClick`**|boolean|false|Enable/disable expand trigger when row is clicked. When False, only expand icon will trigger this action. |**`filter`**|boolean|true|Show/hide filter icon from toolbar. |**`filterType `**|string||Choice of filtering view. `enum('checkbox', 'dropdown', 'multiselect', 'textField')` +|**`filterPopoverOptions`**|object|`{mustConfirm: false, confirmButtonLabel: 'Submit'}`|Options to change the filter popover. Can be useful for serverSide filtering where you want to confirm the filters before applying them. Options: `mustConfirm`: boolean, `confirmButtonLabel`: string |**`fixedHeader`**|boolean|true|Enable/disable fixed header columns. |**`isRowExpandable`**|function||Enable/disable expansion or collapse on certain expandable rows with custom function. Will be considered true if not provided. `function(dataIndex: number, expandedRows: object(lookup: {dataIndex: number}, data: arrayOfObjects: {index: number, dataIndex: number})) => bool`. |**`isRowSelectable`**|function||Enable/disable selection on certain rows with custom function. Returns true if not provided. `function(dataIndex: number, selectedRows: object(lookup: {[dataIndex]: boolean}, data: arrayOfObjects: {index: number, dataIndex: number})) => boolean`. diff --git a/examples/customize-filter/index.js b/examples/customize-filter/index.js index bbc2b9f..59e8f54 100644 --- a/examples/customize-filter/index.js +++ b/examples/customize-filter/index.js @@ -143,6 +143,9 @@ class Example extends React.Component { checkboxColor: 'secondary', filter: true, filterType: 'dropdown', + filterPopoverOptions: { + mustConfirm: true, + }, responsive: 'scroll', onFilterChange: (name, filterList, index) => { console.log('onFilterChange callback'); diff --git a/src/MUIDataTable.js b/src/MUIDataTable.js index 5028578..cd89ae0 100644 --- a/src/MUIDataTable.js +++ b/src/MUIDataTable.js @@ -144,6 +144,10 @@ class MUIDataTable extends React.Component { expandableRowsOnClick: PropTypes.bool, filter: PropTypes.bool, filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField', 'custom']), + filterPopoverOptions: PropTypes.shape({ + mustConfirm: PropTypes.bool, + confirmButtonLabel: PropTypes.string, + }), fixedHeader: PropTypes.bool, isRowExpandable: PropTypes.func, isRowSelectable: PropTypes.func, @@ -264,7 +268,7 @@ class MUIDataTable extends React.Component { updateOptions(options, props) { this.options = assignwith(options, props.options, (objValue, srcValue, key) => { // Merge any default options that are objects, as they will be overwritten otherwise - if (key === 'textLabels' || key === 'downloadOptions') return merge(objValue, srcValue); + if (key === 'textLabels' || key === 'downloadOptions' || key === 'filterPopoverOptions') return merge(objValue, srcValue); return; }); @@ -293,6 +297,10 @@ class MUIDataTable extends React.Component { expandableRowsOnClick: false, filter: true, filterType: 'dropdown', + filterPopoverOptions: { + mustConfirm: false, + confirmButtonLabel: 'Submit', + }, fixedHeader: true, resizableColumns: false, responsive: 'stacked', @@ -976,27 +984,32 @@ class MUIDataTable extends React.Component { ); }; + // filter = + updateFilterByType = (filterList, index, value, column, type) => { + const filterPos = filterList[index].indexOf(value); + + switch (type) { + case 'checkbox': + filterPos >= 0 ? filterList[index].splice(filterPos, 1) : filterList[index].push(value); + break; + case 'multiselect': + case 'dropdown': + case 'custom': + if (!Array.isArray(value)) { + console.warn('filterUpdate: Invalid value for filter.'); + } + filterList[index] = value; + break; + default: + filterList[index] = filterPos >= 0 || value === '' ? [] : [value]; + } + } + filterUpdate = (index, value, column, type) => { this.setState( prevState => { const filterList = prevState.filterList.slice(0); - const filterPos = filterList[index].indexOf(value); - - switch (type) { - case 'checkbox': - filterPos >= 0 ? filterList[index].splice(filterPos, 1) : filterList[index].push(value); - break; - case 'multiselect': - case 'dropdown': - case 'custom': - if (!Array.isArray(value)) { - console.warn('filterUpdate: Invalid value for filter.'); - } - filterList[index] = value; - break; - default: - filterList[index] = filterPos >= 0 || value === '' ? [] : [value]; - } + this.updateFilterByType(filterList, index, value, column, type); return { page: 0, @@ -1328,15 +1341,17 @@ class MUIDataTable extends React.Component { data={data} filterData={filterData} filterList={filterList} + filterPopoverOptions={this.options.filterPopoverOptions} filterUpdate={this.filterUpdate} options={this.options} resetFilters={this.resetFilters} searchText={searchText} searchTextUpdate={this.searchTextUpdate} + setTableAction={this.setTableAction} tableRef={this.getTableContentRef} title={title} - toggleViewColumn={this.toggleViewColumn} - setTableAction={this.setTableAction} /> + toggleViewColumn={this.toggleViewColumn} + updateFilterByType={this.updateFilterByType} /> ) )} ({ +export const useStyles = makeStyles(theme => ({ root: { backgroundColor: theme.palette.background.default, padding: '24px 24px 36px 24px', @@ -39,6 +40,11 @@ export const defaultFilterStyles = theme => ({ noMargin: { marginLeft: '0px', }, + confirmButton: { + marginLeft: '24px', + fontSize: '12px', + cursor: 'pointer', + }, reset: { alignSelf: 'left', }, @@ -77,48 +83,64 @@ export const defaultFilterStyles = theme => ({ gridListTile: { marginTop: '16px', }, -}); +}), { name: 'MUIDataTableFilter' }); + +function TableFilter(props) { -class TableFilter extends React.Component { - static propTypes = { - /** Data used to populate filter dropdown/checkbox */ - filterData: PropTypes.array.isRequired, - /** Data selected to be filtered against dropdown/checkbox */ - filterList: PropTypes.array.isRequired, - /** Options used to describe table */ - options: PropTypes.object.isRequired, - /** Callback to trigger filter update */ - onFilterUpdate: PropTypes.func, - /** Callback to trigger filter reset */ - onFilterRest: PropTypes.func, - /** Extend the style applied to components */ - classes: PropTypes.object, + const classes = useStyles(); + const [filterList, setFilterList] = useState( cloneDeep(props.filterList) ); + + const filterUpdate = (index, value, column, type) => { + let newFilterList = filterList.slice(0); + props.updateFilterByType(newFilterList, index, value, column, type); + setFilterList(newFilterList); }; - handleCheckboxChange = (index, value, column) => { - this.props.onFilterUpdate(index, value, column, 'checkbox'); + const handleCheckboxChange = (index, value, column) => { + filterUpdate(index, value, column, 'checkbox'); + + if (props.filterPopoverOptions.mustConfirm === false) { + props.onFilterUpdate(index, value, column, 'checkbox'); + } }; - handleDropdownChange = (event, index, column) => { - const labelFilterAll = this.props.options.textLabels.filter.all; + const handleDropdownChange = (event, index, column) => { + const labelFilterAll = props.options.textLabels.filter.all; const value = event.target.value === labelFilterAll ? [] : [event.target.value]; - this.props.onFilterUpdate(index, value, column, 'dropdown'); + + filterUpdate(index, value, column, 'dropdown'); + + if (props.filterPopoverOptions.mustConfirm === false) { + props.onFilterUpdate(index, value, column, 'dropdown'); + } }; - handleMultiselectChange = (index, value, column) => { - this.props.onFilterUpdate(index, value, column, 'multiselect'); + const handleMultiselectChange = (index, value, column) => { + filterUpdate(index, value, column, 'multiselect'); + + if (props.filterPopoverOptions.mustConfirm === false) { + props.onFilterUpdate(index, value, column, 'multiselect'); + } }; - handleTextFieldChange = (event, index, column) => { - this.props.onFilterUpdate(index, event.target.value, column, 'textField'); + const handleTextFieldChange = (event, index, column) => { + filterUpdate(index, event.target.value, column, 'textField'); + + if (props.filterPopoverOptions.mustConfirm === false) { + props.onFilterUpdate(index, event.target.value, column, 'textField'); + } }; - handleCustomChange = (value, index, column) => { - this.props.onFilterUpdate(index, value, column.name, column.filterType); + const handleCustomChange = (value, index, column) => { + filterUpdate(index, value, column.name, column.filterType); + + if (props.filterPopoverOptions.mustConfirm === false) { + props.onFilterUpdate(index, value, column.name, column.filterType); + } }; - renderCheckbox(column, index) { - const { classes, filterData, filterList, options } = this.props; + const renderCheckbox = (column, index) => { + const { filterData, options } = props; return ( @@ -140,7 +162,7 @@ class TableFilter extends React.Component { control={ handleCheckboxChange(index, filterValue, column.name)} checked={filterList[index].indexOf(filterValue) >= 0 ? true : false} color={options.checkboxColor} value={filterValue != null ? filterValue.toString() : ''} @@ -154,10 +176,10 @@ class TableFilter extends React.Component { ); - } + }; - renderSelect(column, index) { - const { classes, filterData, filterList, options } = this.props; + const renderSelect = (column, index) => { + const { filterData, options } = props; const textLabels = options.textLabels.filter; return ( @@ -168,7 +190,7 @@ class TableFilter extends React.Component { fullWidth value={filterList[index].length ? filterList[index].toString() : textLabels.all} name={column.name} - onChange={event => this.handleDropdownChange(event, index, column.name)} + onChange={event => handleDropdownChange(event, index, column.name)} input={}> {textLabels.all} @@ -182,11 +204,9 @@ class TableFilter extends React.Component { ); - } - - renderTextField(column, index) { - const { classes, filterList } = this.props; + }; + const renderTextField = (column, index) => { return ( @@ -194,15 +214,15 @@ class TableFilter extends React.Component { fullWidth label={column.label} value={filterList[index].toString() || ''} - onChange={event => this.handleTextFieldChange(event, index, column.name)} + onChange={event => handleTextFieldChange(event, index, column.name)} /> ); - } + }; - renderMultiselect(column, index) { - const { classes, filterData, filterList, options } = this.props; + const renderMultiselect = (column, index) => { + const { filterData, options } = props; return ( @@ -214,7 +234,7 @@ class TableFilter extends React.Component { value={filterList[index] || []} renderValue={selected => selected.join(', ')} name={column.name} - onChange={event => this.handleMultiselectChange(index, event.target.value, column.name)} + onChange={event => handleMultiselectChange(index, event.target.value, column.name)} input={}> {filterData[index].map((filterValue, filterIndex) => ( @@ -231,10 +251,10 @@ class TableFilter extends React.Component { ); - } + }; - renderCustomField(column, index) { - const { classes, filterList, options } = this.props; + const renderCustomField = (column, index) => { + const { options } = props; const display = (column.filterOptions && column.filterOptions.display) || (options.filterOptions && options.filterOptions.display); @@ -247,59 +267,94 @@ class TableFilter extends React.Component { return ( - {display(filterList, this.handleCustomChange, index, column)} + {display(filterList, handleCustomChange, index, column)} ); - } + }; - render() { - const { classes, columns, options, onFilterReset } = this.props; - const textLabels = options.textLabels.filter; - const filterGridColumns = columns.filter(col => col.filter).length === 1 ? 1 : 2; + const applyFilters = () => { + filterList.forEach( (filter, index) => { + props.onFilterUpdate(index, filter, columns[index].name, 'custom'); + }); + props.handleClose(); + }; - return ( -
-
-
- - {textLabels.title} - + const { columns, options, onFilterReset } = props; + const textLabels = options.textLabels.filter; + const filterGridColumns = columns.filter(col => col.filter).length === 1 ? 1 : 2; + + console.log('TableFilter render'); + console.dir(props); + + return ( +
+
+
+ + {textLabels.title} + + {props.filterPopoverOptions.mustConfirm && -
-
+ } +
- - {columns.map((column, index) => { - if (column.filter) { - const filterType = column.filterType || options.filterType; - return filterType === 'checkbox' - ? this.renderCheckbox(column, index) - : filterType === 'multiselect' - ? this.renderMultiselect(column, index) - : filterType === 'textField' - ? this.renderTextField(column, index) - : filterType === 'custom' - ? this.renderCustomField(column, index) - : this.renderSelect(column, index); - } - })} - +
- ); - } + + {columns.map((column, index) => { + if (column.filter) { + const filterType = column.filterType || options.filterType; + return filterType === 'checkbox' + ? renderCheckbox(column, index) + : filterType === 'multiselect' + ? renderMultiselect(column, index) + : filterType === 'textField' + ? renderTextField(column, index) + : filterType === 'custom' + ? renderCustomField(column, index) + : renderSelect(column, index); + } + })} + +
+ ); } -export default withStyles(defaultFilterStyles, { name: 'MUIDataTableFilter' })(TableFilter); +TableFilter.propTypes = { + /** Data used to populate filter dropdown/checkbox */ + filterData: PropTypes.array.isRequired, + /** Data selected to be filtered against dropdown/checkbox */ + filterList: PropTypes.array.isRequired, + /** Options for the filter popover */ + filterPopoverOptions: PropTypes.object.isRequired, + /** Callback to trigger filter reset */ + onFilterRest: PropTypes.func, + /** Callback to trigger filter update */ + onFilterUpdate: PropTypes.func, + /** Options used to describe table */ + options: PropTypes.object.isRequired, +}; + +export default TableFilter; diff --git a/src/components/TableToolbar.js b/src/components/TableToolbar.js index bc1e87e..bb55a8a 100644 --- a/src/components/TableToolbar.js +++ b/src/components/TableToolbar.js @@ -82,6 +82,8 @@ class TableToolbar extends React.Component { iconActive: null, showSearch: Boolean(this.props.searchText || this.props.options.searchText || this.props.options.showSearch), searchText: this.props.searchText || null, + filterPopoverKey: 0, + mustCloseFilterPopover: false, }; componentDidUpdate(prevProps) { @@ -129,6 +131,7 @@ class TableToolbar extends React.Component { this.setState(() => ({ showSearch: this.isSearchShown(iconName), iconActive: iconName, + filterPopoverKey: this.state.filterPopoverKey + 1 })); }; @@ -186,17 +189,28 @@ class TableToolbar extends React.Component { data, filterData, filterList, + filterPopoverOptions, filterUpdate, options, resetFilters, tableRef, title, toggleViewColumn, + updateFilterByType, } = this.props; const { search, downloadCsv, print, viewColumns, filterTable } = options.textLabels.toolbar; const { showSearch, searchText } = this.state; + const filterPopoverExit = () => { + this.setState({hideFilterPopover: false}); + this.setActiveIcon.bind(null); + }; + + const closeFilterPopover = () => { + this.setState({hideFilterPopover: true}); + }; + return (
@@ -285,7 +299,8 @@ class TableToolbar extends React.Component { )} {options.filter && ( @@ -300,12 +315,16 @@ class TableToolbar extends React.Component { } content={ } />