PrettyFilters.tsx 8.98 KB
Newer Older
Maxym Borodenko's avatar
Maxym Borodenko committed
1
import * as React from 'react';
2
import { bindActionCreators } from 'redux';
Maxym Borodenko's avatar
Maxym Borodenko committed
3
import { connect } from 'react-redux';
4
import { withStyles } from '@material-ui/core/styles';
Maxym Borodenko's avatar
Maxym Borodenko committed
5
import { translate } from 'react-i18next';
6
7
import { showSnackbar } from 'actions/snackbar';

Matija Obreza's avatar
Matija Obreza committed
8
9
import Chip from '@material-ui/core/Chip';
import Paper from '@material-ui/core/Paper';
Maxym Borodenko's avatar
Maxym Borodenko committed
10
11
import * as flattenjs from 'flattenjs';
import * as _ from 'lodash';
Matija Obreza's avatar
Subsets    
Matija Obreza committed
12
import { cleanFilters } from 'utilities';
13
import Accession from 'model/accession/Accession';
Maxym Borodenko's avatar
Maxym Borodenko committed
14
import {User} from 'model/user/User';
15
import MaterialRequest from 'model/request/MaterialRequest';
16
import PrettyDate from 'ui/common/time/PrettyDate';
Maxym Borodenko's avatar
Maxym Borodenko committed
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

/**
 * filterObj example: {crop: ["bannana", "apple"], description: {contains: "de"}, title: {contains: "ti"}}
 * If we want to replace the code with a title for showing, we need lookups object.
 * It contains pairs key - value:
 *      where key - should be equal to filterObj key which values we want to substitute.
 *      where value - object of codes and titles we want to show.
 * lookups example: {
 *  crop: {
 *      banana: 'Banana',
 *      apple: 'Apple',
 *      ...
 *    },
 *    ...
 *  }
 */
33
34
35
36
interface IPrettyFiltersProps {
  classes?: any;
  t?: any;

37
  onSubmit: (newFilter) => any;
38
  showSnackbar: (snack: string) => any;
39
  filterObj: object;
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
40
  labels: Map<string, string>;
41
42
  lookups: object;
  prefix: string;
Oleksii Savran's avatar
Oleksii Savran committed
43
44
  displayName?: string;
  amount?: number;
Maxym Borodenko's avatar
Maxym Borodenko committed
45
46
47
}

const styles = (theme) => ({
Oleksii Savran's avatar
Oleksii Savran committed
48
  /*tslint:disable*/
49
  root: {
Oleksii Savran's avatar
Oleksii Savran committed
50
51
52
53
54
    display: 'flex',
    flexDirection: 'row' as 'row',
  },
  withPagination: {
    padding: '0 202px 0 12px',
Oleksii Savran's avatar
Oleksii Savran committed
55
56
57
58
59
60
    'html[dir="rtl"] &' : {
      padding: '0 12px 0 202px',
      [theme.breakpoints.down('sm')]: {
        padding: '0 12px',
      },
    },
Oleksii Savran's avatar
Oleksii Savran committed
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
    [theme.breakpoints.down('sm')]: {
      padding: '0 12px',
      justifyContent: 'space-between' as 'space-between',
    },
  },
  chip: {
    margin: theme.spacing.unit / 2,
  },
  text: {
    fontWeight: 'bold' as 'bold',
    alignSelf: 'center' as 'center',
    paddingRight: '10px',
    lineHeight: '48px',
    width: '180px',
    flexShrink: 0,
    [theme.breakpoints.down('sm')]: {
      width: 'auto' as 'auto',
    },
  },
  chipArea: {
    flexGrow: 2,
82
83
84
85
86
    display: 'flex',
    justifyContent: 'center' as 'center',
    flexWrap: 'wrap' as 'wrap',
    padding: theme.spacing.unit / 2,
  },
Oleksii Savran's avatar
Oleksii Savran committed
87
88
89
90
91
  chipAreaWithPagination: {
    [theme.breakpoints.down('sm')]: {
      flexGrow: 1,
      justifyContent: 'flex-end' as 'flex-end',
    },
92
  },
Oleksii Savran's avatar
Oleksii Savran committed
93
  /*tslint:enable*/
Maxym Borodenko's avatar
Maxym Borodenko committed
94
95
96
});

// Following keywords are skipped for showing
97
const keywordsToSkip = ['eq', 'contains', 'sw'];
Maxym Borodenko's avatar
Maxym Borodenko committed
98
99
100
101
102
103
104
105

/**
 * Remove value by path from filterObj and return updated filterObj.
 * @param filterObj - IPrettyFiltersProps.filterObj.
 * @param path - path to value to remove.
 * @return IPrettyFiltersProps.filterObj.
 */
function handleFilterObj(filterObj, path) {
106
107
108
109
110
111
112
  // clone original filter in order to not mutate it
  const clone = _.cloneDeep(filterObj);
  // array element
  if (path.endsWith(']')) {
    const lastIndex = path.lastIndexOf('[');
    const arrayPath = path.substring(0, lastIndex);
    const index = parseInt(path.substring(lastIndex).replace(/[\[\]']+/g, ''), 10);
Maxym Borodenko's avatar
Maxym Borodenko committed
113

114
    _.get(clone, arrayPath).splice(index, 1);
Maxym Borodenko's avatar
Maxym Borodenko committed
115

116
117
    return clone;
  }
Maxym Borodenko's avatar
Maxym Borodenko committed
118

119
  return _.omit(clone, [path]);
Maxym Borodenko's avatar
Maxym Borodenko committed
120
121
}

122
123
function translatePath(t, prefix: string, prettyPath: string) {
  const isNot = prettyPath.startsWith('NOT.');
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
124
125
  const isNull = prettyPath.startsWith('NULL');
  const isNotNull = prettyPath.startsWith('NOTNULL');
126
127
  const isLe = prettyPath.endsWith('.le');
  const isGe = prettyPath.endsWith('.ge');
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
128
129
130
131
132
133
134
135

  if (isNull) {
    return `${t('common:f.dataNotProvided')}:`;
  }
  if (isNotNull) {
    return `${t('common:f.dataExists')}:`;
  }

136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
  // console.log(`translating not=${isNot} le=${isLe} ge=${isGe} ${prefix}.${prettyPath}`);
  if (isNot) {
    prettyPath = prettyPath.replace(/^NOT\./, '');
  }
  if (isLe) {
    prettyPath = prettyPath.replace(/\.le$/, '');
  }
  if (isGe) {
    prettyPath = prettyPath.replace(/\.ge$/, '');
  }

  // console.log(`Translating ${[ `public.prettyF.${prefix}.${prettyPath}`, `${prefix}.model.${prettyPath}` ]}`);
  let trans = t([ `public.prettyF.${prefix}.${prettyPath}`, `${prefix}.model.${prettyPath}` ], prettyPath);

  if (isGe) {
    trans = t('public.prettyF.ge', { what: trans });
  } else if (isLe) {
    trans = t('public.prettyF.le', { what: trans });
  } else {
    trans = `${trans}:`;
  }

  if (isNot) {
    return t('public.prettyF.NOT', { what: trans });
  } else {
    return trans;
  }
}

Maxym Borodenko's avatar
Maxym Borodenko committed
165
166
167
168
169
170
171
172
173
174
175
176
177
/**
 * Return pretty name.
 * For example we have path crop[0] and value banana, will return 'crop Banana'.
 * Another examples:
 *       'description.contains': 'de' -> 'description de'
 *       'title.eq': 'ti' -> 'title ti'
 * @param path - path to filterObj value ie crop[0]
 * @param value - filterObj value ie BN27.
 * @param lookups - IPrettyFiltersProps.filterObj.
 * @param prefix - for determination which page filter localize
 * @param t - i18n translation function
 * @return IPrettyFiltersProps.filterObj.
 */
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
178
function getLabelName(path, value, lookups, prefix, t, labels) {
179
180
181
182
183
184
185
186
  const lastKey = path.replace(/\[(.+?)\]/g, '').split('.').pop();

  let name = value;
  if (lookups && lookups[lastKey]) {
    name = lookups[lastKey][value] || value;
  }

  const prettyPath: string = path
187
  // remove array indexes square brackets [0], [1]... from path.
188
189
190
191
192
193
194
195
    .replace(/\[(.+?)\]/g, '')
    // split path title.eq -> ['title', 'eq']
    .split('.')
    // skip for showing keywords such as 'contains', 'eq'...
    .filter((e) => keywordsToSkip.indexOf(e) === -1)
    // join
    .join('.');

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
196

197
198
  if (typeof name === 'boolean') {
    if (name) {
199
      return t([ `public.prettyF.${prefix}.${prettyPath}`, `public.${prefix}.${prettyPath}` ]);
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
200
    }
201
    return t('public.prettyF.NOT', { what: t([ `public.prettyF.${prefix}.${prettyPath.replace(/^NOT\./, '')}`, `public.${prefix}.${prettyPath.replace(/^NOT\./, '')}` ]) });
202
  }
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
203

204
  const translatedPrettyPath = translatePath(t, prefix, prettyPath);
205
  // console.log(`${prettyPath} = ${translatedPrettyPath}`);
206

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
207
208
209
  if (labels && !_.isEmpty(labels) && prettyPath.includes('uuid')) {
    name = labels.get(name);
  }
210
  const translatedPrettyName = t(`${name}`);
Maxym Borodenko's avatar
Maxym Borodenko committed
211

Oleksii Savran's avatar
Oleksii Savran committed
212
  if (prettyPath.includes('Date') || prettyPath.includes('lastLogin')) {
213
    return <span>{ translatedPrettyPath } <PrettyDate value={ new Date(name) } /></span>;
214
  }
215

216
  return `${translatedPrettyPath} ${translatedPrettyName}`;
Maxym Borodenko's avatar
Maxym Borodenko committed
217
218
219
220
}

class PrettyFilters extends React.Component<IPrettyFiltersProps, any> {

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
221
222
  private banWords = ['NULL', 'NOTNULL'];

223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
  constructor(props: IPrettyFiltersProps, context: any) {
    super(props, context);
    this.state = {
      chipData: flattenjs.convert(cleanFilters(props.filterObj)),
    };
  }

  protected handleDelete = (path) => () => {
    const { filterObj, onSubmit, showSnackbar } = this.props;
    const updated = handleFilterObj(filterObj, path);
    showSnackbar('Applying filters...');
    onSubmit(updated);
  }

  public componentWillReceiveProps(nextProps) {
    const chipData = flattenjs.convert(cleanFilters(nextProps.filterObj));
    if (!_.isEqual(chipData, this.state.chipData)) {
      this.setState({ chipData });
Maxym Borodenko's avatar
Maxym Borodenko committed
241
    }
242
243
244
  }

  public render() {
245
246
    const { classes, lookups, prefix, t, amount, displayName } = this.props;
    const labels = new Map(this.props.labels);
247
    const { chipData } = this.state;
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
248
    const dataArr = Object.getOwnPropertyNames(chipData).filter((key) => this.banWords.indexOf(chipData[key]) === -1);
Oleksii Savran's avatar
Oleksii Savran committed
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
    const withAmount = amount !== undefined && amount !== null;

    return (dataArr.length > 0 || withAmount) ? (
      <Paper square className={ `${classes.root} ${withAmount ? classes.withPagination : ''}` }>
        { withAmount &&
          <div className={ classes.text }>
            { t(amount < 10000 ? 'common:paginate.numberOfItems' : 'common:paginate.estimatedNumberOfItems',
              { count: amount, what: t(displayName || 'common:label.item', { count: amount }) }) }
          </div>
        }
        <div className={ `${classes.chipArea} ${withAmount ? classes.chipAreaWithPagination : ''}` }>
          { dataArr.map((key) => {
            return (
              <Chip
                key={ key }
                label={ getLabelName(key, chipData[key], lookups, prefix, t, labels) }
                onDelete={ this.handleDelete(key) }
                className={ classes.chip }
              />
            );
          }) }
        </div>
271
      </Paper>
Oleksii Savran's avatar
Oleksii Savran committed
272
    ) : null;
273
  }
Maxym Borodenko's avatar
Maxym Borodenko committed
274
275
276
}

const mapStateToProps = (state, ownProps) => ({
277
278
279
280
281
  lookups: {
    crop: [],
    category: '',
    sampStat: Accession.SAMPSTAT,
    storage: Accession.STORAGE,
Maxym Borodenko's avatar
Maxym Borodenko committed
282
    role: User.USERROLES,
283
284
285
    accessions: {
      true: 'Yes',
      false: 'No',
Maxym Borodenko's avatar
Maxym Borodenko committed
286
    },
Oleksii Savran's avatar
Oleksii Savran committed
287
288
289
290
291
292
    state: {
      ...MaterialRequest.STATE,
      REVIEWING: 'status.inReview',
      DRAFT: 'status.inProgress',
      PUBLISHED: 'status.published',
    },
293
  },
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
294
  labels: state.uuidDecoder.labels,
Maxym Borodenko's avatar
Maxym Borodenko committed
295
296
});

297
298
299
300
const mapDispatchToProps = (dispatch) => bindActionCreators({
  showSnackbar,
}, dispatch);

301
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(translate()(PrettyFilters)));