StringArrFilter.tsx 14.6 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';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
5
import { WithTranslation, withTranslation } from 'react-i18next';
6
import * as _ from 'lodash';
Maxym Borodenko's avatar
Maxym Borodenko committed
7

Matija Obreza's avatar
Matija Obreza committed
8
import FormControl from '@material-ui/core/FormControl';
Maxym Borodenko's avatar
Maxym Borodenko committed
9

Matija Obreza's avatar
Matija Obreza committed
10 11
import IconButton from '@material-ui/core/IconButton';
import InputAdornment from '@material-ui/core/InputAdornment';
12
import { AddCircle as Add, RemoveCircle as Remove, PlusOne, HighlightOff as Clear} from '@material-ui/icons';
13
import Number from 'ui/common/Number';
14
import { Properties, PropertiesItem } from 'ui/common/Properties';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
15
import { TextField, WithStyles } from '@material-ui/core';
Maxym Borodenko's avatar
Maxym Borodenko committed
16

17 18 19
interface IStringListProps extends React.ClassAttributes<any> {
  input: any;
  notInput: any;
20
  removeByValue: (index: number, isNot: boolean) => void;
21 22 23 24 25 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
}


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() {
92
        const {removeByValue} = this.props;
93 94 95 96
        const {renderList} = this.state;
        return (
            <div>
                { renderList.map((renderItem, index) => (
97
                    <div style={ { margin: '.2rem 0', padding: '.2rem 1rem', backgroundColor: '#e8e5e1', color: '#202222', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } } key={ renderItem.value }>
98
                        <span style={ {display: 'inline-block', whiteSpace: 'nowrap', overflow: 'hidden',  textOverflow: 'ellipsis' } }>
99 100 101 102 103 104 105 106
                            { 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>
107 108 109 110 111 112 113 114 115
                    </div>
                )) }
            </div>

        );
    }
}


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

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

    private constructor(props, context) {
        super(props, context);
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
133 134 135 136
        const input = _.get(props, `${this.props.names[0]}.input`);
        const value = typeof input.value[0] === 'number' ? input.value.map((key) => `${key}`) : input.value;
        const notInput = _.get(props, `${this.props.names[1]}.input`);
        const notValue = typeof notInput.value[0] === 'number' ? notInput.value.map((key) => `${key}`) : notInput.value;
Maxym Borodenko's avatar
Maxym Borodenko committed
137 138

        this.state = {
Maxym Borodenko's avatar
Maxym Borodenko committed
139
            excludedValues: [],
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
140 141
            values: [ ...value ],
            notValues: [ ...notValue ],
Maxym Borodenko's avatar
Maxym Borodenko committed
142
            text: '',
143
            error: null,
Maxym Borodenko's avatar
Maxym Borodenko committed
144 145 146
        };
    }

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

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

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

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

177
    private maybeAdd = (...newValues: string[]) => {
Maxym Borodenko's avatar
Maxym Borodenko committed
178
      const values = [ ...this.state.values ];
179
      const notValues = [ ...this.state.notValues ];
180
      const excludedValues = [ ...this.state.excludedValues ];
Maxym Borodenko's avatar
Maxym Borodenko committed
181

182 183
      newValues.forEach((text) => {
        if (text && text.length > 0) {
184
          if (values.indexOf(text) < 0 && notValues.indexOf(text) < 0) {
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
185
            values.push(`${text}`);
186
          }
187
          if (excludedValues.indexOf(text) < 0) {
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
188
            excludedValues.push(`${text}`);
189
          }
Maxym Borodenko's avatar
Maxym Borodenko committed
190
        }
191 192 193
      });

      if (!_.isEqual(values, this.state.values)) {
Maxym Borodenko's avatar
Maxym Borodenko committed
194 195 196
        this.setState({
          text: '',
          values,
197
          excludedValues,
Maxym Borodenko's avatar
Maxym Borodenko committed
198 199 200 201 202
        });
      }
      return values;
    }

203 204 205
    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
206

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

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

    private handleKeyPres = (event) => {
232
        const { input } = _.get(this.props, this.props.names[0]);
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
233
        const { text, error } = this.state;
Maxym Borodenko's avatar
Maxym Borodenko committed
234 235

        if (event.key === 'Enter') {
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
236
          if (text && text.length > 0 && !error) {
Maxym Borodenko's avatar
Maxym Borodenko committed
237 238 239 240
            event.preventDefault();
            const values = this.maybeAdd(text);
            input.onChange(values);
          } else {
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
241
            return this.setState({text: '', error: null});
Maxym Borodenko's avatar
Maxym Borodenko committed
242 243 244 245
          }
        }
    }

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

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

253 254 255 256 257 258 259 260 261 262 263 264
    private validate = (value) => {
      const {validators} = this.props;

      for (const validator of validators)  {
        const validateResult = validator(value);
        if (validateResult) {
          return validateResult;
        }
      }
      return  null;
    }

Maxym Borodenko's avatar
Maxym Borodenko committed
265 266
    // only for the textbox
    private handleChange = (event) => {
267
        this.setState({ ...this.state, text: event.target.value, error: this.validate(event.target.value) });
Maxym Borodenko's avatar
Maxym Borodenko committed
268 269 270
    }

    private handleAddCurrent = (event) => {
271
        const { input } = _.get(this.props, this.props.names[0]);
Maxym Borodenko's avatar
Maxym Borodenko committed
272 273
        const { text } = this.state;

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
274 275 276 277
        if (this.validate(text)) {
          return this.setState({text: '', error: null});
        }

Maxym Borodenko's avatar
Maxym Borodenko committed
278 279 280 281 282
        event.preventDefault();
        const values = this.maybeAdd(text);
        input.onChange(values);
    }

283 284 285 286 287
    private dataPasted = (e) => {
      const data = e.clipboardData.getData('text/plain');
      const dataArr = data.split(/[,\n;]/).map((item) => item.trim());
      if (dataArr && dataArr.length > 1) {
          e.preventDefault();
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
288 289
          const validValues = dataArr.filter((data) => !this.validate(data));
          const values = this.maybeAdd(...validValues);
290 291 292 293 294
          const { input } = _.get(this.props, this.props.names[0]);
          input.onChange(values);
        }
    }

Maxym Borodenko's avatar
Maxym Borodenko committed
295
    public render() {
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
296
        const { placeholder, label, t, names, terms, suggestionTranslations, classes } = this.props;
297 298 299
        const { input } = _.get(this.props, names[0]);
        const { input: notInput } = _.get(this.props, names[1]);

300
        const { text, values, notValues, excludedValues = [] } = this.state;
Maxym Borodenko's avatar
Maxym Borodenko committed
301 302

        return (
303
          <div>
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
            <FormControl fullWidth className="full-width">
              <TextField
                className="full-width"
                label={ t(`${label}`) }
                value={ text }
                onChange={ this.handleChange }
                placeholder={ placeholder ? t(`${placeholder}`) : '' }
                onKeyPress={ this.handleKeyPres }
                onBlur={ this.handleAddCurrent }
                onPaste={ this.dataPasted }
                error={ !!this.state.error }
                helperText={ t(this.state.error) }
                InputProps={ {
                  endAdornment: (
                    <InputAdornment position="end">
                      <IconButton type="button" onClick={ this.handleAddCurrent }>
                        <PlusOne style={ { fontSize: '1.5rem' } } />
                      </IconButton>
                    </InputAdornment>
                  ),
                } }
              />
            </FormControl>
              { ((values && values.length > 0) || (notValues && notValues.length > 0)) &&
328
                <StringList input={ input } notInput={ notInput } removeByValue={ this.removeByValue } />
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
329 330
              }
              { terms &&
331
                <Properties>
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
332 333
                  <h5 className="pl-10 pt-1rem mb-10">{ t('common:f.suggestedFilters') }</h5>
                  { terms && Array.from(terms).slice(0, 10).filter(([key, value]) => !['missing', ...excludedValues].includes(key)).map(([key, value]) => (
334
                      <PropertiesItem suggestion key={ key } title={ suggestionTranslations && suggestionTranslations[key] || key }  onClick={ () => input.onChange(this.maybeAdd(key)) } classes={ {...classes, propertiesRow: 'cursor-pointer'} }>
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
335 336 337 338 339
                          <span className="float-right">
                              <Number value={ value }/>
                          </span>
                      </PropertiesItem>
                  )) }
340
                </Properties>
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
341 342
              }
          </div>
Maxym Borodenko's avatar
Maxym Borodenko committed
343 344 345 346
        );
    }
}

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
347
interface IStringArrFilter extends React.ClassAttributes<any>, WithStyles, WithTranslation {
Maxym Borodenko's avatar
Maxym Borodenko committed
348 349 350
    name: string;
    placeholder?: string;
    label?: string;
351
    options?: { [key: string]: any };
352
    suggestionTranslations: any;
353
    terms?: { [key: string]: any };
354 355 356
    valueField?: string;
    labelField?: string;
    byKey?: boolean;
357
    validate: any;
358
    indented?: boolean;
Maxym Borodenko's avatar
Maxym Borodenko committed
359 360 361 362 363 364
}

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


    public render() {
365
        const { name, label, placeholder, options, indented, terms, valueField, labelField, byKey, suggestionTranslations, classes, t, validate = [] } = this.props;
Maxym Borodenko's avatar
Maxym Borodenko committed
366 367
        return (
            <div>
368 369
                <Fields
                    names={ [`${name}`, `NOT.${name}`] }
Maxym Borodenko's avatar
Maxym Borodenko committed
370
                    component={ InternalStringArrField }
Viacheslav Pavlov's avatar
i18n  
Viacheslav Pavlov committed
371
                    label={ t(label) }
Maxym Borodenko's avatar
Maxym Borodenko committed
372 373
                    placeholder={ placeholder }
                    options={ options }
374 375 376 377
                    terms={ terms }
                    valueField={ valueField }
                    labelField={ labelField }
                    byKey={ byKey }
378
                    classes={ classes }
379
                    indented={ indented }
380
                    suggestionTranslations={ suggestionTranslations }
381
                    t={ t }
382
                    validators={ [...validate] }
Maxym Borodenko's avatar
Maxym Borodenko committed
383 384 385 386 387 388
                />
            </div>
        );
    }
}

389 390 391 392 393 394

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


Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
395
export default connect(null, mapDispatchToProps)(withTranslation()(StringArrFilter));