From 1e771e8083df71d11da85dae8bfd0a0cdfb1737f Mon Sep 17 00:00:00 2001 From: Lucas Di Pentima Date: Mon, 14 Feb 2022 17:56:42 -0300 Subject: [PATCH] 18560: Restricts synonyms display to the ones matching the user input. Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima --- src/models/vocabulary.test.ts | 36 +++++++++---------- src/models/vocabulary.ts | 34 ++++++++++-------- .../property-key-field.tsx | 11 ++++-- .../property-value-field.tsx | 11 ++++-- 4 files changed, 54 insertions(+), 38 deletions(-) diff --git a/src/models/vocabulary.test.ts b/src/models/vocabulary.test.ts index 582af0ec..761c785b 100644 --- a/src/models/vocabulary.test.ts +++ b/src/models/vocabulary.test.ts @@ -73,19 +73,19 @@ describe('Vocabulary', () => { const preferredTagKeys = Vocabulary.getPreferredTags(vocabulary); // Alphabetically ordered by label expect(preferredTagKeys).toEqual([ - {id: "IDKEYANIMALS", label: "Animal", description: "Animal"}, - {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT"}, - {id: "IDKEYSIZES", label: "Sizes", description: "Sizes"}, + {id: "IDKEYANIMALS", label: "Animal", synonyms: []}, + {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT", synonyms: []}, + {id: "IDKEYSIZES", label: "Sizes", synonyms: []}, ]); }); - it('returns the list of preferred tag keys with synonyms', () => { - const preferredTagKeys = Vocabulary.getPreferredTags(vocabulary, true); + it('returns the list of preferred tag keys with matching synonyms', () => { + const preferredTagKeys = Vocabulary.getPreferredTags(vocabulary, 'creat'); // Alphabetically ordered by label expect(preferredTagKeys).toEqual([ - {id: "IDKEYANIMALS", label: "Animal", description: "Animal (Creature, Beast)"}, - {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT"}, - {id: "IDKEYSIZES", label: "Sizes", description: "Sizes"}, + {id: "IDKEYANIMALS", label: "Animal", synonyms: ["Creature"]}, + {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT", synonyms: []}, + {id: "IDKEYSIZES", label: "Sizes", synonyms: []}, ]); }); @@ -108,21 +108,21 @@ describe('Vocabulary', () => { const preferredTagValues = Vocabulary.getPreferredTagValues('IDKEYSIZES', vocabulary); // Alphabetically ordered by label expect(preferredTagValues).toEqual([ - {id: "IDVALSIZES4", label: "IDVALSIZES4"}, - {id: "IDVALSIZES3", label: "Large", description: "Large"}, - {id: "IDVALSIZES2", label: "Medium", description: "Medium"}, - {id: "IDVALSIZES1", label: "Small", description: "Small"}, + {id: "IDVALSIZES4", label: "IDVALSIZES4", synonyms: []}, + {id: "IDVALSIZES3", label: "Large", synonyms: []}, + {id: "IDVALSIZES2", label: "Medium", synonyms: []}, + {id: "IDVALSIZES1", label: "Small", synonyms: []}, ]) }); - it('returns the preferred tag values with synonyms for a given key', () => { - const preferredTagValues = Vocabulary.getPreferredTagValues('IDKEYSIZES', vocabulary, true); + it('returns the preferred tag values with matching synonyms for a given key', () => { + const preferredTagValues = Vocabulary.getPreferredTagValues('IDKEYSIZES', vocabulary, 'litt'); // Alphabetically ordered by label expect(preferredTagValues).toEqual([ - {id: "IDVALSIZES4", label: "IDVALSIZES4"}, - {id: "IDVALSIZES3", label: "Large", description: "Large (L)"}, - {id: "IDVALSIZES2", label: "Medium", description: "Medium (M)"}, - {id: "IDVALSIZES1", label: "Small", description: "Small (S, Little)"}, + {id: "IDVALSIZES4", label: "IDVALSIZES4", synonyms: []}, + {id: "IDVALSIZES3", label: "Large", synonyms: []}, + {id: "IDVALSIZES2", label: "Medium", synonyms: []}, + {id: "IDVALSIZES1", label: "Small", synonyms: ["Little"]}, ]) }); diff --git a/src/models/vocabulary.ts b/src/models/vocabulary.ts index 55525c22..6c629059 100644 --- a/src/models/vocabulary.ts +++ b/src/models/vocabulary.ts @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 +import { escapeRegExp } from 'common/regexp'; import { isObject, has, every } from 'lodash/fp'; export interface Vocabulary { @@ -27,7 +28,7 @@ export interface Tag { export interface PropFieldSuggestion { id: string; label: string; - description?: string; + synonyms?: string[]; } const VOCABULARY_VALIDATORS = [ @@ -78,19 +79,22 @@ export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary): PropFiel : []; }; -export const getPreferredTagValues = (tagKeyID: string, vocabulary: Vocabulary, withSynonyms?: boolean): PropFieldSuggestion[] => { +export const getPreferredTagValues = (tagKeyID: string, vocabulary: Vocabulary, withMatch?: string): PropFieldSuggestion[] => { const tag = vocabulary.tags[tagKeyID]; + const regex = !!withMatch ? new RegExp(escapeRegExp(withMatch), 'i') : undefined; return tag && tag.values ? Object.keys(tag.values).map( tagValueID => tag.values![tagValueID].labels && tag.values![tagValueID].labels.length > 0 ? { "id": tagValueID, "label": tag.values![tagValueID].labels[0].label, - "description": tag.values![tagValueID].labels[0].label + ( - withSynonyms && tag.values![tagValueID].labels.length > 1 - ? ` (${tag.values![tagValueID].labels.slice(1).map(l => l.label).join(', ')})` - : '')} - : {"id": tagValueID, "label": tagValueID}) + "synonyms": !!withMatch && tag.values![tagValueID].labels.length > 1 + ? tag.values![tagValueID].labels.slice(1) + .filter(l => !!regex ? regex.test(l.label) : true) + .map(l => l.label) + : [] + } + : {"id": tagValueID, "label": tagValueID, "synonyms": []}) .sort(compare) : []; }; @@ -107,19 +111,21 @@ export const getTags = ({ tags }: Vocabulary): PropFieldSuggestion[] => { : []; }; -export const getPreferredTags = ({ tags }: Vocabulary, withSynonyms?: boolean): PropFieldSuggestion[] => { +export const getPreferredTags = ({ tags }: Vocabulary, withMatch?: string): PropFieldSuggestion[] => { + const regex = !!withMatch ? new RegExp(escapeRegExp(withMatch), 'i') : undefined; return tags && Object.keys(tags) ? Object.keys(tags).map( tagID => tags[tagID].labels && tags[tagID].labels.length > 0 ? { "id": tagID, "label": tags[tagID].labels[0].label, - "description": tags[tagID].labels[0].label + ( - withSynonyms && tags[tagID].labels.length > 1 - ? ` (${tags[tagID].labels.slice(1).map(lbl => lbl.label).join(', ')})` - : '' - )} - : {"id": tagID, "label": tagID}) + "synonyms": !!withMatch && tags[tagID].labels.length > 1 + ? tags[tagID].labels.slice(1) + .filter(l => !!regex ? regex.test(l.label) : true) + .map(lbl => lbl.label) + : [] + } + : {"id": tagID, "label": tagID, "synonyms": []}) .sort(compare) : []; }; diff --git a/src/views-components/resource-properties-form/property-key-field.tsx b/src/views-components/resource-properties-form/property-key-field.tsx index e566c708..0be4527a 100644 --- a/src/views-components/resource-properties-form/property-key-field.tsx +++ b/src/views-components/resource-properties-form/property-key-field.tsx @@ -46,7 +46,11 @@ const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & Vocabula {...buildProps(props)} label='Key' suggestions={getSuggestions(props.input.value, vocabulary)} - renderSuggestion={(s: PropFieldSuggestion) => (s.description || s.label)} + renderSuggestion={ + (s: PropFieldSuggestion) => s.synonyms && s.synonyms.length > 0 + ? `${s.label} (${s.synonyms.join('; ')})` + : s.label + } onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)} onBlur={() => { // Case-insensitive search for the key in the vocabulary @@ -77,8 +81,9 @@ const matchTags = (vocabulary: Vocabulary) => const getSuggestions = (value: string, vocabulary: Vocabulary): PropFieldSuggestion[] => { const re = new RegExp(escapeRegExp(value), "i"); - return getPreferredTags(vocabulary, value !== '').filter( - tag => re.test((tag.description || tag.label)) && tag.label !== value); + return getPreferredTags(vocabulary, value).filter( + tag => (tag.label !== value && re.test(tag.label)) || + (tag.synonyms && tag.synonyms.some(s => re.test(s)))); }; const handleChange = ( diff --git a/src/views-components/resource-properties-form/property-value-field.tsx b/src/views-components/resource-properties-form/property-value-field.tsx index 56b7fe05..b8e525bf 100644 --- a/src/views-components/resource-properties-form/property-value-field.tsx +++ b/src/views-components/resource-properties-form/property-value-field.tsx @@ -60,7 +60,11 @@ const PropertyValueInput = ({ vocabulary, propertyKeyId, propertyKeyName, ...pro label='Value' disabled={props.disabled} suggestions={getSuggestions(props.input.value, propertyKeyId, vocabulary)} - renderSuggestion={(s: PropFieldSuggestion) => (s.description || s.label)} + renderSuggestion={ + (s: PropFieldSuggestion) => s.synonyms && s.synonyms.length > 0 + ? `${s.label} (${s.synonyms.join('; ')})` + : s.label + } onSelect={handleSelect(PROPERTY_VALUE_FIELD_ID, data.form, props.input, props.meta)} onBlur={() => { // Case-insensitive search for the value in the vocabulary @@ -91,8 +95,9 @@ const matchTagValues = ({ vocabulary, propertyKeyId }: PropertyValueFieldProps) const getSuggestions = (value: string, tagName: string, vocabulary: Vocabulary) => { const re = new RegExp(escapeRegExp(value), "i"); - return getPreferredTagValues(tagName, vocabulary, value !== '').filter( - v => re.test((v.description || v.label)) && v.label !== value); + return getPreferredTagValues(tagName, vocabulary, value).filter( + val => (val.label !== value && re.test(val.label)) || + (val.synonyms && val.synonyms.some(s => re.test(s)))); }; const handleChange = ( -- 2.30.2