18560: Restricts synonyms display to the ones matching the user input.
[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 }: VocabularyProp & ValidationProp) =>
34         <span data-cy='property-field-key'>
35         <Field
36             name={PROPERTY_KEY_FIELD_NAME}
37             component={PropertyKeyInput}
38             vocabulary={vocabulary}
39             validate={skipValidation ? undefined : getValidation(vocabulary)} />
40         </span>
41 );
42
43 const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) =>
44     <FormName children={data => (
45         <Autocomplete
46             {...buildProps(props)}
47             label='Key'
48             suggestions={getSuggestions(props.input.value, vocabulary)}
49             renderSuggestion={
50                 (s: PropFieldSuggestion) => s.synonyms && s.synonyms.length > 0
51                     ? `${s.label} (${s.synonyms.join('; ')})`
52                     : s.label
53             }
54             onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
55             onBlur={() => {
56                 // Case-insensitive search for the key in the vocabulary
57                 const foundKeyID = getTagKeyID(props.input.value, vocabulary);
58                 if (foundKeyID !== '') {
59                     props.input.value = getTagKeyLabel(foundKeyID, vocabulary);
60                 }
61                 handleBlur(PROPERTY_KEY_FIELD_ID, data.form, props.meta, props.input, foundKeyID)();
62             }}
63             onChange={(e: ChangeEvent<HTMLInputElement>) => {
64                 const newValue = e.currentTarget.value;
65                 handleChange(data.form, props.input, props.meta, newValue);
66             }}
67         />
68     )} />;
69
70 const getValidation = memoize(
71     (vocabulary: Vocabulary) =>
72         vocabulary.strict_tags
73             ? [...TAG_KEY_VALIDATION, matchTags(vocabulary)]
74             : TAG_KEY_VALIDATION);
75
76 const matchTags = (vocabulary: Vocabulary) =>
77     (value: string) =>
78         getTags(vocabulary).find(tag => tag.label === value)
79             ? undefined
80             : 'Incorrect key';
81
82 const getSuggestions = (value: string, vocabulary: Vocabulary): PropFieldSuggestion[] => {
83     const re = new RegExp(escapeRegExp(value), "i");
84     return getPreferredTags(vocabulary, value).filter(
85         tag => (tag.label !== value && re.test(tag.label)) ||
86             (tag.synonyms && tag.synonyms.some(s => re.test(s))));
87 };
88
89 const handleChange = (
90     formName: string,
91     { onChange }: WrappedFieldInputProps,
92     { dispatch }: WrappedFieldMetaProps,
93     value: string) => {
94         // Properties' values are dependant on the keys, if any value is
95         // pre-existant, a change on the property key should mean that the
96         // previous value is invalid, so we better reset the whole form before
97         // setting the new tag key.
98         dispatch(reset(formName));
99
100         onChange(value);
101         dispatch(change(formName, PROPERTY_KEY_FIELD_NAME, value));
102     };