17266: Avoids adding a property value without a key.
[arvados.git] / src / views-components / search-bar / search-bar-advanced-properties-view.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import * as React from 'react';
6 import { Dispatch, compose } from 'redux';
7 import { connect } from 'react-redux';
8 import { InjectedFormProps, formValueSelector } from 'redux-form';
9 import { Grid, withStyles, StyleRulesCallback, WithStyles, Button } from '@material-ui/core';
10 import { RootState } from '~/store/store';
11 import {
12     SEARCH_BAR_ADVANCED_FORM_NAME,
13     changeAdvancedFormProperty,
14     resetAdvancedFormProperty
15 } from '~/store/search-bar/search-bar-actions';
16 import { PropertyValue } from '~/models/search-bar';
17 import { ArvadosTheme } from '~/common/custom-theme';
18 import { SearchBarKeyField, SearchBarValueField } from '~/views-components/form-fields/search-bar-form-fields';
19 import { Chips } from '~/components/chips/chips';
20 import { formatPropertyValue } from "~/common/formatters";
21 import { Vocabulary } from '~/models/vocabulary';
22 import { connectVocabulary } from '../resource-properties-form/property-field-common';
23 import * as _ from 'lodash';
24
25 type CssRules = 'label' | 'button';
26
27 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
28     label: {
29         color: theme.palette.grey["500"],
30         fontSize: '0.8125rem',
31         alignSelf: 'center'
32     },
33     button: {
34         boxShadow: 'none'
35     }
36 });
37
38 interface SearchBarAdvancedPropertiesViewDataProps {
39     submitting: boolean;
40     invalid: boolean;
41     pristine: boolean;
42     propertyValues: PropertyValue;
43     fields: PropertyValue[];
44     vocabulary: Vocabulary;
45 }
46
47 interface SearchBarAdvancedPropertiesViewActionProps {
48     setProps: () => void;
49     addProp: (propertyValues: PropertyValue, properties: PropertyValue[]) => void;
50     getAllFields: (propertyValues: PropertyValue[]) => PropertyValue[] | [];
51 }
52
53 type SearchBarAdvancedPropertiesViewProps = SearchBarAdvancedPropertiesViewDataProps
54     & SearchBarAdvancedPropertiesViewActionProps
55     & InjectedFormProps & WithStyles<CssRules>;
56
57 const selector = formValueSelector(SEARCH_BAR_ADVANCED_FORM_NAME);
58 const mapStateToProps = (state: RootState) => {
59     return {
60         propertyValues: selector(state, 'key', 'value', 'keyID', 'valueID')
61     };
62 };
63
64 const mapDispatchToProps = (dispatch: Dispatch) => ({
65     setProps: (propertyValues: PropertyValue[]) => {
66         dispatch<any>(changeAdvancedFormProperty('properties', propertyValues));
67     },
68     addProp: (propertyValue: PropertyValue, properties: PropertyValue[]) => {
69         // Remove potential duplicates
70         properties = properties.filter(x => ! _.isEqual(
71             {
72                 key: x.keyID || x.key,
73                 value: x.valueID || x.value
74             }, {
75                 key: propertyValue.keyID || propertyValue.key,
76                 value: propertyValue.valueID || propertyValue.value
77             }));
78         dispatch<any>(changeAdvancedFormProperty(
79             'properties',
80             [...properties, propertyValue]
81         ));
82         dispatch<any>(resetAdvancedFormProperty('key'));
83         dispatch<any>(resetAdvancedFormProperty('value'));
84         dispatch<any>(resetAdvancedFormProperty('keyID'));
85         dispatch<any>(resetAdvancedFormProperty('valueID'));
86     },
87     getAllFields: (fields: any) => {
88         return fields.getAll() || [];
89     }
90 });
91
92 export const SearchBarAdvancedPropertiesView = compose(
93     connectVocabulary,
94     connect(mapStateToProps, mapDispatchToProps))(
95     withStyles(styles)(
96         ({ classes, fields, propertyValues, setProps, addProp, getAllFields, vocabulary }: SearchBarAdvancedPropertiesViewProps) =>
97             <Grid container item xs={12} spacing={16}>
98                 <Grid item xs={2} className={classes.label}>Properties</Grid>
99                 <Grid item xs={4}>
100                     <SearchBarKeyField />
101                 </Grid>
102                 <Grid item xs={4}>
103                     <SearchBarValueField />
104                 </Grid>
105                 <Grid container item xs={2} justify='flex-end' alignItems="center">
106                     <Button className={classes.button} onClick={() => addProp(propertyValues, getAllFields(fields))}
107                         color="primary"
108                         size='small'
109                         variant="contained"
110                         disabled={!Boolean(propertyValues.key && propertyValues.value)}>
111                         Add
112                     </Button>
113                 </Grid>
114                 <Grid item xs={2} />
115                 <Grid container item xs={10} spacing={8}>
116                     <Chips values={getAllFields(fields)}
117                         deletable
118                         onChange={setProps}
119                         getLabel={(field: PropertyValue) => formatPropertyValue(field, vocabulary)} />
120                 </Grid>
121             </Grid>
122     )
123 );