AutocompleteFilter.tsx 13.9 KB
Newer Older
Maxym Borodenko's avatar
Maxym Borodenko committed
1
import * as React from 'react';
Matija Obreza's avatar
Matija Obreza committed
2
import { Fields, Field } from 'redux-form';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
3
import { WithTranslation, withTranslation } from 'react-i18next';
Maxym Borodenko's avatar
Maxym Borodenko committed
4 5 6 7 8 9 10 11
import * as _ from 'lodash';

import Number from 'ui/common/Number';
import { Properties, PropertiesItem } from 'ui/common/Properties';
import LabelValue from 'model/LabelValue';
import AccessionFilter from 'model/accession/AccessionFilter';
import MaterialAutosuggest from 'ui/common/material-autosuggest';
import { debounce } from 'debounce';
12
import { AddCircle as Add, RemoveCircle as Remove, HighlightOff as Clear} from '@material-ui/icons';
Maxym Borodenko's avatar
Maxym Borodenko committed
13 14 15 16

interface IStringListProps extends React.ClassAttributes<any> {
  input: any;
  notInput: any;
17
  removeByValue: (index: number, isNot: boolean) => void;
Maxym Borodenko's avatar
Maxym Borodenko committed
18 19 20 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
}

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

    );
  }
}

interface IAutocompleteFilterInternal extends React.ClassAttributes<any> {
  field: string;
  filterCode: string;
  input: any;
115
  validators: any;
Maxym Borodenko's avatar
Maxym Borodenko committed
116 117
  placeholder?: string;
  label?: string;
118
  options?: { [key: string]: any };
Maxym Borodenko's avatar
Maxym Borodenko committed
119 120 121
  t: any;
  addToNotList: (item: any) => void;
  notList: any[];
122
  autocomplete: (field: string, term: string, filter: string | AccessionFilter) => Promise<LabelValue<string>[]>;
123
  InputLabelProps?: any;
Maxym Borodenko's avatar
Maxym Borodenko committed
124 125 126 127 128 129 130 131 132 133
}

class AutocompleteFilterInternal extends React.Component<IAutocompleteFilterInternal & any, any> {

  private constructor(props, context) {
    super(props, context);
    const notValues = _.get(props, `${props.names[0]}.input.value`);
    const values = _.get(props, `${props.names[1]}.input.value`);

    this.state = {
Maxym Borodenko's avatar
Maxym Borodenko committed
134
      excludedValues: [],
Maxym Borodenko's avatar
Maxym Borodenko committed
135 136 137 138 139 140 141 142 143 144 145 146 147
      values,
      notValues,
      text: '',
      autocompleteObj: [],
    };
  }

  public componentWillMount() {
    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({
148
      excludedValues: [...value, ...notValue],
Maxym Borodenko's avatar
Maxym Borodenko committed
149 150 151 152 153 154 155 156 157 158 159 160
      values: [ ...value ],
      notValues: [ ...notValue ],
      text: '',
    });
  }

  public componentWillReceiveProps(nextProps) {
    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`);

    this.setState({
161
      excludedValues: [...value, ...notValue],
Maxym Borodenko's avatar
Maxym Borodenko committed
162 163
      values: [ ...value ],
      notValues: [...notValue],
164
      // text: '',
Maxym Borodenko's avatar
Maxym Borodenko committed
165 166 167 168 169
    });
  }

  private maybeAdd = (...newValues: string[]) => {
    const values = [ ...this.state.values ];
170
    const notValues = [ ...this.state.notValues ];
171 172 173 174
    const excludedValues = [ ];
    if (this.state.excludedValues) {
      excludedValues.push(...this.state.excludedValues);
    }
Maxym Borodenko's avatar
Maxym Borodenko committed
175 176 177

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

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

197 198 199
  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
200 201 202 203 204 205 206

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

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

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

    const newValues = this.maybeRemove(isNot, value);
Maxym Borodenko's avatar
Maxym Borodenko committed
229 230 231 232 233 234 235 236
    input.onChange(newValues);
  }

  private onInputChange = (e) => {
    if (e.target && typeof e.target.value === 'string') {
      if (e.target && e.target.value) {
        if (e.target.value !== this.state.text) {
          if (e.target.value.length >= 3) {
237 238 239
            this.setState({autocompleteObj: [], text: e.target.value}, this.autocomplete(e.target.value));
          } else {
            this.setState({autocompleteObj: [], text: e.target.value});
Maxym Borodenko's avatar
Maxym Borodenko committed
240 241 242 243 244 245
          }
        }
      }
    }
  }

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
246 247 248 249 250 251 252 253
  private autocomplete = debounce((text) => {
    const { autocomplete, field, filterCode } = this.props;
    autocomplete(field, text, filterCode)
      .then((autocompleteObj) => {
        this.setState({...this.state, autocompleteObj});
      });
  }, 1000);

254 255 256
  private dataPasted = (e) => {
    const data = e.clipboardData.getData('text/plain');
    const dataArr = data.split(/[,\n;]/).map((item) => item.trim());
257
    const { validators } = this.props;
258
    if (dataArr && dataArr.length > 1) {
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
      e.preventDefault();
      let values = this.maybeAdd(...dataArr);
      const {input} = _.get(this.props, this.props.names[0]);

      if (validators && validators.length !== 0) {
        const errors = [];
        validators.forEach((validator) => {
          values = values.filter((value) => {
            if (validator(value)) {
              errors.push(validator(value));
              return false
            } else {
              return true;
            }
          });
        });

        if (errors.length !== 0) {
          if (input.value && input.value.length !== 0) {
            const unique = [...new Set([...input.value, ...values])];
            input.onChange('');
            this.setState({
              text: '',
              values: unique,
              excludedValues: unique,
            });
            input.onChange(unique);
            return;
          }
          if (values.length === 0) {
            input.onChange('');
            return;
          }
        }
293
      }
294 295
      input.onChange(values);
    }
296 297
  }

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
298 299 300 301 302 303
  private onKeyPress = (e, data, autocompleteInput, that) => {
    const { input } = _.get(this.props, this.props.names[0]);
    const {text} = this.state;

    if (e.key === 'Enter' && text) {
      e.preventDefault();
304 305 306
      if (!that.props.meta || !that.props.meta.error) {
        input.onChange(this.maybeAdd(this.state.text));
      }
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
307 308 309 310
      this.setState({text: ''});
    }
  }

311
  private handleOnBlur = (value, error) => {
312
    const { input } = _.get(this.props, this.props.names[0]);
313 314 315 316

    if (!error) {
      input.onChange(this.maybeAdd(value));
    }
317 318 319
    this.setState({text: '', autocompleteObj: []});
  }

Maxym Borodenko's avatar
Maxym Borodenko committed
320
  private onSuggestionSelected = (e, data, autocompleteInput, that) => {
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
321
    e.preventDefault();
Maxym Borodenko's avatar
Maxym Borodenko committed
322 323 324 325 326 327
    autocompleteInput.onChange.call(that, '');
    const { input } = _.get(this.props, this.props.names[0]);
    input.onChange(this.maybeAdd(data.suggestion.value));
  }

  public render() {
328
    const { placeholder, label, t, names, terms, classes, options, InputLabelProps, validators } = this.props;
Maxym Borodenko's avatar
Maxym Borodenko committed
329 330
    const { input } = _.get(this.props, names[0]);
    const { input: notInput } = _.get(this.props, names[1]);
331
    const {  values, notValues, excludedValues = [] } = this.state;
Maxym Borodenko's avatar
Maxym Borodenko committed
332 333 334 335

    return (
      <div>
          <Field
Matija Obreza's avatar
Matija Obreza committed
336
            name={ `auto-${names[0]}` }
Maxym Borodenko's avatar
Maxym Borodenko committed
337 338 339
            component={ MaterialAutosuggest }
            label={ label }
            placeholder={ placeholder }
340
            onPaste={ this.dataPasted }
341
            handleOnBlur={ this.handleOnBlur }
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
342
            onChange={ this.onInputChange }
Maxym Borodenko's avatar
Maxym Borodenko committed
343 344
            suggestions={ this.state.autocompleteObj }
            onSuggestionSelected={ this.onSuggestionSelected }
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
345
            onKeyPress={ this.onKeyPress }
Maxym Borodenko's avatar
Maxym Borodenko committed
346 347
            suggestionLabel="label"
            className="full-width"
348
            InputLabelProps={ InputLabelProps }
349
            validate={ validators }
Maxym Borodenko's avatar
Maxym Borodenko committed
350 351
           />
        { ((values && values.length > 0) || (notValues && notValues.length > 0)) &&
352
          <StringList input={ input } notInput={ notInput } removeByValue={ this.removeByValue } />
Maxym Borodenko's avatar
Maxym Borodenko committed
353 354 355 356
        }
        { terms &&
          <Properties>
            <h5 className="pl-10 pt-1rem mb-10">{ t('common:f.suggestedFilters') }</h5>
357
            { terms && Array.from(terms).slice(0, 10).filter(([key, value]) => !['missing', ...excludedValues].includes(key)).map(([key, value]) => (
358
              <PropertiesItem suggestion key={ key } title={ options && options[key] || key }  onClick={ () => input.onChange(this.maybeAdd(key)) } classes={ {...classes, propertiesRow: 'cursor-pointer'} }>
Maxym Borodenko's avatar
Maxym Borodenko committed
359 360 361 362 363 364 365 366 367 368 369 370
                  <span className="float-right">
                    <Number value={ value }/>
                  </span>
              </PropertiesItem>
            )) }
          </Properties>
        }
      </div>
    );
  }
}

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
371
interface IAutocompleteFilter extends React.ClassAttributes<any>, WithTranslation {
Maxym Borodenko's avatar
Maxym Borodenko committed
372 373 374
  name: string;
  placeholder?: string;
  label?: string;
375 376
  options?: { [key: string]: any };
  terms?: { [key: string]: any };
Maxym Borodenko's avatar
Maxym Borodenko committed
377
  byKey?: boolean;
378
  autocomplete: (field: string, term: string, filter: string | AccessionFilter) => Promise<LabelValue<string>[]>;
Maxym Borodenko's avatar
Maxym Borodenko committed
379
  filterCode: string;
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
380 381
  classes?: any;
  validate?: any;
Maxym Borodenko's avatar
Maxym Borodenko committed
382
  t: any;
383
  InputLabelProps?: any;
Maxym Borodenko's avatar
Maxym Borodenko committed
384 385
}

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
386
class AutocompleteFilter extends React.Component<IAutocompleteFilter , any> {
Maxym Borodenko's avatar
Maxym Borodenko committed
387 388

  public render() {
389
    const { name, label, placeholder, options, terms, byKey, autocomplete, filterCode, classes, t, InputLabelProps, validate } = this.props;
Maxym Borodenko's avatar
Maxym Borodenko committed
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
    return (
      <div>
        <Fields
          names={ [`${name}`, `NOT.${name}`] }
          field={ name }
          component={ AutocompleteFilterInternal }
          label={ t(label) }
          placeholder={ placeholder }
          options={ options }
          terms={ terms }
          autocomplete={ autocomplete }
          filterCode={ filterCode }
          byKey={ byKey }
          classes={ classes }
          t={ t }
405
          InputLabelProps={ InputLabelProps }
406
          validators={ validate }
Maxym Borodenko's avatar
Maxym Borodenko committed
407 408 409 410 411 412
        />
      </div>
    );
  }
}

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
413
export default withTranslation()(AutocompleteFilter);