From: Lucas Di Pentima Date: Tue, 1 Mar 2022 22:30:29 +0000 (-0300) Subject: Merge branch '17754-federated-acct-merge'. Closes #17754. X-Git-Tag: 2.4.0~8 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/6e335a900ab99ddc7b7288e00d20a54f6c75ec8f?hp=0e86ce9638f17cf801b6dbf08e6dfd91edf89153 Merge branch '17754-federated-acct-merge'. Closes #17754. Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima --- diff --git a/cypress/integration/create-workflow.spec.js b/cypress/integration/create-workflow.spec.js index 4da74757..b1ea5dbf 100644 --- a/cypress/integration/create-workflow.spec.js +++ b/cypress/integration/create-workflow.spec.js @@ -64,9 +64,6 @@ describe('Multi-file deletion tests', function () { cy.get('@testWorkflow').then(() => { cy.loginAs(adminUser); - cy.get('[data-cy=linear-progress]').should('exist'); - cy.get('[data-cy=linear-progress]').should('not.exist'); - cy.get('[data-cy=side-panel-button]').click(); cy.get('[data-cy=side-panel-run-process]').click(); diff --git a/src/models/vocabulary.test.ts b/src/models/vocabulary.test.ts index 18e2f19f..761c785b 100644 --- a/src/models/vocabulary.test.ts +++ b/src/models/vocabulary.test.ts @@ -18,7 +18,8 @@ describe('Vocabulary', () => { strict: false, labels: [ {label: "Animal" }, - {label: "Creature"} + {label: "Creature"}, + {label: "Beast"}, ], values: { IDVALANIMALS1: { @@ -39,13 +40,13 @@ describe('Vocabulary', () => { labels: [{label: "Sizes"}], values: { IDVALSIZES1: { - labels: [{label: "Small"}] + labels: [{label: "Small"}, {label: "S"}, {label: "Little"}] }, IDVALSIZES2: { - labels: [{label: "Medium"}] + labels: [{label: "Medium"}, {label: "M"}] }, IDVALSIZES3: { - labels: [{label: "Large"}] + labels: [{label: "Large"}, {label: "L"}] }, IDVALSIZES4: { labels: [] @@ -61,23 +62,70 @@ describe('Vocabulary', () => { // Alphabetically ordered by label expect(tagKeys).toEqual([ {id: "IDKEYANIMALS", label: "Animal"}, + {id: "IDKEYANIMALS", label: "Beast"}, {id: "IDKEYANIMALS", label: "Creature"}, {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT"}, {id: "IDKEYSIZES", label: "Sizes"}, ]); }); + it('returns the list of preferred tag keys', () => { + const preferredTagKeys = Vocabulary.getPreferredTags(vocabulary); + // Alphabetically ordered by label + expect(preferredTagKeys).toEqual([ + {id: "IDKEYANIMALS", label: "Animal", synonyms: []}, + {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT", synonyms: []}, + {id: "IDKEYSIZES", label: "Sizes", synonyms: []}, + ]); + }); + + 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", synonyms: ["Creature"]}, + {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT", synonyms: []}, + {id: "IDKEYSIZES", label: "Sizes", synonyms: []}, + ]); + }); + it('returns the tag values for a given key', () => { const tagValues = Vocabulary.getTagValues('IDKEYSIZES', vocabulary); // Alphabetically ordered by label expect(tagValues).toEqual([ {id: "IDVALSIZES4", label: "IDVALSIZES4"}, + {id: "IDVALSIZES3", label: "L"}, {id: "IDVALSIZES3", label: "Large"}, + {id: "IDVALSIZES1", label: "Little"}, + {id: "IDVALSIZES2", label: "M"}, {id: "IDVALSIZES2", label: "Medium"}, + {id: "IDVALSIZES1", label: "S"}, {id: "IDVALSIZES1", label: "Small"}, ]) }); + it('returns the preferred tag values for a given key', () => { + const preferredTagValues = Vocabulary.getPreferredTagValues('IDKEYSIZES', vocabulary); + // Alphabetically ordered by label + expect(preferredTagValues).toEqual([ + {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 matching synonyms for a given key', () => { + const preferredTagValues = Vocabulary.getPreferredTagValues('IDKEYSIZES', vocabulary, 'litt'); + // Alphabetically ordered by label + expect(preferredTagValues).toEqual([ + {id: "IDVALSIZES4", label: "IDVALSIZES4", synonyms: []}, + {id: "IDVALSIZES3", label: "Large", synonyms: []}, + {id: "IDVALSIZES2", label: "Medium", synonyms: []}, + {id: "IDVALSIZES1", label: "Small", synonyms: ["Little"]}, + ]) + }); + it('returns an empty list of values for an non-existent key', () => { const tagValues = Vocabulary.getTagValues('IDNONSENSE', vocabulary); expect(tagValues).toEqual([]); diff --git a/src/models/vocabulary.ts b/src/models/vocabulary.ts index 3c542844..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,6 +28,7 @@ export interface Tag { export interface PropFieldSuggestion { id: string; label: string; + synonyms?: string[]; } const VOCABULARY_VALIDATORS = [ @@ -64,9 +66,9 @@ const compare = (a: PropFieldSuggestion, b: PropFieldSuggestion) => { return 0; }; -export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary) => { +export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary): PropFieldSuggestion[] => { const tag = vocabulary.tags[tagKeyID]; - const ret = tag && tag.values + return tag && tag.values ? Object.keys(tag.values).map( tagValueID => tag.values![tagValueID].labels && tag.values![tagValueID].labels.length > 0 ? tag.values![tagValueID].labels.map( @@ -75,11 +77,30 @@ export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary) => { .reduce((prev, curr) => [...prev, ...curr], []) .sort(compare) : []; - return ret; }; -export const getTags = ({ tags }: Vocabulary) => { - const ret = tags && Object.keys(tags) +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, + "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) + : []; +}; + +export const getTags = ({ tags }: Vocabulary): PropFieldSuggestion[] => { + return tags && Object.keys(tags) ? Object.keys(tags).map( tagID => tags[tagID].labels && tags[tagID].labels.length > 0 ? tags[tagID].labels.map( @@ -88,7 +109,25 @@ export const getTags = ({ tags }: Vocabulary) => { .reduce((prev, curr) => [...prev, ...curr], []) .sort(compare) : []; - return ret; +}; + +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, + "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) + : []; }; export const getTagKeyID = (tagKeyLabel:string, vocabulary: Vocabulary) => 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 791949f5..0be4527a 100644 --- a/src/views-components/resource-properties-form/property-key-field.tsx +++ b/src/views-components/resource-properties-form/property-key-field.tsx @@ -6,7 +6,14 @@ import React from 'react'; import { WrappedFieldProps, Field, FormName, reset, change, WrappedFieldInputProps, WrappedFieldMetaProps } from 'redux-form'; import { memoize } from 'lodash'; import { Autocomplete } from 'components/autocomplete/autocomplete'; -import { Vocabulary, getTags, getTagKeyID, getTagKeyLabel } from 'models/vocabulary'; +import { + Vocabulary, + getTags, + getTagKeyID, + getTagKeyLabel, + getPreferredTags, + PropFieldSuggestion +} from 'models/vocabulary'; import { handleSelect, handleBlur, @@ -36,8 +43,14 @@ export const PropertyKeyField = connectVocabulary( const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) => ( 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 @@ -51,7 +64,6 @@ const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & Vocabula const newValue = e.currentTarget.value; handleChange(data.form, props.input, props.meta, newValue); }} - {...buildProps(props)} /> )} />; @@ -67,9 +79,11 @@ const matchTags = (vocabulary: Vocabulary) => ? undefined : 'Incorrect key'; -const getSuggestions = (value: string, vocabulary: Vocabulary) => { +const getSuggestions = (value: string, vocabulary: Vocabulary): PropFieldSuggestion[] => { const re = new RegExp(escapeRegExp(value), "i"); - return getTags(vocabulary).filter(tag => re.test(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 b023e412..b8e525bf 100644 --- a/src/views-components/resource-properties-form/property-value-field.tsx +++ b/src/views-components/resource-properties-form/property-value-field.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { WrappedFieldProps, Field, formValues, FormName, WrappedFieldInputProps, WrappedFieldMetaProps, change } from 'redux-form'; import { compose } from 'redux'; import { Autocomplete } from 'components/autocomplete/autocomplete'; -import { Vocabulary, isStrictTag, getTagValues, getTagValueID, getTagValueLabel } from 'models/vocabulary'; +import { Vocabulary, isStrictTag, getTagValues, getTagValueID, getTagValueLabel, PropFieldSuggestion, getPreferredTagValues } from 'models/vocabulary'; import { PROPERTY_KEY_FIELD_ID, PROPERTY_KEY_FIELD_NAME } from 'views-components/resource-properties-form/property-key-field'; import { handleSelect, @@ -56,9 +56,15 @@ export const PropertyValueField = connectVocabularyAndPropertyKey( const PropertyValueInput = ({ vocabulary, propertyKeyId, propertyKeyName, ...props }: WrappedFieldProps & PropertyValueFieldProps) => ( 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 @@ -73,7 +79,6 @@ const PropertyValueInput = ({ vocabulary, propertyKeyId, propertyKeyName, ...pro const tagValueID = getTagValueID(propertyKeyId, newValue, vocabulary); handleChange(data.form, tagValueID, props.input, props.meta, newValue); }} - {...buildProps(props)} /> )} />; @@ -90,7 +95,9 @@ const matchTagValues = ({ vocabulary, propertyKeyId }: PropertyValueFieldProps) const getSuggestions = (value: string, tagName: string, vocabulary: Vocabulary) => { const re = new RegExp(escapeRegExp(value), "i"); - return getTagValues(tagName, vocabulary).filter(v => re.test(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 = (