18560: Improves the property form's autocomplete feature.
[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={(s: PropFieldSuggestion) => (s.description || s.label)}
50             onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
51             onBlur={() => {
52                 // Case-insensitive search for the key in the vocabulary
53                 const foundKeyID = getTagKeyID(props.input.value, vocabulary);
54                 if (foundKeyID !== '') {
55                     props.input.value = getTagKeyLabel(foundKeyID, vocabulary);
56                 }
57                 handleBlur(PROPERTY_KEY_FIELD_ID, data.form, props.meta, props.input, foundKeyID)();
58             }}
59             onChange={(e: ChangeEvent<HTMLInputElement>) => {
60                 const newValue = e.currentTarget.value;
61                 handleChange(data.form, props.input, props.meta, newValue);
62             }}
63         />
64     )} />;
65
66 const getValidation = memoize(
67     (vocabulary: Vocabulary) =>
68         vocabulary.strict_tags
69             ? [...TAG_KEY_VALIDATION, matchTags(vocabulary)]
70             : TAG_KEY_VALIDATION);
71
72 const matchTags = (vocabulary: Vocabulary) =>
73     (value: string) =>
74         getTags(vocabulary).find(tag => tag.label === value)
75             ? undefined
76             : 'Incorrect key';
77
78 const getSuggestions = (value: string, vocabulary: Vocabulary): PropFieldSuggestion[] => {
79     const re = new RegExp(escapeRegExp(value), "i");
80     return getPreferredTags(vocabulary, value !== '').filter(
81         tag => re.test((tag.description || tag.label)) && tag.label !== value);
82 };
83
84 const handleChange = (
85     formName: string,
86     { onChange }: WrappedFieldInputProps,
87     { dispatch }: WrappedFieldMetaProps,
88     value: string) => {
89         // Properties' values are dependant on the keys, if any value is
90         // pre-existant, a change on the property key should mean that the
91         // previous value is invalid, so we better reset the whole form before
92         // setting the new tag key.
93         dispatch(reset(formName));
94
95         onChange(value);
96         dispatch(change(formName, PROPERTY_KEY_FIELD_NAME, value));
97     };