StringArrFilter.tsx 17.7 KB
Newer Older
Maxym Borodenko's avatar
Maxym Borodenko committed
1
import * as React from 'react';
2
import { Fields, change } from 'redux-form';
3
4
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
5
import { translate } from 'react-i18next';
6
import * as _ from 'lodash';
Maxym Borodenko's avatar
Maxym Borodenko committed
7

8
9
10
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormLabel from '@material-ui/core/FormLabel';
Matija Obreza's avatar
Matija Obreza committed
11
import FormControl from '@material-ui/core/FormControl';
Maxym Borodenko's avatar
Maxym Borodenko committed
12

13
import Checkbox from '@material-ui/core/Checkbox';
Matija Obreza's avatar
Matija Obreza committed
14
15
16
17
import IconButton from '@material-ui/core/IconButton';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
import InputAdornment from '@material-ui/core/InputAdornment';
18
import { AddCircle as Add, RemoveCircle as Remove, PlusOne, HighlightOff as Clear} from '@material-ui/icons';
19
import Number from 'ui/common/Number';
20
import { Properties, PropertiesItem } from 'ui/common/Properties';
Maxym Borodenko's avatar
Maxym Borodenko committed
21

22
23
24
interface IStringListProps extends React.ClassAttributes<any> {
  input: any;
  notInput: any;
25
  removeByValue: (index: number, isNot: boolean) => void;
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
}


class StringList extends React.Component<IStringListProps> {

    public state = {
        renderList: [],
    };

    private addToNotList = (item, index) => {
        const { input, notInput } = this.props;
        const {renderList} = this.state;
        renderList[index] = {value: item.value, state: false};
        this.setState({renderList});

        input.onChange(renderList.filter((renderItem) => renderItem.state).map((renderItem) => renderItem.value));
        notInput.onChange(renderList.filter((renderItem) => !renderItem.state).map((renderItem) => renderItem.value));
    }

    private removeFromNotList = (item, index) => {
        const { input, notInput } = this.props;
        const {renderList} = this.state;
        renderList[index] = {value: item.value, state: true};
        this.setState({renderList});

        input.onChange(renderList.filter((renderItem) => renderItem.state).map((renderItem) => renderItem.value));
        notInput.onChange(renderList.filter((renderItem) => !renderItem.state).map((renderItem) => renderItem.value));
    }

    public componentWillMount() {
        let {input: {value: doList}, notInput: {value: notList}} = this.props;
        doList = doList || [];
        notList = notList || [];
        const {renderList} = this.state;

        doList
          .filter((doItem) => renderList.findIndex((renderItem) => renderItem.value === doItem) === -1)
          .map((doItem) => renderList.push({value: doItem, state: true}));

        notList
          .filter((notItem) => renderList.findIndex((renderItem) => renderItem.value === notItem) === -1)
          .map((notItem) => renderList.push({value: notItem, state: false}));

        this.setState({renderList});
    }

    public componentWillReceiveProps(nextProps) {
        let {input: {value: doList}, notInput: {value: notList}} = nextProps;
        doList = doList || [];
        notList = notList || [];
        const {renderList} = this.state;
        const newRenderList = [];

        renderList.map((renderItem) => {
            if (doList.indexOf(renderItem.value) !== -1 || notList.indexOf(renderItem.value) !== -1) {
                newRenderList.push(renderItem);
            }
        });

        doList
          .filter((doItem) => newRenderList.findIndex((renderItem) => renderItem.value === doItem) === -1)
          .map((doItem) => newRenderList.push({value: doItem, state: true}));

        notList
          .filter((notItem) => newRenderList.findIndex((renderItem) => renderItem.value === notItem) === -1)
          .map((notItem) => newRenderList.push({value: notItem, state: false}));

        this.setState({renderList: newRenderList});
    }

    public render() {
97
        const {removeByValue} = this.props;
98
99
100
101
        const {renderList} = this.state;
        return (
            <div>
                { renderList.map((renderItem, index) => (
102
103
104
105
106
107
108
109
110
111
                    <div style={ { margin: '.2rem 0', padding: '.2rem 1rem', backgroundColor: '#e8e5e1', color: '#202222', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } } key={ renderItem.value }>
                        <span style={ {display: 'flex', alignItems: 'center'} }>
                            { renderItem.state ?
                                <span className="font-bold float-left mr-5" onClick={ () => this.addToNotList(renderItem, index) }><Add style={ {cursor: 'pointer', color: '#6f6f6f', fontSize: '18px'} }/></span>
                                :
                                <span className="font-bold float-left mr-5" onClick={ () => this.removeFromNotList(renderItem, index) }><Remove style={ {cursor: 'pointer', color: '#6f6f6f', fontSize: '18px'} }/></span>
                            }
                            { renderItem.value }
                        </span>
                        <div className="font-bold float-right" onClick={ () => removeByValue(renderItem.value, !renderItem.state) }><Clear style={ {cursor: 'pointer', color: '#6f6f6f', fontSize: '20px'} }/></div>
112
113
114
115
116
117
118
119
120
                    </div>
                )) }
            </div>

        );
    }
}


Maxym Borodenko's avatar
Maxym Borodenko committed
121
122
123
124
125
126
interface IStringArrFilterInternal extends React.ClassAttributes<any> {
    name: string;
    input: any;
    placeholder?: string;
    label?: string;
    options?: { [key: string]: any; };
127
    indented?: boolean;
128
    t: any;
129
130
    addToNotList: (item: any) => void;
    notList: any[];
Maxym Borodenko's avatar
Maxym Borodenko committed
131
132
133
134
135
136
}

class InternalStringArrField extends React.Component<IStringArrFilterInternal & any, any> {

    private constructor(props, context) {
        super(props, context);
137
138
        const notValues = _.get(props, `${props.names[0]}.input.value`);
        const values = _.get(props, `${props.names[1]}.input.value`);
Maxym Borodenko's avatar
Maxym Borodenko committed
139
140

        this.state = {
141
            excludedValues: null,
142
143
            values,
            notValues,
Maxym Borodenko's avatar
Maxym Borodenko committed
144
145
146
147
            text: '',
        };
    }

148
    public componentWillMount() {
149
150
151
152
153
        const input = _.get(this.props, `${this.props.names[0]}.input`);
        const value = typeof input.value[0] === 'number' ? input.value.map((key) => `${key}`) : input.value;
        const notValue = _.get(this.props, `${this.props.names[1]}.input.value`);

        this.setState({
154
            excludedValues: null,
155
156
157
158
            values: [ ...value ],
            notValues: [ ...notValue ],
            text: '',
        });
159
160
    }

Maxym Borodenko's avatar
Maxym Borodenko committed
161
    public componentWillReceiveProps(nextProps) {
162
163
164
165
        const input = _.get(nextProps, `${nextProps.names[0]}.input`);
        const value = typeof input.value[0] === 'number' ? input.value.map((key) => `${key}`) : input.value;
        const notValue = _.get(nextProps, `${nextProps.names[1]}.input.value`);

Maxym Borodenko's avatar
Maxym Borodenko committed
166
        this.setState({
167
            excludedValues: [...value, ...notValue],
168
            values: [ ...value ],
169
            notValues: [...notValue],
Maxym Borodenko's avatar
Maxym Borodenko committed
170
171
172
173
            text: '',
        });
    }

174
    private maybeAdd = (...newValues: string[]) => {
Maxym Borodenko's avatar
Maxym Borodenko committed
175
      const values = [ ...this.state.values ];
176
      const excludedValues = [ ...this.state.excludedValues ];
Maxym Borodenko's avatar
Maxym Borodenko committed
177

178
179
180
181
182
      newValues.forEach((text) => {
        if (text && text.length > 0) {
          if (values.indexOf(text) < 0) {
            values.push(text);
          }
183
184
185
          if (excludedValues.indexOf(text) < 0) {
            excludedValues.push(text);
          }
Maxym Borodenko's avatar
Maxym Borodenko committed
186
        }
187
188
189
      });

      if (!_.isEqual(values, this.state.values)) {
Maxym Borodenko's avatar
Maxym Borodenko committed
190
191
192
        this.setState({
          text: '',
          values,
193
          excludedValues,
Maxym Borodenko's avatar
Maxym Borodenko committed
194
195
196
197
198
        });
      }
      return values;
    }

199
200
201
    private maybeRemove = (isNot?: boolean, ...newValues: string[]) => {
      const values = isNot ? [ ...this.state.notValues ] : [ ...this.state.values ];
      const excludedValues = this.state.excludedValues;
Maxym Borodenko's avatar
Maxym Borodenko committed
202

203
204
205
206
207
208
      newValues.forEach((text) => {
        if (text && text.length > 0) {
          const index: number = values.indexOf(text);
          if (index >= 0) {
            values.splice(index, 1);
          }
209
210
211
212
          const indexExcluded: number = excludedValues.indexOf(text);
          if (indexExcluded >= 0) {
            excludedValues.splice(indexExcluded, 1);
          }
Maxym Borodenko's avatar
Maxym Borodenko committed
213
        }
214
215
      });

216
      if (!_.isEqual(values, isNot ? this.state.notValues : this.state.values)) {
Maxym Borodenko's avatar
Maxym Borodenko committed
217
218
        this.setState({
          text: '',
219
220
221
          values: isNot ? this.state.values : values,
          notValues: !isNot ? this.state.notValues : values,
          excludedValues,
Maxym Borodenko's avatar
Maxym Borodenko committed
222
223
224
225
226
227
        });
      }
      return values;
    }

    private handleKeyPres = (event) => {
228
        const { input } = _.get(this.props, this.props.names[0]);
Maxym Borodenko's avatar
Maxym Borodenko committed
229
230
231
232
233
234
235
236
237
238
239
240
241
242
        const { text } = this.state;

        if (event.key === 'Enter') {
          if (text && text.length > 0) {
            event.preventDefault();
            const values = this.maybeAdd(text);
            input.onChange(values);
          } else {
            // on blank, submit it
            // input.onChange(values);
          }
        }
    }

243
    private removeByValue = (value, isNot) => {
244
        const { input } = _.get(this.props, this.props.names[isNot ? 1 : 0]);
Maxym Borodenko's avatar
Maxym Borodenko committed
245

246
        const newValues = this.maybeRemove(isNot, value);
Maxym Borodenko's avatar
Maxym Borodenko committed
247
248
249
250
251
252
253
254
255
        input.onChange(newValues);
    }

    // only for the textbox
    private handleChange = (event) => {
        this.setState({ ...this.state, text: event.target.value });
    }

    private handleAddCurrent = (event) => {
256
        const { input } = _.get(this.props, this.props.names[0]);
Maxym Borodenko's avatar
Maxym Borodenko committed
257
258
259
260
261
262
263
        const { text } = this.state;

        event.preventDefault();
        const values = this.maybeAdd(text);
        input.onChange(values);
    }

264
265
266
267
268
269
    private handleCheckbox = (event) => {
      const { input } = _.get(this.props, this.props.names[0]);
      const value = event.target.value;

      if (this.props.indented) {
        const subCategories = this.getSubcategories(value);
270
        const values = event.target.checked ? this.maybeAdd(value, ...subCategories) : this.maybeRemove(false, value);
271
272
        input.onChange(values);
      } else {
273
        const values = event.target.checked ? this.maybeAdd(value) : this.maybeRemove(false, value);
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
        input.onChange(values);
      }
    }

    private getSubcategories = (value) => {
      const myRegexp = /^([1-9]+)(0+)$/;
      const match = myRegexp.exec(value);
      const category = match ? match[1] : null;
      const subCategories = [];
      const filteredOptions = this.getFilteredOptions();
      Object.keys(this.props.options).forEach((option) => {
        if (category && option !== value && filteredOptions.indexOf(option) > 0 && option.startsWith(category)) {
          subCategories.push(option);
        }
      });
      return subCategories;
    }

    private getFilteredOptions = () => {
      const { options, byKey, terms, valueField } = this.props;
      const fieldOptions = options && (byKey ? Object.keys(options) : options);
      return terms ? (fieldOptions && fieldOptions.filter((opt) => terms.get(valueField ? opt[valueField] : `${opt}`) > 0)) : fieldOptions;
    }

    private buildMapOfCategories = (map: Map<string, number>, options: string[], level: number) => {
      options.forEach((option) => {
        const subCategories = this.getSubcategories(option);
        if (subCategories.length === 0) {
          if (map.has(option)) {
            const oldLevel = map.get(option);
            if (oldLevel <= level) {
              map.set(option, level);
            }
          } else {
            map.set(option, level);
          }
        } else {
          if (map.has(option)) {
            const oldLevel = map.get(option);
            if (oldLevel <= level) {
              map.set(option, level);
              this.buildMapOfCategories(map, subCategories, level + 1);
            }
          } else {
            map.set(option, level);
            this.buildMapOfCategories(map, subCategories, level + 1);
          }
        }
      });
    }

Maxym Borodenko's avatar
Maxym Borodenko committed
325
    public render() {
326
        const { placeholder, label, options, indented, t, names, terms, valueField, labelField, classes } = this.props;
327
328
329
        const { input } = _.get(this.props, names[0]);
        const { input: notInput } = _.get(this.props, names[1]);

330
        const { text, values, notValues, excludedValues } = this.state;
Maxym Borodenko's avatar
Maxym Borodenko committed
331
332

        const withOptions: boolean = options !== undefined && typeof options === 'object';
333
        const filteredOptions = this.getFilteredOptions();
334

335
336
337
338
339
        let tree;
        if (indented && options) {
          tree = new Map<string, number>();
          this.buildMapOfCategories(tree, Object.keys(options), 0);
        }
Maxym Borodenko's avatar
Maxym Borodenko committed
340
341

        return (
342
343
344
345
346
347
348
349
350
          <div>
            { withOptions ?
              (!filteredOptions || filteredOptions.length === 0 ?
                  (<div className="pl-15">{ t('common:f.noFilters') }</div>)
                  : <FormControl fullWidth component={ 'fieldset' as 'div' } className="full-width">
                    { label && <FormLabel component="label">{ label }</FormLabel> }
                    <FormGroup>
                      { filteredOptions.map((key) => (
                        <FormControlLabel
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
351
                          key={ valueField ? `${ key[valueField] }` : `${ key }` }
Oleksii Savran's avatar
Oleksii Savran committed
352
353
354
355
356
                          style={
                            (document && document.dir === 'rtl') ?
                              { paddingRight: `${tree ? (tree.get(key) * 20) : 0}px`, marginLeft: 0 } :
                              { paddingLeft: `${tree ? (tree.get(key) * 20) : 0}px`, marginRight: 0 }
                          }
357
                          classes={ {label: 'full-width', root: 'mr-0'} }
Oleksii Savran's avatar
Oleksii Savran committed
358
                          className="form-control-label-rtl"
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
                          label={
                            <span className="full-width" style={ {display: 'flex', justifyContent: 'space-between'} }>
                              { `${ t(options[key]) || (labelField ? key[labelField] : key) }` }
                              { terms &&
                              <span className="float-right">
                                <Number value={ terms.get(valueField ? `${ key[valueField] }` : `${ key }`) }/>
                              </span>
                              }
                              </span>
                          }
                          control={
                            <Checkbox
                              value={ valueField ? `${ key[valueField] }` :  key }
                              checked={ values.indexOf(valueField ? `${ key[valueField] }` :  key) >= 0 }
                              onChange={ this.handleCheckbox }
                            />
                          }
                        />
                      )) }
                      </FormGroup>
                  </FormControl>
              ) :
Maxym Borodenko's avatar
Maxym Borodenko committed
381
                  <FormControl fullWidth className="full-width">
382
                    <InputLabel>{ t(`${label}`) }</InputLabel>
Maxym Borodenko's avatar
Maxym Borodenko committed
383
384
385
386
                    <Input
                      className="full-width"
                      value={ text }
                      onChange={ this.handleChange }
387
                      placeholder={ placeholder ? t(`${placeholder}`) : '' }
Maxym Borodenko's avatar
Maxym Borodenko committed
388
389
390
391
392
                      onKeyPress={ this.handleKeyPres }
                      onBlur={ this.handleAddCurrent }
                      endAdornment={
                        <InputAdornment position="end">
                          <IconButton type="button" onClick={ this.handleAddCurrent }>
Maxym Borodenko's avatar
Maxym Borodenko committed
393
                            <PlusOne style={ { fontSize: '1.5rem' } } />
Maxym Borodenko's avatar
Maxym Borodenko committed
394
395
396
397
398
399
                          </IconButton>
                        </InputAdornment>
                      }
                    />
                  </FormControl>
                }
400
                { (! withOptions && ((values && values.length > 0) || (notValues && notValues.length > 0))) &&
401
                <StringList input={ input } notInput={ notInput } removeByValue={ this.removeByValue } />
402
                }
403
404
405
                { !withOptions && terms &&
                <Properties>
                    <h5 className="pl-10 pt-1rem mb-10">{ t('common:f.suggestedFilters') }</h5>
406
                    { terms && Array.from(terms).slice(0, 10).filter(([key, value]) => !excludedValues || !excludedValues.includes(key)).map(([key, value]) => (
407
                        <PropertiesItem key={ key } title={ key }  onClick={ () => input.onChange(this.maybeAdd(key)) } classes={ {...classes, propertiesRow: 'cursor-pointer'} }>
408
409
410
411
412
413
414
415
                            <span className="float-right">
                                <Number value={ value }/>
                            </span>
                        </PropertiesItem>
                    )) }
                </Properties>
                }

Maxym Borodenko's avatar
Maxym Borodenko committed
416
417
418
419
420
421
422
423
424
425
            </div>
        );
    }
}

interface IStringArrFilter extends React.ClassAttributes<any> {
    name: string;
    placeholder?: string;
    label?: string;
    options?: { [key: string]: any; };
426
427
428
429
    terms?: { [key: string]: any; };
    valueField?: string;
    labelField?: string;
    byKey?: boolean;
430
    classes: any;
431
    indented?: boolean;
432
    t: any;
Maxym Borodenko's avatar
Maxym Borodenko committed
433
434
435
436
437
438
}

class StringArrFilter extends React.Component<IStringArrFilter, any> {


    public render() {
439
        const { name, label, placeholder, options, indented, terms, valueField, labelField, byKey, classes, t } = this.props;
Maxym Borodenko's avatar
Maxym Borodenko committed
440
441
        return (
            <div>
442
443
                <Fields
                    names={ [`${name}`, `NOT.${name}`] }
Maxym Borodenko's avatar
Maxym Borodenko committed
444
                    component={ InternalStringArrField }
Viacheslav Pavlov's avatar
i18n    
Viacheslav Pavlov committed
445
                    label={ t(label) }
Maxym Borodenko's avatar
Maxym Borodenko committed
446
447
                    placeholder={ placeholder }
                    options={ options }
448
449
450
451
                    terms={ terms }
                    valueField={ valueField }
                    labelField={ labelField }
                    byKey={ byKey }
452
                    classes={ classes }
453
                    indented={ indented }
454
                    t={ t }
Maxym Borodenko's avatar
Maxym Borodenko committed
455
456
457
458
459
460
                />
            </div>
        );
    }
}

461
462
463
464
465
466
467

const mapDispatchToProps = (dispatch) => bindActionCreators({
  change,
}, dispatch);


export default connect(null, mapDispatchToProps)(translate()(StringArrFilter));