├── .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 |
--------------------------------------------------------------------------------