18203: added support for adding multi properties at once
[arvados-workbench2.git] / src / views-components / resource-properties-form / property-key-field.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { WrappedFieldProps, Field, FormName, reset, change, WrappedFieldInputProps, WrappedFieldMetaProps } from 'redux-form';
7 import { memoize } from 'lodash';
8 import { Autocomplete } from 'components/autocomplete/autocomplete';
9 import {
10     Vocabulary,
11     getTags,
12     getTagKeyID,
13     getTagKeyLabel,
14     getPreferredTags,
15     PropFieldSuggestion
16 } from 'models/vocabulary';
17 import {
18     handleSelect,
19     handleBlur,
20     connectVocabulary,
21     VocabularyProp,
22     ValidationProp,
23     buildProps
24 } from 'views-components/resource-properties-form/property-field-common';
25 import { TAG_KEY_VALIDATION } from 'validators/validators';
26 import { escapeRegExp } from 'common/regexp';
27 import { ChangeEvent } from 'react';
28
29 export const PROPERTY_KEY_FIELD_NAME = 'key';
30 export const PROPERTY_KEY_FIELD_ID = 'keyID';
31
32 export const PropertyKeyField = connectVocabulary(
33     ({ vocabulary, skipValidation, clearPropertyKeyOnSelect }: VocabularyProp & ValidationProp) =>
34         <span data-cy='property-field-key'>
35         <Field
36             clearPropertyKeyOnSelect
37             name={PROPERTY_KEY_FIELD_NAME}
38             component={PropertyKeyInput}
39             vocabulary={vocabulary}
40             validate={skipValidation ? undefined : getValidation(vocabulary)} />
41         </span>
42 );
43
44 const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp & { clearPropertyKeyOnSelect?: boolean }) =>
45     <FormName children={data => (
46         <Autocomplete
47             {...buildProps(props)}
48             label='Key'
49             suggestions={getSuggestions(props.input.value, vocabulary)}
50             renderSuggestion={
51                 (s: PropFieldSuggestion) => s.synonyms && s.synonyms.length > 0
52                     ? `${s.label} (${s.synonyms.join('; ')})`
53                     : s.label
54             }
55             onFocus={() => {
56                 if (props.clearPropertyKeyOnSelect && props.input.value) {
57                     props.meta.dispatch(reset(props.meta.form));
58                 }
59             }}
60             onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
61             onBlur={() => {
62                 // Case-insensitive search for the key in the vocabulary
63                 const foundKeyID = getTagKeyID(props.input.value, vocabulary);
64                 if (foundKeyID !== '') {
65                     props.input.value = getTagKeyLabel(foundKeyID, vocabulary);
66                 }
67                 handleBlur(PROPERTY_KEY_FIELD_ID, data.form, props.meta, props.input, foundKeyID)();
68             }}
69             onChange={(e: ChangeEvent<HTMLInputElement>) => {
70                 const newValue = e.currentTarget.value;
71                 handleChange(data.form, props.input, props.meta, newValue);
72             }}
73         />
74     )} />;
75
76 const getValidation = memoize(
77     (vocabulary: Vocabulary) =>
78         vocabulary.strict_tags
79             ? [...TAG_KEY_VALIDATION, matchTags(vocabulary)]
80             : TAG_KEY_VALIDATION);
81
82 const matchTags = (vocabulary: Vocabulary) =>
83     (value: string) =>
84         getTags(vocabulary).find(tag => tag.label === value)
85             ? undefined
86             : 'Incorrect key';
87
88 const getSuggestions = (value: string, vocabulary: Vocabulary): PropFieldSuggestion[] => {
89     const re = new RegExp(escapeRegExp(value), "i");
90     return getPreferredTags(vocabulary, value).filter(
91         tag => (tag.label !== value && re.test(tag.label)) ||
92             (tag.synonyms && tag.synonyms.some(s => re.test(s))));
93 };
94
95 const handleChange = (
96     formName: string,
97     { onChange }: WrappedFieldInputProps,
98     { dispatch }: WrappedFieldMetaProps,
99     value: string) => {
100         // Properties' values are dependant on the keys, if any value is
101         // pre-existant, a change on the property key should mean that the
102         // previous value is invalid, so we better reset the whole form before
103         // setting the new tag key.
104         dispatch(reset(formName));
105
106         onChange(value);
107         dispatch(change(formName, PROPERTY_KEY_FIELD_NAME, value));
108     };