├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── demo ├── public │ ├── index.css │ └── index.html └── src │ ├── App.js │ ├── data.json │ ├── data3.json │ ├── index.js │ └── reducers.js ├── nwb.config.js ├── package-lock.json ├── package.json └── src ├── components ├── FilterDrawer.js ├── OperatorField.js ├── SearchField.js └── index.js ├── index.js └── store ├── actions.js ├── reducer.js ├── reducer.spec.js ├── selectors.js └── types.js /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | 9 | before_install: 10 | - npm install codecov.io coveralls 11 | 12 | script: 13 | - npm test 14 | 15 | after_success: 16 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 17 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 18 | - npm run build 19 | 20 | deploy: 21 | - provider: pages 22 | skip_cleanup: true 23 | github_token: $GITHUB_TOKEN 24 | local_dir: demo/dist 25 | on: 26 | branch: master 27 | condition: $TRAVIS_PULL_REQUEST = false 28 | 29 | - provider: npm 30 | skip_cleanup: true 31 | email: $NPM_EMAIL 32 | api_key: $NPM_TOKEN 33 | on: 34 | branch: master 35 | condition: $TRAVIS_PULL_REQUEST = false 36 | 37 | branches: 38 | only: 39 | - master 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= v4 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the component's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # material-ui-filter 2 | [![Build Status][travis-image]][travis-url] 3 | [![Dependency Status][daviddm-image]][daviddm-url] 4 | [![License][license-image]][license-url] 5 | [![Code Coverage][coverage-image]][coverage-url] 6 | [![Code Style][code-style-image]][code-style-url] 7 | 8 | This project was bootstrapped with [nwb](https://github.com/insin/nwb) 9 | 10 | Material UI filter is a filter drawer that lets you filter any Array. 11 | You can sort the array and add as many filters as you please. 12 | 13 | Just try out the [DEMO](https://tarikhuber.github.io/material-ui-filter/). 14 | You can find the full code of the demo in the 'demo' folder above. 15 | 16 | (Demo data generated with: http://www.json-generator.com/). 17 | 18 | ## Table of Contents 19 | 20 | - [Features](#features) 21 | - [Implementation](#implementation) 22 | - [Contributors](#contributors) 23 | - [License](#license) 24 | 25 | ## Features 26 | 27 | Material UI filter allows you to filter and sort arrays. The filter currently supports 28 | - strings 29 | - dates 30 | - booleans 31 | 32 | ## Implementation 33 | 34 | We will use code snippets from the demo project to explain how to implement the filter: 35 | 36 | The first step is to install the filter: 37 | 38 | ``` 39 | npm install material-ui-filter 40 | ``` 41 | 42 | Then you have to import the filter: 43 | ```js 44 | import { FilterDrawer, filterSelectors, filterActions } from 'material-ui-filter' 45 | ``` 46 | 47 | After that you have to add the filter component to the rest of your components in the render function. 48 | 49 | The filter takes a few props: 50 | - name: Name of the filter 51 | - fields: An array of the properties you want to filter/ sort by. 52 | The array should consist of objects with at least a name property. 53 | Additionally you can add a label and a datatype for each property. 54 | The standard datatype is string. Other possible datatypes are bool and date. 55 | - locale, DateTimeFormat, okLabel, cancelLabel: Will be forwarded to the DatePicker. 56 | 57 | ```js 58 | const filterFields = [ 59 | { name: 'name', label: 'Name' }, 60 | { name: 'email', label: 'Email' }, 61 | { name: 'registered', label: 'Registered', type: 'date' }, 62 | { name: 'isActive', label: 'Is Active', type: 'bool' }, 63 | ]; 64 | ``` 65 | 66 | ```js 67 | 77 | ``` 78 | 79 | 80 | In your mapStateToProps function you have to set the filter props and filter the array. 81 | 82 | The getFilteredList function takes the following parameters: 83 | - filter name 84 | - filters 85 | - array 86 | - A function to get the array values (eg. If your array value is in an Object.) 87 | 88 | ```js 89 | const { hasFilters } = filterSelectors.selectFilterProps('demo', filters); 90 | const list = filterSelectors.getFilteredList('demo', filters, source /*, fieldValue => fieldValue.val*/); 91 | ``` 92 | 93 | 94 | And last but not least you have to add the reducer to your combineReducers function to make it work with your store. 95 | 96 | ```js 97 | import { filterReducer } from '../../src' 98 | 99 | const reducers = combineReducers({ 100 | //your other reducers 101 | filters: filterReducer 102 | }) 103 | 104 | export default reducers 105 | ``` 106 | 107 | For more information feel free to play around with the [DEMO](https://tarikhuber.github.io/material-ui-filter/). 108 | 109 | 110 | ## Contributors 111 | 112 | Tarik Huber (https://github.com/TarikHuber) 113 | 114 | Maximilian Pichler (https://github.com/MaximilianPichler) 115 | 116 | ## License 117 | 118 | MIT @TarikHuber & @MaximilianPichler 119 | 120 | [travis-image]: https://travis-ci.org/TarikHuber/material-ui-filter.svg?branch=master 121 | [travis-url]: https://travis-ci.org/TarikHuber/material-ui-filter 122 | [daviddm-image]: https://img.shields.io/david/TarikHuber/material-ui-filter.svg?style=flat-square 123 | [daviddm-url]: https://david-dm.org/TarikHuber/material-ui-filter 124 | [coverage-image]: https://img.shields.io/codecov/c/github/TarikHuber/material-ui-filter.svg?style=flat-square 125 | [coverage-url]: https://codecov.io/gh/TarikHuber/material-ui-filter 126 | [license-image]: https://img.shields.io/npm/l/express.svg 127 | [license-url]: https://github.com/TarikHuber/material-ui-filter/master/LICENSE 128 | [code-style-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 129 | [code-style-url]: http://standardjs.com/ 130 | -------------------------------------------------------------------------------- /demo/public/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: 'Roboto', sans-serif; 5 | height: 100% 6 | } 7 | 8 | html, body, #root{ 9 | height: 100% 10 | } 11 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/src/App.js: -------------------------------------------------------------------------------- 1 | import AppBar from '@material-ui/core/AppBar' 2 | import Button from '@material-ui/core/Button' 3 | import Divider from '@material-ui/core/Divider' 4 | import FilterList from '@material-ui/icons/FilterList' 5 | import IconButton from '@material-ui/core/IconButton' 6 | import List from '@material-ui/core/List' 7 | import ListItem from '@material-ui/core/ListItem' 8 | import MenuIcon from '@material-ui/icons/Menu' 9 | import PropTypes from 'prop-types' 10 | import React, { Component } from 'react' 11 | import ReactList from 'react-list' 12 | import Search from '@material-ui/icons/Search' 13 | import TextField from '@material-ui/core/TextField' 14 | import Toolbar from '@material-ui/core/Toolbar' 15 | import Typography from '@material-ui/core/Typography' 16 | import source from '../src/data.json' 17 | import { FilterDrawer, filterSelectors, filterActions } from '../../src' 18 | import { connect } from 'react-redux' 19 | import { withStyles } from '@material-ui/core/styles' 20 | 21 | const styles = theme => ({ 22 | root: { 23 | flexGrow: 1, 24 | }, 25 | flex: { 26 | flex: 1, 27 | }, 28 | menuButton: { 29 | marginLeft: -12, 30 | marginRight: 20, 31 | }, 32 | appBar: { 33 | zIndex: theme.zIndex.drawer + 1, 34 | }, 35 | typography: { 36 | useNextVariants: true, 37 | }, 38 | }) 39 | 40 | class App extends Component { 41 | renderItem = (i, k) => { 42 | const { list } = this.props 43 | const key = i 44 | const val = list[i] 45 | 46 | return ( 47 |
48 | 49 |
50 |
{val.name}
51 |
{val.email}
52 |
{val.registered}
53 |
{val.isActive ? 'Active' : ''}
54 |
55 |
56 | 57 |
58 | ) 59 | } 60 | 61 | render() { 62 | const { setFilterIsOpen, list, setSearch, muiTheme, classes } = this.props 63 | 64 | const filterFields = [ 65 | { name: 'name', label: 'Name' }, 66 | { name: 'email', label: 'Email' }, 67 | { name: 'registered', label: 'Registered', type: 'date' }, 68 | { name: 'isActive', label: 'Is Active', type: 'bool' }, 69 | { name: 'testObject', label: 'Object', type: 'object' }, 70 | { name: 'amount', label: 'Amount', type: 'number' }, 71 | ] 72 | 73 | return ( 74 |
75 | 76 | 77 | 78 | {`material-ui-filter (${source.length} entries)`} 79 | 80 |
81 |
82 |
91 |
99 | 102 | { 105 | setSearch('demo', e.target.value) 106 | }} 107 | /> 108 |
109 |
110 |
111 |
112 | setFilterIsOpen('demo', true)} 115 | > 116 | 117 | 118 |
119 |
120 | 121 | 122 | 127 | 128 | 129 | 139 |
140 | ) 141 | } 142 | } 143 | 144 | App.propTypes = { 145 | setFilterIsOpen: PropTypes.func.isRequired, 146 | } 147 | 148 | const mapStateToProps = state => { 149 | const { filters, muiTheme } = state 150 | const { hasFilters } = filterSelectors.selectFilterProps('demo', filters) 151 | const list = filterSelectors.getFilteredList( 152 | 'demo', 153 | filters, 154 | source /*, fieldValue => fieldValue.val*/ 155 | ) 156 | 157 | return { 158 | hasFilters, 159 | list, 160 | } 161 | } 162 | 163 | export default connect(mapStateToProps, { ...filterActions })( 164 | withStyles(styles, { withTheme: true })(App) 165 | ) 166 | -------------------------------------------------------------------------------- /demo/src/data3.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Malone Austin", 4 | "email": "maloneaustin@apextri.com", 5 | "registered": "2001-10-01", 6 | "isActive": false, 7 | "testObject": { "prop1": "123456", "prop2": "987654321" } 8 | }, 9 | { 10 | "name": "Angelia Mitchell", 11 | "email": "angeliamitchell@apextri.com", 12 | "registered": "1994-10-17", 13 | "isActive": false, 14 | "testObject": { "prop1": "2222222", "prop2": "3333333" } 15 | }, 16 | { 17 | "name": "Angelia Mitchell", 18 | "email": "angeliamitchell@apextri.com", 19 | "registered": "1994-10-17", 20 | "isActive": false 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import App from './App.js' 2 | import Utils from '@date-io/moment' 3 | import React from 'react' 4 | import moment from 'moment' 5 | import reducers from './reducers' 6 | import { IntlProvider } from 'react-intl' 7 | import { MuiPickersUtilsProvider } from '@material-ui/pickers' 8 | import { Provider } from 'react-redux' 9 | import { createLogger } from 'redux-logger' 10 | import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles' 11 | import { createStore, compose, applyMiddleware } from 'redux' 12 | import { render } from 'react-dom' 13 | 14 | const logger = createLogger({}) 15 | const muiTheme = createMuiTheme({ 16 | typography: { 17 | useNextVariants: true 18 | } 19 | }) 20 | const store = createStore(reducers, {}, compose(applyMiddleware(logger))) 21 | 22 | render( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | , 32 | document.querySelector('#demo') 33 | ) 34 | -------------------------------------------------------------------------------- /demo/src/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { filterReducer } from '../../src' 3 | 4 | const reducers = combineReducers({ 5 | filters: filterReducer 6 | }) 7 | 8 | export default reducers 9 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'react-component', 3 | npm: { 4 | esModules: true, 5 | umd: { 6 | global: 'material-ui-filter', 7 | externals: { 8 | react: 'React' 9 | } 10 | } 11 | }, 12 | webpack: { 13 | html: { 14 | template: 'demo/public/index.html' 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-ui-filter", 3 | "version": "3.1.3", 4 | "description": "Material UI Drawer for filtering local arrays", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "css", 9 | "es", 10 | "lib", 11 | "umd" 12 | ], 13 | "scripts": { 14 | "build": "nwb build-react-component", 15 | "clean": "nwb clean-module && nwb clean-demo", 16 | "start": "nwb serve-react-demo", 17 | "test": "nwb test-react", 18 | "test:coverage": "nwb test-react --coverage", 19 | "test:watch": "nwb test-react --server", 20 | "gpublish": "nwb build-demo && gh-pages -d demo/dist" 21 | }, 22 | "dependencies": {}, 23 | "peerDependencies": { 24 | "@material-ui/core": "4.x", 25 | "@material-ui/icons": "4.x", 26 | "@material-ui/pickers": "3.x", 27 | "@date-io/moment": "1.x", 28 | "downshift": "3.x", 29 | "match-sorter": "4.x", 30 | "moment": "2.x", 31 | "react": "16.x", 32 | "react-dom": "16.x", 33 | "react-list": "0.x", 34 | "react-virtualized": "9.x", 35 | "muishift": "1.x", 36 | "redux": "4.x" 37 | }, 38 | "devDependencies": { 39 | "@date-io/moment": "^1.3.11", 40 | "@material-ui/core": "^4.7.0", 41 | "@material-ui/icons": "^4.5.1", 42 | "@material-ui/pickers": "^3.2.8", 43 | "downshift": "^3.4.3", 44 | "gh-pages": "^2.1.1", 45 | "match-sorter": "^4.0.2", 46 | "moment": "^2.22.2", 47 | "muishift": "^1.1.3", 48 | "nwb": "^0.23.0", 49 | "prop-types": "^15.6.2", 50 | "react": "^16.12.0", 51 | "react-dom": "^16.12.0", 52 | "react-intl": "^3.7.0", 53 | "react-list": "^0.8.13", 54 | "react-redux": "^7.1.3", 55 | "react-virtualized": "^9.21.2", 56 | "redux": "^4.0.4", 57 | "redux-logger": "^3.0.6" 58 | }, 59 | "prettier": { 60 | "trailingComma": "es5", 61 | "tabWidth": 2, 62 | "semi": false, 63 | "singleQuote": true 64 | }, 65 | "author": "Tarik Huber", 66 | "homepage": "https://tarikhuber.github.io/material-ui-filter/", 67 | "license": "MIT", 68 | "repository": "https://github.com/TarikHuber/material-ui-filter", 69 | "keywords": [ 70 | "react-component", 71 | "material-ui", 72 | "filter", 73 | "react", 74 | "redux" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /src/components/FilterDrawer.js: -------------------------------------------------------------------------------- 1 | import * as filterActions from "../store/actions"; 2 | import * as filterSelectors from "../store/selectors"; 3 | import AddCircle from "@material-ui/icons/AddCircle"; 4 | import AppBar from "@material-ui/core/AppBar"; 5 | import Button from "@material-ui/core/Button"; 6 | import ChevronRight from "@material-ui/icons/ChevronRight"; 7 | import Divider from "@material-ui/core/Divider"; 8 | import Drawer from "@material-ui/core/Drawer"; 9 | import IconButton from "@material-ui/core/IconButton"; 10 | import Input from "@material-ui/core/Input"; 11 | import ListSubheader from "@material-ui/core/ListSubheader"; 12 | import MenuIcon from "@material-ui/icons/Menu"; 13 | import OperatorField from "./OperatorField"; 14 | import PropTypes from "prop-types"; 15 | import React, { Component } from "react"; 16 | import SortByAlpha from "@material-ui/icons/SortByAlpha"; 17 | import Toolbar from "@material-ui/core/Toolbar"; 18 | import Tooltip from "@material-ui/core/Tooltip"; 19 | import Typography from "@material-ui/core/Typography"; 20 | import { SearchField } from "./SearchField"; 21 | import { SelectField } from "muishift"; 22 | import { connect } from "react-redux"; 23 | import { withStyles } from "@material-ui/core/styles"; 24 | 25 | const styles = theme => ({ 26 | flex: { 27 | //flexGrow: 1 28 | }, 29 | list: { 30 | zIndex: theme.zIndex.drawer + 2, 31 | width: 250 32 | }, 33 | drawer: { 34 | zIndex: theme.zIndex.drawer + 2 35 | }, 36 | typography: { 37 | useNextVariants: true 38 | } 39 | }); 40 | 41 | class FilterDrawer extends Component { 42 | handleCloseFilter = () => { 43 | const { setFilterIsOpen, name } = this.props; 44 | 45 | setFilterIsOpen(name, false); 46 | }; 47 | 48 | handleSortFieldChange = (selectedField, fieldName) => { 49 | const { setFilterSortField, name } = this.props; 50 | 51 | setFilterSortField(name, selectedField); 52 | }; 53 | 54 | handleSortOrientationChange = orientation => { 55 | const { setFilterSortOrientation, name } = this.props; 56 | 57 | setFilterSortOrientation(name, orientation); 58 | }; 59 | 60 | handleAddFilterQuery = () => { 61 | const { addFilterQuery, name, formatMessage } = this.props; 62 | 63 | addFilterQuery(name, { 64 | operator: { 65 | value: "like", 66 | label: formatMessage 67 | ? formatMessage({ id: "operator_like_label" }) 68 | : "operator_like_label" 69 | } 70 | }); 71 | }; 72 | 73 | handleQueryChange = (index, field, value, operator) => { 74 | const { editFilterQuery, name } = this.props; 75 | 76 | let change = { 77 | [field]: value 78 | }; 79 | 80 | if (operator !== undefined) { 81 | change.operator = operator; 82 | } 83 | 84 | editFilterQuery(name, index, change); 85 | }; 86 | 87 | getFieldType = currentField => { 88 | const { fields } = this.props; 89 | 90 | if (!currentField) { 91 | return "string"; 92 | } 93 | 94 | let fieldType = "string"; 95 | 96 | fields.map(field => { 97 | if (field.name === currentField.value) { 98 | fieldType = field.type ? field.type : "string"; 99 | } 100 | return field; 101 | }); 102 | 103 | return fieldType; 104 | }; 105 | 106 | getFirstOperator = currentField => { 107 | const { operators } = this.props; 108 | const fieldType = this.getFieldType(currentField); 109 | 110 | if (!fieldType) { 111 | return undefined; 112 | } 113 | 114 | let op = undefined; 115 | 116 | operators.map(operator => { 117 | if ( 118 | operator.type === fieldType || 119 | (operator.type === "string" && fieldType === undefined) 120 | ) { 121 | op = { 122 | value: operator.operators[0].value, 123 | label: operator.operators[0].label 124 | }; 125 | } 126 | return op; 127 | }); 128 | 129 | return op; 130 | }; 131 | 132 | handleFieldChange = (i, field, val) => { 133 | const { editFilterQuery, name } = this.props; 134 | const operator = this.getFirstOperator(val); 135 | const type = this.getFieldType(val); 136 | 137 | editFilterQuery(name, i, { [field]: val, type, operator, value: "" }); 138 | }; 139 | 140 | handleQueryDelete = index => { 141 | const { removeFilterQuery, name } = this.props; 142 | 143 | removeFilterQuery(name, index); 144 | }; 145 | 146 | render() { 147 | const { 148 | theme, 149 | formatMessage, 150 | filters, 151 | name, 152 | fields, 153 | operators, 154 | DateTimeFormat, 155 | locale, 156 | okLabel, 157 | cancelLabel, 158 | setFilterIsOpen, 159 | classes 160 | } = this.props; 161 | 162 | const { 163 | isOpen, 164 | sortField, 165 | sortOrientation, 166 | queries 167 | } = filterSelectors.selectFilterProps(name, filters); 168 | 169 | return ( 170 |
171 | {isOpen && ( 172 | { 179 | setFilterIsOpen(name, false); 180 | }} 181 | > 182 |
183 | 184 | 185 | 194 | 198 | 199 | 200 | 201 | 202 | {formatMessage ? formatMessage({ id: "filter" }) : "Filter"} 203 | 204 | 205 | 206 | 207 | 208 |
209 | ({ 212 | value: suggestion.name, 213 | label: suggestion.label 214 | }))} 215 | itemToString={item => (item ? item.label : "")} 216 | onChange={this.handleSortFieldChange} 217 | inputProps={{ 218 | fullWidth: true, 219 | placeholder: formatMessage 220 | ? formatMessage({ id: "select_field" }) 221 | : "Select field" 222 | }} 223 | /> 224 |
225 | 234 | { 236 | this.handleSortOrientationChange(!sortOrientation); 237 | }} 238 | color={sortOrientation ? "primary" : "secondary"} 239 | > 240 | 241 | 242 | 243 |
244 | 245 | 246 | 247 | 248 | 253 | {formatMessage ? formatMessage({ id: "filter" }) : "Filter"} 254 | 255 | 264 | 268 | 269 | 270 | 271 | 272 |
273 | {queries.map((query, i) => { 274 | const { field } = filterSelectors.selectQueryProps(query); 275 | 276 | return ( 277 |
278 | 279 |
280 | ({ 283 | value: suggestion.name, 284 | label: suggestion.label 285 | }))} 286 | itemToString={item => (item ? item.label : "")} 287 | onChange={val => { 288 | this.handleFieldChange(i, "field", val); 289 | }} 290 | inputProps={{ 291 | fullWidth: true, 292 | placeholder: formatMessage 293 | ? formatMessage({ id: "select_field" }) 294 | : "Select field" 295 | }} 296 | /> 297 |
298 |
299 | 300 | { 309 | this.handleQueryDelete(i); 310 | }} 311 | /> 312 | 313 | 327 | 328 |
329 | ); 330 | })} 331 |
332 |
333 |
334 | )} 335 |
336 | ); 337 | } 338 | } 339 | 340 | FilterDrawer.propTypes = { 341 | formatMessage: PropTypes.func, 342 | theme: PropTypes.object.isRequired, 343 | name: PropTypes.string.isRequired, 344 | fields: PropTypes.array.isRequired, 345 | setFilterIsOpen: PropTypes.func.isRequired 346 | }; 347 | 348 | const mapStateToProps = (state, ownProps) => { 349 | const { filters, userSetOperators } = state; 350 | const { fields, formatMessage } = ownProps; 351 | 352 | const allOperators = [ 353 | { 354 | value: "like", 355 | label: formatMessage 356 | ? formatMessage({ id: "operator_like_label" }) 357 | : "Like" 358 | }, 359 | { 360 | value: "notlike", 361 | label: formatMessage 362 | ? formatMessage({ id: "operator_notlike_label" }) 363 | : "Not like" 364 | }, 365 | { 366 | value: "=", 367 | label: formatMessage ? formatMessage({ id: "operator_equal_label" }) : "=" 368 | }, 369 | { 370 | value: "!=", 371 | label: formatMessage 372 | ? formatMessage({ id: "operator_notequal_label" }) 373 | : "!=" 374 | }, 375 | { value: ">", label: ">" }, 376 | { value: ">=", label: ">=" }, 377 | { value: "<", label: "<" }, 378 | { value: "<=", label: "<=" }, 379 | { 380 | value: "novalue", 381 | label: formatMessage 382 | ? formatMessage({ id: "operator_novalue_label" }) 383 | : "No value" 384 | }, 385 | { 386 | value: "contains", 387 | label: formatMessage 388 | ? formatMessage({ id: "operator_contains_label" }) 389 | : "Contains" 390 | } 391 | ]; 392 | 393 | const operators = [ 394 | { 395 | type: "string", 396 | operators: allOperators.filter(operator => { 397 | return ( 398 | operator.value === "like" || 399 | operator.value === "notlike" || 400 | operator.value === "=" || 401 | operator.value === "!=" || 402 | operator.value === "novalue" 403 | ); 404 | }) 405 | }, 406 | { 407 | type: "date", 408 | operators: allOperators.filter(operator => { 409 | return ( 410 | operator.value === "=" || 411 | operator.value === "!=" || 412 | operator.value === "<=" || 413 | operator.value === ">=" || 414 | operator.value === "<" || 415 | operator.value === ">" 416 | ); 417 | }) 418 | }, 419 | { 420 | type: "number", 421 | operators: allOperators.filter(operator => { 422 | return ( 423 | operator.value === "=" || 424 | operator.value === "!=" || 425 | operator.value === "<=" || 426 | operator.value === ">=" || 427 | operator.value === "<" || 428 | operator.value === ">" 429 | ); 430 | }) 431 | }, 432 | { 433 | type: "bool", 434 | operators: allOperators.filter(operator => { 435 | return operator.value === "="; 436 | }) 437 | }, 438 | { 439 | type: "object", 440 | operators: allOperators.filter(operator => { 441 | return operator.value === "contains"; 442 | }) 443 | } 444 | ]; 445 | 446 | return { 447 | fields, 448 | operators: userSetOperators ? userSetOperators : operators, 449 | filters, 450 | formatMessage 451 | }; 452 | }; 453 | 454 | export default connect(mapStateToProps, { ...filterActions })( 455 | withStyles(styles, { withTheme: true })(FilterDrawer) 456 | ); 457 | -------------------------------------------------------------------------------- /src/components/OperatorField.js: -------------------------------------------------------------------------------- 1 | import * as filterSelectors from '../store/selectors' 2 | import Delete from '@material-ui/icons/Delete' 3 | import IconButton from '@material-ui/core/IconButton' 4 | import Input from '@material-ui/core/Input' 5 | import React, { Component } from 'react' 6 | import Toolbar from '@material-ui/core/Toolbar' 7 | import Tooltip from '@material-ui/core/Tooltip' 8 | import { SelectField } from 'muishift' 9 | import { withTheme, withStyles } from '@material-ui/core/styles' 10 | 11 | const styles = {} 12 | 13 | export const OperatorField = ({ 14 | queryIndex, 15 | currentField, 16 | query, 17 | fields, 18 | operators, 19 | handleQueryChange, 20 | formatMessage, 21 | classes, 22 | onClick 23 | }) => { 24 | const getFieldType = currentField => { 25 | let fieldType = '' 26 | 27 | fields.map(field => { 28 | if (field.name === currentField.value) { 29 | fieldType = field.type 30 | } 31 | return field 32 | }) 33 | 34 | return fieldType 35 | } 36 | 37 | const { operator, isCaseSensitive } = filterSelectors.selectQueryProps(query) 38 | 39 | if (queryIndex == null || currentField == null || query == null || handleQueryChange == null || fields == null) { 40 | return
41 | } 42 | 43 | const fieldType = getFieldType(currentField) 44 | let divFields = [] 45 | 46 | operators.map(operator => { 47 | if (operator.type === fieldType || (operator.type === 'string' && fieldType === undefined)) { 48 | operator.operators.map(op => { 49 | return divFields.push({ 50 | value: op.value, 51 | label: op.label 52 | }) 53 | }) 54 | } 55 | return divFields 56 | }) 57 | 58 | return ( 59 | 60 |
61 | { 64 | handleQueryChange(queryIndex, 'operator', val) 65 | }} 66 | items={divFields} 67 | itemToString={item => (item ? item.label : '')} 68 | id="react-select-single" 69 | inputProps={{ 70 | fullWidth: true, 71 | placeholder: formatMessage ? formatMessage({ id: 'hint_autocomplete' }) : 'Select operator' 72 | }} 73 | /> 74 |
75 | 76 | 85 | 86 | 87 | 88 | 89 |
90 | ) 91 | } 92 | 93 | export default withStyles(styles)(OperatorField) 94 | -------------------------------------------------------------------------------- /src/components/SearchField.js: -------------------------------------------------------------------------------- 1 | import * as filterSelectors from '../store/selectors' 2 | import FormatSize from '@material-ui/icons/FormatSize' 3 | import IconButton from '@material-ui/core/IconButton' 4 | import React, { Component } from 'react' 5 | import Switch from '@material-ui/core/Switch' 6 | import TextField from '@material-ui/core/TextField' 7 | import Toolbar from '@material-ui/core/Toolbar' 8 | import Tooltip from '@material-ui/core/Tooltip' 9 | import { KeyboardDatePicker } from '@material-ui/pickers' 10 | import moment from 'moment' 11 | 12 | export const SearchField = ({ 13 | theme, 14 | queryIndex, 15 | currentField, 16 | query, 17 | formatMessage, 18 | fields, 19 | handleQueryChange, 20 | DateTimeFormat, 21 | locale, 22 | okLabel, 23 | cancelLabel 24 | }) => { 25 | const { value, isCaseSensitive } = filterSelectors.selectQueryProps(query) 26 | 27 | if (queryIndex == null || currentField == null || query == null || handleQueryChange == null || fields == null) { 28 | return
29 | } 30 | 31 | let fieldType = '' 32 | let fieldLabel = '' 33 | 34 | fields.map(field => { 35 | if (field.name === currentField.value) { 36 | fieldType = field.type 37 | fieldLabel = field.label 38 | } 39 | return field 40 | }) 41 | 42 | if (fieldType === 'date') { 43 | return ( 44 | 45 | { 52 | handleQueryChange(queryIndex, 'value', val ? val.format() : null) 53 | }} 54 | autoOk 55 | animateYearScrolling={false} 56 | /> 57 | 58 | ) 59 | } 60 | 61 | if (fieldType === 'bool') { 62 | return ( 63 | 64 | { 67 | handleQueryChange(queryIndex, 'value', val) 68 | }} 69 | value={value} 70 | /> 71 | 72 | ) 73 | } else { 74 | //string 75 | 76 | return ( 77 | 78 | { 82 | handleQueryChange(queryIndex, 'value', e.target.value) 83 | }} 84 | value={value ? value : ''} 85 | placeholder={formatMessage ? formatMessage({ id: 'enter_query_text' }) : ''} 86 | /> 87 | 96 | { 98 | handleQueryChange(queryIndex, 'isCaseSensitive', !isCaseSensitive) 99 | }} 100 | color={isCaseSensitive ? 'primary' : undefined} 101 | > 102 | 103 | 104 | 105 | 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export default from './FilterDrawer' 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export FilterDrawer from './components/FilterDrawer' 2 | export filterReducer from './store/reducer' 3 | export * as filterSelectors from './store/selectors' 4 | export * as filterActions from './store/actions' 5 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | 3 | export function setFilterIsOpen(name, isOpen) { 4 | return { 5 | type: types.ON_FILTER_OPEN_CHANGED, 6 | name, 7 | payload: { isOpen } 8 | } 9 | } 10 | 11 | export function setFilterSortField(name, sortField) { 12 | return { 13 | type: types.ON_FILTER_SORT_FIELD_CHANGED, 14 | name, 15 | payload: { sortField } 16 | } 17 | } 18 | 19 | export function setFilterSortOrientation(name, sortOrientation) { 20 | return { 21 | type: types.ON_FILTER_SORT_FIELD_CHANGED, 22 | name, 23 | payload: { sortOrientation } 24 | } 25 | } 26 | 27 | export function addFilterQuery(name, query) { 28 | return { 29 | type: types.ON_ADD_FILTER_QUERY, 30 | name, 31 | payload: { ...query } 32 | } 33 | } 34 | 35 | export function setSearch(name, search) { 36 | return { 37 | type: types.ON_SET_SEARCH, 38 | name, 39 | payload: search 40 | } 41 | } 42 | 43 | export function editFilterQuery(name, index, query) { 44 | return { 45 | type: types.ON_EDIT_FILTER_QUERY, 46 | name, 47 | index, 48 | payload: { ...query } 49 | } 50 | } 51 | 52 | export function removeFilterQuery(name, index) { 53 | return { 54 | type: types.ON_REMOVE_FILTER_QUERY, 55 | name, 56 | index 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/store/reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | 3 | function query (query, action) { 4 | const { payload } = action 5 | 6 | switch (action.type) { 7 | case types.ON_ADD_FILTER_QUERY: 8 | case types.ON_EDIT_FILTER_QUERY: 9 | return { ...query, ...payload } 10 | 11 | default: 12 | return query 13 | } 14 | } 15 | 16 | function queries (queries = [], action) { 17 | const { index } = action 18 | 19 | switch (action.type) { 20 | case types.ON_ADD_FILTER_QUERY: 21 | return [...queries, query({}, action)] 22 | 23 | case types.ON_EDIT_FILTER_QUERY: 24 | return queries.map((q, i) => { 25 | if (index !== i) { 26 | return q 27 | } 28 | return query(q, action) 29 | }) 30 | 31 | case types.ON_REMOVE_FILTER_QUERY: 32 | return queries.filter((item, i) => i !== index) 33 | 34 | default: 35 | return queries 36 | } 37 | } 38 | 39 | function search (search = {}, action) { 40 | const { payload } = action 41 | 42 | switch (action.type) { 43 | case types.ON_SET_SEARCH: 44 | return { ...search, value: payload } 45 | 46 | default: 47 | return search 48 | } 49 | } 50 | 51 | function filter (filter = {}, action) { 52 | const { payload } = action 53 | 54 | switch (action.type) { 55 | case types.ON_FILTER_OPEN_CHANGED: 56 | case types.ON_FILTER_SORT_FIELD_CHANGED: 57 | case types.ON_FILTER_SORT_ORIENTATION_CHANGED: 58 | return { ...filter, ...payload } 59 | 60 | case types.ON_ADD_FILTER_QUERY: 61 | case types.ON_EDIT_FILTER_QUERY: 62 | case types.ON_REMOVE_FILTER_QUERY: 63 | return { ...filter, queries: queries(filter.queries, action) } 64 | 65 | case types.ON_SET_SEARCH: 66 | return { ...filter, search: search(filter.search, action) } 67 | 68 | default: 69 | return filter 70 | } 71 | } 72 | 73 | export default function filters (state = {}, action) { 74 | const { name } = action 75 | switch (action.type) { 76 | case types.ON_FILTER_OPEN_CHANGED: 77 | case types.ON_FILTER_SORT_FIELD_CHANGED: 78 | case types.ON_FILTER_SORT_ORIENTATION_CHANGED: 79 | case types.ON_ADD_FILTER_QUERY: 80 | case types.ON_EDIT_FILTER_QUERY: 81 | case types.ON_REMOVE_FILTER_QUERY: 82 | case types.ON_SET_SEARCH: 83 | return { ...state, [name]: filter(state[name], action) } 84 | 85 | default: 86 | return state 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/store/reducer.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import reducer from './reducer' 3 | import * as actions from './actions' 4 | 5 | const initialState = { 6 | } 7 | 8 | describe('locale reducer', () => { 9 | it('should return the initial state', () => { 10 | expect( 11 | reducer(undefined, {}) 12 | ).toEqual(initialState) 13 | }) 14 | 15 | it('should not affect state', () => { 16 | expect( 17 | reducer(initialState, { type: 'NOT_EXISTING' }) 18 | ).toEqual(initialState) 19 | }) 20 | 21 | it('should handle setFilterIsOpen', () => { 22 | expect( 23 | reducer(initialState, actions.setFilterIsOpen('demo', true)) 24 | ).toEqual({ ...initialState, ...{ demo: { isOpen: true } } }) 25 | }) 26 | 27 | it('should handle setFilterSortOrientation', () => { 28 | expect( 29 | reducer(initialState, actions.setFilterSortOrientation('demo', true)) 30 | ).toEqual({ ...initialState, ...{ demo: { sortOrientation: true } } }) 31 | }) 32 | 33 | it('should handle setFilterSortField', () => { 34 | expect( 35 | reducer(initialState, actions.setFilterSortField('demo', 'test')) 36 | ).toEqual({ ...initialState, ...{ demo: { sortField: 'test' } } }) 37 | }) 38 | 39 | it('should handle addFilterQuery', () => { 40 | expect( 41 | reducer(initialState, actions.addFilterQuery('demo', { field: 'test' })) 42 | ).toEqual({ ...initialState, ...{ demo: { queries: [{ field: 'test' }] } } }) 43 | }) 44 | 45 | it('should handle editFilterQuery', () => { 46 | expect( 47 | reducer({ demo: { queries: [{ field: 'test' }, { field: 'test3' }] } }, actions.editFilterQuery('demo', 0, { field: 'test2' })) 48 | ).toEqual({ ...initialState, ...{ demo: { queries: [{ field: 'test2' }, { field: 'test3' }] } } }) 49 | }) 50 | 51 | it('should handle removeFilterQuery', () => { 52 | expect( 53 | reducer({ demo: { queries: [{ field: 'test' }, { field: 'test3' }] } }, actions.removeFilterQuery('demo', 0)) 54 | ).toEqual({ ...initialState, ...{ demo: { queries: [{ field: 'test3' }] } } }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/store/selectors.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | export const STRING_TYPE = 'string' 4 | export const NUMBER_TYPE = 'number' 5 | export const DATE_TYPE = 'date' 6 | export const TIME_TYPE = 'time' 7 | export const ARRAY_TYPE = 'array' 8 | export const SELECT_FIELD_TYPE = 'select_field' 9 | 10 | function getValue( 11 | source, 12 | fieldName, 13 | getSourceValue = fieldValue => fieldValue, 14 | isCaseSensitive, 15 | type 16 | ) { 17 | if (source != null && getSourceValue(source)) { 18 | let fieldValue = getSourceValue(source)[fieldName] 19 | 20 | if (type === 'object') { 21 | return fieldValue ? fieldValue : {} 22 | } 23 | 24 | if (type === 'number') { 25 | return fieldValue ? parseFloat(fieldValue) : 0 26 | } 27 | 28 | if (typeof fieldValue === 'object' || fieldValue instanceof Object) { 29 | if (fieldValue.hasOwnProperty('label')) { 30 | fieldValue = fieldValue.label 31 | } 32 | } 33 | 34 | if (type === 'date') { 35 | return new Date(fieldValue).setHours(0, 0, 0, 0) 36 | } else if (type === 'bool') { 37 | return fieldValue === undefined ? 'false' : fieldValue 38 | } else { 39 | return isCaseSensitive === true 40 | ? String(fieldValue) 41 | : String(fieldValue).toUpperCase() 42 | } 43 | } 44 | } 45 | 46 | export function dynamicSort(sortField, sortOrientation, getSourceValue) { 47 | var sortOrder = sortOrientation ? 1 : -1 48 | 49 | return (x, y) => { 50 | var a = getValue(x, sortField, getSourceValue) 51 | var b = getValue(y, sortField, getSourceValue) 52 | var result = a < b ? -1 : a > b ? 1 : 0 53 | return result * sortOrder 54 | } 55 | } 56 | 57 | export function selectFilterProps(filterName, filters) { 58 | let isOpen = false 59 | let hasFilters = false 60 | let sortField = null 61 | let sortOrientation = true 62 | let queries = [] 63 | let searchValue = null 64 | 65 | if (filters !== undefined && filters[filterName] !== undefined) { 66 | const filter = filters[filterName] 67 | 68 | isOpen = filter.isOpen !== undefined ? filter.isOpen : isOpen 69 | hasFilters = 70 | filter.queries !== undefined ? filter.queries.length : hasFilters 71 | sortField = filter.sortField !== undefined ? filter.sortField : sortField 72 | sortOrientation = 73 | filter.sortOrientation !== undefined 74 | ? filter.sortOrientation 75 | : sortOrientation 76 | queries = filter.queries !== undefined ? filter.queries : queries 77 | searchValue = 78 | filter.search !== undefined ? filter.search.value : searchValue 79 | } 80 | 81 | return { 82 | isOpen, 83 | hasFilters, 84 | sortField, 85 | sortOrientation, 86 | queries, 87 | searchValue, 88 | } 89 | } 90 | 91 | export function selectQueryProps(query) { 92 | let value = '' 93 | let operator 94 | let field 95 | let type = 'string' 96 | let isCaseSensitive = false 97 | let isSet = false 98 | 99 | if (query !== undefined) { 100 | value = query.value !== undefined ? query.value : value 101 | operator = query.operator !== undefined ? query.operator : operator 102 | field = query.field !== undefined ? query.field : field 103 | type = query.type !== undefined ? query.type : type 104 | isCaseSensitive = 105 | query.isCaseSensitive !== undefined 106 | ? query.isCaseSensitive 107 | : isCaseSensitive 108 | isSet = 109 | field !== undefined && 110 | field !== null && 111 | operator !== undefined && 112 | operator !== null && 113 | value !== undefined 114 | } 115 | 116 | return { 117 | value, 118 | operator, 119 | field, 120 | type, 121 | isCaseSensitive, 122 | isSet, 123 | } 124 | } 125 | 126 | export function getFilteredList(filterName, filters, list, getSourceValue) { 127 | const { 128 | sortField, 129 | sortOrientation, 130 | queries, 131 | searchValue, 132 | } = selectFilterProps(filterName, filters) 133 | const dateOptions = { day: '2-digit', month: '2-digit', year: 'numeric' } 134 | 135 | if (list == null || list.length < 1) { 136 | return [] 137 | } 138 | 139 | let result = [...list] 140 | 141 | result = result.filter((row, i) => { 142 | let show = true 143 | 144 | if (queries) { 145 | for (let query of queries) { 146 | const { 147 | value, 148 | operator, 149 | field, 150 | isCaseSensitive, 151 | isSet, 152 | type, 153 | } = selectQueryProps(query) 154 | 155 | if (isSet) { 156 | let fieldValue = getValue( 157 | row, 158 | field.value, 159 | getSourceValue, 160 | isCaseSensitive, 161 | type 162 | ) 163 | 164 | if (type === 'date') { 165 | const queryDate = moment(value) 166 | 167 | switch (operator.value) { 168 | case '=': 169 | show = queryDate.isSame(fieldValue, 'day') 170 | break 171 | 172 | case '!=': 173 | show = !queryDate.isSame(fieldValue, 'day') 174 | break 175 | 176 | case '>': 177 | show = queryDate.isBefore(fieldValue, 'day') 178 | break 179 | 180 | case '>=': 181 | show = queryDate.isSameOrBefore(fieldValue, 'day') 182 | break 183 | 184 | case '<': 185 | show = queryDate.isAfter(fieldValue, 'day') 186 | break 187 | 188 | case '<=': 189 | show = queryDate.isSameOrAfter(fieldValue, 'day') 190 | break 191 | 192 | default: 193 | break 194 | } 195 | 196 | if (!show) { 197 | return show 198 | } 199 | } else if (type === 'number') { 200 | const queryNumber = parseFloat(value) 201 | 202 | switch (operator.value) { 203 | case '=': 204 | show = fieldValue === queryNumber 205 | break 206 | 207 | case '!=': 208 | show = fieldValue !== queryNumber 209 | break 210 | 211 | case '>': 212 | show = fieldValue > queryNumber 213 | break 214 | 215 | case '>=': 216 | show = fieldValue >= queryNumber 217 | break 218 | 219 | case '<': 220 | show = fieldValue < queryNumber 221 | break 222 | 223 | case '<=': 224 | show = fieldValue <= queryNumber 225 | break 226 | 227 | default: 228 | break 229 | } 230 | 231 | if (!show) { 232 | return show 233 | } 234 | } else if (type === 'bool') { 235 | let fieldVal = false 236 | if (fieldValue === true || fieldValue === 'true') { 237 | fieldVal = true 238 | } 239 | 240 | let queryVal = false 241 | if (value === true || value === 'true') { 242 | queryVal = true 243 | } 244 | 245 | show = fieldVal === queryVal 246 | 247 | if (!show) { 248 | return show 249 | } 250 | } else if (type === 'object') { 251 | show = 252 | JSON.stringify(fieldValue ? fieldValue : '') 253 | .toUpperCase() 254 | .indexOf(String(value ? value : '').toUpperCase()) !== -1 255 | 256 | if (!show) { 257 | return show 258 | } 259 | } else { 260 | const valueString = String(value) 261 | const fieldValueString = String(fieldValue) 262 | let queryValueString = 263 | isCaseSensitive === true ? valueString : valueString.toUpperCase() 264 | 265 | switch (operator.value) { 266 | case 'like': 267 | show = fieldValueString.indexOf(queryValueString) !== -1 268 | break 269 | 270 | case 'notlike': 271 | show = fieldValueString.indexOf(queryValueString) === -1 272 | break 273 | 274 | case '=': 275 | show = fieldValueString === queryValueString 276 | break 277 | 278 | case '>': 279 | show = fieldValueString.localeCompare(queryValueString) > 0 280 | break 281 | 282 | case '>=': 283 | show = fieldValueString.localeCompare(queryValueString) >= 0 284 | break 285 | 286 | case '<': 287 | show = fieldValueString.localeCompare(queryValueString) < 0 288 | break 289 | 290 | case '<=': 291 | show = fieldValueString.localeCompare(valueString) <= 0 292 | break 293 | 294 | default: 295 | break 296 | } 297 | 298 | if (!show) { 299 | return show 300 | } 301 | } 302 | } 303 | } 304 | } 305 | 306 | if (searchValue != null && searchValue !== '' && show) { 307 | let rowInString = ""; 308 | for (let key of Object.keys(row)) { 309 | rowInString += row[key] + ","; 310 | } 311 | show = JSON.stringify(rowInString).toUpperCase().indexOf(String(searchValue).toUpperCase()) !== -1; 312 | } 313 | 314 | return show 315 | }) 316 | 317 | if (result !== undefined && sortField !== null) { 318 | result.sort(dynamicSort(sortField.value, sortOrientation, getSourceValue)) 319 | } 320 | 321 | return result 322 | } 323 | -------------------------------------------------------------------------------- /src/store/types.js: -------------------------------------------------------------------------------- 1 | export const ON_FILTER_OPEN_CHANGED = 'filters@ON_FILTER_OPEN_CHANGED' 2 | export const ON_FILTER_SORT_FIELD_CHANGED = 'filters@ON_FILTER_SORT_FIELD_CHANGED' 3 | export const ON_FILTER_SORT_ORIENTATION_CHANGED = 'filters@ON_FILTER_SORT_ORIENTATION_CHANGED' 4 | export const ON_ADD_FILTER_QUERY = 'filters@ON_ADD_FILTER_QUERY' 5 | export const ON_EDIT_FILTER_QUERY = 'filters@ON_EDIT_FILTER_QUERY' 6 | export const ON_REMOVE_FILTER_QUERY = 'filters@ON_REMOVE_FILTER_QUERY' 7 | export const ON_SET_SEARCH = 'filters@ON_SET_SEARCH' 8 | --------------------------------------------------------------------------------