[ARVADOS-WORKBENCH2] created: 2.1.0-182-g57c6aae1

Git user git at public.arvados.org
Wed Jan 27 00:29:39 UTC 2021


        at  57c6aae12f63f3a6a3df9b4cdf201879126f685e (commit)


commit 57c6aae12f63f3a6a3df9b4cdf201879126f685e
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue Jan 26 11:27:52 2021 -0300

    17266: Handles onChange event separate on the property editor.
    
    When changing the property value field, on every event a vocabulary match
    is attempted just in case the user submits the form by hitting <enter>.
    The bug happened because the vocabulary matching was only relying on the
    onBlur event, that doesn't fire in this case.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/views-components/resource-properties-form/property-field-common.tsx b/src/views-components/resource-properties-form/property-field-common.tsx
index 90a6a301..2e3f17c3 100644
--- a/src/views-components/resource-properties-form/property-field-common.tsx
+++ b/src/views-components/resource-properties-form/property-field-common.tsx
@@ -2,13 +2,11 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { ChangeEvent } from 'react';
 import { connect } from 'react-redux';
-import { change, WrappedFieldMetaProps, WrappedFieldInputProps, WrappedFieldProps, reset } from 'redux-form';
+import { change, WrappedFieldMetaProps, WrappedFieldInputProps, WrappedFieldProps } from 'redux-form';
 import { Vocabulary, PropFieldSuggestion } from '~/models/vocabulary';
 import { RootState } from '~/store/store';
 import { getVocabulary } from '~/store/vocabulary/vocabulary-selectors';
-import { PROPERTY_KEY_FIELD_ID } from './property-key-field';
 
 export interface VocabularyProp {
     vocabulary: Vocabulary;
@@ -70,19 +68,3 @@ export const handleSelect = (
             dispatch(change(formName, fieldName, item.id));
         }
     };
-
-export const handleChange = (
-    fieldName: string,
-    formName: string,
-    { onChange }: WrappedFieldInputProps,
-    { dispatch }: WrappedFieldMetaProps) =>
-    (value: ChangeEvent<HTMLInputElement>) => {
-        if (fieldName === PROPERTY_KEY_FIELD_ID) {
-            // Properties' values are dependant on the keys, if any value is
-            // pre-existant, a change on the property key should mean that the
-            // previous value is invalid.
-            dispatch(reset(formName));
-        }
-        onChange(value);
-        dispatch(change(formName, fieldName, value));
-    };
\ No newline at end of file
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 0c3a49be..de8a1140 100644
--- a/src/views-components/resource-properties-form/property-key-field.tsx
+++ b/src/views-components/resource-properties-form/property-key-field.tsx
@@ -3,11 +3,18 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { WrappedFieldProps, Field, FormName } from 'redux-form';
+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 } from '~/models/vocabulary';
-import { handleSelect, handleBlur, connectVocabulary, VocabularyProp, ValidationProp, buildProps, handleChange } from '~/views-components/resource-properties-form/property-field-common';
+import {
+    handleSelect,
+    handleBlur,
+    connectVocabulary,
+    VocabularyProp,
+    ValidationProp,
+    buildProps
+} from '~/views-components/resource-properties-form/property-field-common';
 import { TAG_KEY_VALIDATION } from '~/validators/validators';
 import { escapeRegExp } from '~/common/regexp.ts';
 import { ChangeEvent } from 'react';
@@ -33,8 +40,9 @@ const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & Vocabula
             suggestions={getSuggestions(props.input.value, vocabulary)}
             onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
             onBlur={handleBlur(PROPERTY_KEY_FIELD_ID, data.form, props.meta, props.input, getTagKeyID(props.input.value, vocabulary))}
-            onChange={(value: ChangeEvent<HTMLInputElement>) => {
-                handleChange(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)(value);
+            onChange={(e: ChangeEvent<HTMLInputElement>) => {
+                const newValue = e.currentTarget.value;
+                handleChange(data.form, props.input, props.meta, newValue);
             }}
             {...buildProps(props)}
         />
@@ -56,3 +64,18 @@ const getSuggestions = (value: string, vocabulary: Vocabulary) => {
     const re = new RegExp(escapeRegExp(value), "i");
     return getTags(vocabulary).filter(tag => re.test(tag.label) && tag.label !== value);
 };
+
+const handleChange = (
+    formName: string,
+    { onChange }: WrappedFieldInputProps,
+    { dispatch }: WrappedFieldMetaProps,
+    value: string) => {
+        // Properties' values are dependant on the keys, if any value is
+        // pre-existant, a change on the property key should mean that the
+        // previous value is invalid, so we better reset the whole form before
+        // setting the new tag key.
+        dispatch(reset(formName));
+
+        onChange(value);
+        dispatch(change(formName, PROPERTY_KEY_FIELD_NAME, value));
+    };
\ No newline at end of file
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 9ce9d521..d0044e18 100644
--- a/src/views-components/resource-properties-form/property-value-field.tsx
+++ b/src/views-components/resource-properties-form/property-value-field.tsx
@@ -3,14 +3,22 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { WrappedFieldProps, Field, formValues, FormName } from 'redux-form';
+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 } from '~/models/vocabulary';
 import { PROPERTY_KEY_FIELD_ID, PROPERTY_KEY_FIELD_NAME } from '~/views-components/resource-properties-form/property-key-field';
-import { handleSelect, handleBlur, VocabularyProp, ValidationProp, connectVocabulary, buildProps, handleChange } from '~/views-components/resource-properties-form/property-field-common';
+import {
+    handleSelect,
+    handleBlur,
+    VocabularyProp,
+    ValidationProp,
+    connectVocabulary,
+    buildProps
+} from '~/views-components/resource-properties-form/property-field-common';
 import { TAG_VALUE_VALIDATION } from '~/validators/validators';
 import { escapeRegExp } from '~/common/regexp.ts';
+import { ChangeEvent } from 'react';
 
 interface PropertyKeyProp {
     propertyKeyId: string;
@@ -53,7 +61,11 @@ const PropertyValueInput = ({ vocabulary, propertyKeyId, propertyKeyName, ...pro
             suggestions={getSuggestions(props.input.value, propertyKeyId, vocabulary)}
             onSelect={handleSelect(PROPERTY_VALUE_FIELD_ID, data.form, props.input, props.meta)}
             onBlur={handleBlur(PROPERTY_VALUE_FIELD_ID, data.form, props.meta, props.input, getTagValueID(propertyKeyId, props.input.value, vocabulary))}
-            onChange={handleChange(PROPERTY_VALUE_FIELD_ID, data.form, props.input, props.meta)}
+            onChange={(e: ChangeEvent<HTMLInputElement>) => {
+                const newValue = e.currentTarget.value;
+                const tagValueID = getTagValueID(propertyKeyId, newValue, vocabulary);
+                handleChange(data.form, tagValueID, props.input, props.meta, newValue);
+            }}
             {...buildProps(props)}
         />
     )} />;
@@ -73,3 +85,14 @@ 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);
 };
+
+const handleChange = (
+    formName: string,
+    tagValueID: string,
+    { onChange }: WrappedFieldInputProps,
+    { dispatch }: WrappedFieldMetaProps,
+    value: string) => {
+        onChange(value);
+        dispatch(change(formName, PROPERTY_VALUE_FIELD_NAME, value));
+        dispatch(change(formName, PROPERTY_VALUE_FIELD_ID, tagValueID));
+    };
\ No newline at end of file
diff --git a/src/views-components/resource-properties-form/resource-properties-form.tsx b/src/views-components/resource-properties-form/resource-properties-form.tsx
index 0632b97c..c8d0959a 100644
--- a/src/views-components/resource-properties-form/resource-properties-form.tsx
+++ b/src/views-components/resource-properties-form/resource-properties-form.tsx
@@ -30,6 +30,7 @@ export const ResourcePropertiesForm = ({ handleSubmit, submitting, invalid, clas
             </Grid>
             <Grid item xs>
                 <Button
+                    data-cy='property-add-btn'
                     disabled={invalid}
                     loading={submitting}
                     color='primary'

commit fc5b0373d8897c9d24f8f7ab192b20a3eca49655
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue Jan 26 21:06:54 2021 -0300

    17266: Exposes the bug with an integration test.
    
    When the user typed a valid property value and hit <enter>, the literal
    value's label is saved on the backend instead of its vocabulary ID.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/cypress/integration/collection-panel.spec.js b/cypress/integration/collection-panel.spec.js
index 0669b313..3e241ceb 100644
--- a/cypress/integration/collection-panel.spec.js
+++ b/cypress/integration/collection-panel.spec.js
@@ -28,6 +28,39 @@ describe('Collection panel tests', function() {
         cy.clearLocalStorage();
     });
 
+    it('uses the property editor with vocabulary terms', function() {
+        cy.createCollection(adminUser.token, {
+            name: `Test collection ${Math.floor(Math.random() * 999999)}`,
+            owner_uuid: activeUser.user.uuid,
+            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
+        .as('testCollection').then(function() {
+            cy.loginAs(activeUser);
+            cy.doSearch(`${this.testCollection.uuid}`);
+
+            // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
+            cy.get('[data-cy=collection-properties-form]').within(() => {
+                cy.get('[data-cy=property-field-key]').within(() => {
+                    cy.get('input').type('Color');
+                });
+                cy.get('[data-cy=property-field-value]').within(() => {
+                    cy.get('input').type('Magenta');
+                });
+                cy.root().submit();
+            });
+            // Confirm proper vocabulary labels are displayed on the UI.
+            cy.get('[data-cy=collection-properties-panel]')
+                .should('contain', 'Color')
+                .and('contain', 'Magenta');
+            // Confirm proper vocabulary IDs were saved on the backend.
+            cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`)
+            .its('body').as('collection')
+            .then(function() {
+                expect(this.collection.properties).to.deep.equal(
+                    {IDTAGCOLORS: 'IDVALCOLORS3'});
+            });
+        });
+    });
+
     it('shows collection by URL', function() {
         cy.loginAs(activeUser);
         [true, false].map(function(isWritable) {

commit 76090ef444f95160521b4458ed256f3a6306fa90
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue Jan 26 11:23:43 2021 -0300

    17266: Avoids reloading the app on cypress tests by using search.
    
    Instead of using visit(url), whenever possible we should use some method
    that makes the app do the routing. One way is to use the search bar with
    UUIDs, as it will use the route to the proper resource panel.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/cypress/integration/collection-panel.spec.js b/cypress/integration/collection-panel.spec.js
index 424c2bad..0669b313 100644
--- a/cypress/integration/collection-panel.spec.js
+++ b/cypress/integration/collection-panel.spec.js
@@ -28,7 +28,7 @@ describe('Collection panel tests', function() {
         cy.clearLocalStorage();
     });
 
-    it.only('shows collection by URL', function() {
+    it('shows collection by URL', function() {
         cy.loginAs(activeUser);
         [true, false].map(function(isWritable) {
             cy.createGroup(adminUser.token, {
@@ -50,10 +50,7 @@ describe('Collection panel tests', function() {
                         head_uuid: this.sharedGroup.uuid,
                         tail_uuid: activeUser.user.uuid
                     })
-                    cy.visit(`/collections/${this.testCollection.uuid}`);
-
-                    cy.get('[data-cy=linear-progress]').should('exist');
-                    cy.get('[data-cy=linear-progress]').should('not.exist');
+                    cy.doSearch(`${this.testCollection.uuid}`);
 
                     // Check that name & uuid are correct.
                     cy.get('[data-cy=collection-info-panel]')
@@ -143,10 +140,7 @@ describe('Collection panel tests', function() {
             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
         .as('testCollection').then(function() {
             cy.loginAs(activeUser);
-            cy.visit(`/collections/${this.testCollection.uuid}`);
-
-            cy.get('[data-cy=linear-progress]').should('exist');
-            cy.get('[data-cy=linear-progress]').should('not.exist');
+            cy.doSearch(`${this.testCollection.uuid}`);
 
             const nameTransitions = [
                 ['bar', '&'],
@@ -183,10 +177,7 @@ describe('Collection panel tests', function() {
             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
         .as('testCollection').then(function() {
             cy.loginAs(activeUser);
-            cy.visit(`/collections/${this.testCollection.uuid}`);
-
-            cy.get('[data-cy=linear-progress]').should('exist');
-            cy.get('[data-cy=linear-progress]').should('not.exist');
+            cy.doSearch(`${this.testCollection.uuid}`);
 
             // Rename 'bar' to 'subdir/foo'
             cy.get('[data-cy=collection-files-panel]')
@@ -234,10 +225,7 @@ describe('Collection panel tests', function() {
             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
         .as('testCollection').then(function() {
             cy.loginAs(activeUser);
-            cy.visit(`/collections/${this.testCollection.uuid}`);
-
-            cy.get('[data-cy=linear-progress]').should('exist');
-            cy.get('[data-cy=linear-progress]').should('not.exist');
+            cy.doSearch(`${this.testCollection.uuid}`);
 
             const illegalNamesFromUI = [
                 ['.', "Name cannot be '.' or '..'"],
@@ -312,10 +300,7 @@ describe('Collection panel tests', function() {
             });
             // Check the old version displays as what it is.
             cy.loginAs(activeUser)
-            cy.visit(`/collections/${oldVersionUuid}`);
-
-            cy.get('[data-cy=linear-progress]').should('exist');
-            cy.get('[data-cy=linear-progress]').should('not.exist');
+            cy.doSearch(`${oldVersionUuid}`);
 
             cy.get('[data-cy=collection-info-panel]').should('contain', 'This is an old version');
             cy.get('[data-cy=read-only-icon]').should('exist');
@@ -337,10 +322,7 @@ describe('Collection panel tests', function() {
         .as('collection').then(function() {
             // Visit collection, check basic information
             cy.loginAs(activeUser)
-            cy.visit(`/collections/${this.collection.uuid}`);
-
-            cy.get('[data-cy=linear-progress]').should('exist');
-            cy.get('[data-cy=linear-progress]').should('not.exist');
+            cy.doSearch(`${this.collection.uuid}`);
 
             cy.get('[data-cy=collection-info-panel]').should('not.contain', 'This is an old version');
             cy.get('[data-cy=read-only-icon]').should('not.exist');
diff --git a/cypress/integration/delete-multiple-files.spec.js b/cypress/integration/delete-multiple-files.spec.js
index 0955f56a..f9e87117 100644
--- a/cypress/integration/delete-multiple-files.spec.js
+++ b/cypress/integration/delete-multiple-files.spec.js
@@ -32,10 +32,7 @@ describe('Multi-file deletion tests', function () {
         })
             .as('testCollection').then(function () {
                 cy.loginAs(activeUser);
-                cy.visit(`/collections/${this.testCollection.uuid}`);
-
-                cy.get('[data-cy=linear-progress]').should('exist');
-                cy.get('[data-cy=linear-progress]').should('not.exist');
+                cy.doSearch(`${this.testCollection.uuid}`);
 
                 cy.get('[data-cy=collection-files-panel]').within(() => {
                     cy.get('[type="checkbox"]').first().check();
@@ -58,10 +55,7 @@ describe('Multi-file deletion tests', function () {
         })
             .as('testCollection').then(function () {
                 cy.loginAs(activeUser);
-                cy.visit(`/collections/${this.testCollection.uuid}`);
-
-                cy.get('[data-cy=linear-progress]').should('exist');
-                cy.get('[data-cy=linear-progress]').should('not.exist');
+                cy.doSearch(`${this.testCollection.uuid}`);
 
                 cy.get('[data-cy=virtual-file-tree] > div > i').first().click();
                 cy.get('[data-cy=collection-files-panel]')
diff --git a/cypress/integration/search.spec.js b/cypress/integration/search.spec.js
index 06131a2e..8806d12d 100644
--- a/cypress/integration/search.spec.js
+++ b/cypress/integration/search.spec.js
@@ -72,11 +72,11 @@ describe('Search tests', function() {
             cy.loginAs(activeUser);
             const searchQuery = `${colName} type:arvados#collection`;
             // Search for only collection's current version
-            cy.visit(`/search-results?q=${encodeURIComponent(searchQuery)}`);
+            cy.doSearch(`${searchQuery}`);
             cy.get('[data-cy=search-results]').should('contain', 'head version');
             cy.get('[data-cy=search-results]').should('not.contain', 'version 1');
             // ...and then, include old versions.
-            cy.visit(`/search-results?q=${encodeURIComponent(searchQuery + ' is:pastVersion')}`);
+            cy.doSearch(`${searchQuery} is:pastVersion`);
             cy.get('[data-cy=search-results]').should('contain', 'head version');
             cy.get('[data-cy=search-results]').should('contain', 'version 1');
         });
diff --git a/cypress/integration/side-panel.spec.js b/cypress/integration/side-panel.spec.js
index 40f39144..fe9d8400 100644
--- a/cypress/integration/side-panel.spec.js
+++ b/cypress/integration/side-panel.spec.js
@@ -30,7 +30,7 @@ describe('Side panel tests', function() {
 
     it('enables the +NEW side panel button on users home project', function() {
         cy.loginAs(activeUser);
-        cy.visit(`/projects/${activeUser.user.uuid}`);
+        cy.doSearch(`${activeUser.user.uuid}`);
         cy.get('[data-cy=side-panel-button]')
             .should('exist')
             .and('not.be.disabled');
@@ -49,7 +49,7 @@ describe('Side panel tests', function() {
                     head_uuid: this.sharedGroup.uuid,
                     tail_uuid: activeUser.user.uuid
                 })
-                cy.visit(`/projects/${this.sharedGroup.uuid}`);
+                cy.doSearch(`${this.sharedGroup.uuid}`);
                 cy.get('[data-cy=side-panel-button]')
                     .should('exist')
                     .and(`${isWritable ? 'not.' : ''}be.disabled`);
@@ -78,7 +78,7 @@ describe('Side panel tests', function() {
 
     it('creates new collection on home project', function() {
         cy.loginAs(activeUser);
-        cy.visit(`/projects/${activeUser.user.uuid}`);
+        cy.doSearch(`${activeUser.user.uuid}`);
         cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
         cy.get('[data-cy=breadcrumb-last]').should('not.exist');
         // Create new collection
@@ -122,7 +122,7 @@ describe('Side panel tests', function() {
         }
 
         cy.loginAs(activeUser);
-        cy.visit(`/projects/${activeUser.user.uuid}`);
+        cy.doSearch(`${activeUser.user.uuid}`);
         cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
         cy.get('[data-cy=breadcrumb-last]').should('not.exist');
         // Create new project
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 216012c4..bba04ba8 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -176,10 +176,14 @@ Cypress.Commands.add(
 Cypress.Commands.add(
     "loginAs", (user) => {
         cy.visit(`/token/?api_token=${user.token}`);
-        cy.get('[data-cy=loading-spinner]').should('exist');
-        cy.get('[data-cy=loading-spinner]').should('not.exist');
         cy.url().should('contain', '/projects/');
         cy.get('div#root').should('contain', 'Arvados Workbench (zzzzz)');
         cy.get('div#root').should('not.contain', 'Your account is inactive');
     }
 )
+
+Cypress.Commands.add(
+    "doSearch", (searchTerm) => {
+        cy.get('[data-cy=searchbar-input-field]').type(`{selectall}${searchTerm}{enter}`);
+    }
+)
\ No newline at end of file
diff --git a/src/views-components/search-bar/search-bar-view.tsx b/src/views-components/search-bar/search-bar-view.tsx
index 20536fd7..073aae6e 100644
--- a/src/views-components/search-bar/search-bar-view.tsx
+++ b/src/views-components/search-bar/search-bar-view.tsx
@@ -177,7 +177,7 @@ export const SearchBarView = compose(connectVocabulary, withStyles(styles))(
                     <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
                         <form onSubmit={this.handleSubmit}>
                             <Input
-                                data-cy='search-input-field'
+                                data-cy='searchbar-input-field'
                                 className={classes.input}
                                 onChange={this.handleChange}
                                 placeholder="Search"

commit bb18ea7238065603568d21fd9a44abbd35768599
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Mon Jan 25 17:33:55 2021 -0300

    17266: Avoids adding a property value without a key.
    
    Also resets the form whenever a property key changes, this avoids a previously
    validated property value to be taken as valid with a different property key.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/components/autocomplete/autocomplete.tsx b/src/components/autocomplete/autocomplete.tsx
index e01673b7..67bc9fa1 100644
--- a/src/components/autocomplete/autocomplete.tsx
+++ b/src/components/autocomplete/autocomplete.tsx
@@ -18,6 +18,7 @@ export interface AutocompleteProps<Item, Suggestion> {
     label?: string;
     value: string;
     items: Item[];
+    disabled?: boolean;
     suggestions?: Suggestion[];
     error?: boolean;
     helperText?: string;
@@ -67,6 +68,7 @@ export class Autocomplete<Value, Suggestion> extends React.Component<Autocomplet
 
     renderInput() {
         return <Input
+            disabled={this.props.disabled}
             autoFocus={this.props.autofocus}
             inputRef={this.inputRef}
             value={this.props.value}
diff --git a/src/views-components/resource-properties-form/property-field-common.tsx b/src/views-components/resource-properties-form/property-field-common.tsx
index e802ad5c..90a6a301 100644
--- a/src/views-components/resource-properties-form/property-field-common.tsx
+++ b/src/views-components/resource-properties-form/property-field-common.tsx
@@ -2,11 +2,13 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { ChangeEvent } from 'react';
 import { connect } from 'react-redux';
-import { change, WrappedFieldMetaProps, WrappedFieldInputProps, WrappedFieldProps } from 'redux-form';
+import { change, WrappedFieldMetaProps, WrappedFieldInputProps, WrappedFieldProps, reset } from 'redux-form';
 import { Vocabulary, PropFieldSuggestion } from '~/models/vocabulary';
 import { RootState } from '~/store/store';
 import { getVocabulary } from '~/store/vocabulary/vocabulary-selectors';
+import { PROPERTY_KEY_FIELD_ID } from './property-key-field';
 
 export interface VocabularyProp {
     vocabulary: Vocabulary;
@@ -36,7 +38,6 @@ export const getErrorMsg = (meta: WrappedFieldMetaProps) =>
 export const buildProps = ({ input, meta }: WrappedFieldProps) => {
     return {
         value: input.value,
-        onChange: input.onChange,
         items: ITEMS_PLACEHOLDER,
         renderSuggestion: (item: PropFieldSuggestion) => item.label,
         error: hasError(meta),
@@ -69,3 +70,19 @@ export const handleSelect = (
             dispatch(change(formName, fieldName, item.id));
         }
     };
+
+export const handleChange = (
+    fieldName: string,
+    formName: string,
+    { onChange }: WrappedFieldInputProps,
+    { dispatch }: WrappedFieldMetaProps) =>
+    (value: ChangeEvent<HTMLInputElement>) => {
+        if (fieldName === PROPERTY_KEY_FIELD_ID) {
+            // Properties' values are dependant on the keys, if any value is
+            // pre-existant, a change on the property key should mean that the
+            // previous value is invalid.
+            dispatch(reset(formName));
+        }
+        onChange(value);
+        dispatch(change(formName, fieldName, value));
+    };
\ No newline at end of file
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 d17f50d4..0c3a49be 100644
--- a/src/views-components/resource-properties-form/property-key-field.tsx
+++ b/src/views-components/resource-properties-form/property-key-field.tsx
@@ -7,9 +7,10 @@ import { WrappedFieldProps, Field, FormName } from 'redux-form';
 import { memoize } from 'lodash';
 import { Autocomplete } from '~/components/autocomplete/autocomplete';
 import { Vocabulary, getTags, getTagKeyID } from '~/models/vocabulary';
-import { handleSelect, handleBlur, connectVocabulary, VocabularyProp, ValidationProp, buildProps } from '~/views-components/resource-properties-form/property-field-common';
+import { handleSelect, handleBlur, connectVocabulary, VocabularyProp, ValidationProp, buildProps, handleChange } from '~/views-components/resource-properties-form/property-field-common';
 import { TAG_KEY_VALIDATION } from '~/validators/validators';
 import { escapeRegExp } from '~/common/regexp.ts';
+import { ChangeEvent } from 'react';
 
 export const PROPERTY_KEY_FIELD_NAME = 'key';
 export const PROPERTY_KEY_FIELD_ID = 'keyID';
@@ -32,6 +33,9 @@ const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & Vocabula
             suggestions={getSuggestions(props.input.value, vocabulary)}
             onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
             onBlur={handleBlur(PROPERTY_KEY_FIELD_ID, data.form, props.meta, props.input, getTagKeyID(props.input.value, vocabulary))}
+            onChange={(value: ChangeEvent<HTMLInputElement>) => {
+                handleChange(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)(value);
+            }}
             {...buildProps(props)}
         />
     )} />;
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 c5a5071f..9ce9d521 100644
--- a/src/views-components/resource-properties-form/property-value-field.tsx
+++ b/src/views-components/resource-properties-form/property-value-field.tsx
@@ -7,23 +7,31 @@ import { WrappedFieldProps, Field, formValues, FormName } from 'redux-form';
 import { compose } from 'redux';
 import { Autocomplete } from '~/components/autocomplete/autocomplete';
 import { Vocabulary, isStrictTag, getTagValues, getTagValueID } from '~/models/vocabulary';
-import { PROPERTY_KEY_FIELD_ID } from '~/views-components/resource-properties-form/property-key-field';
-import { handleSelect, handleBlur, VocabularyProp, ValidationProp, connectVocabulary, buildProps } from '~/views-components/resource-properties-form/property-field-common';
+import { PROPERTY_KEY_FIELD_ID, PROPERTY_KEY_FIELD_NAME } from '~/views-components/resource-properties-form/property-key-field';
+import { handleSelect, handleBlur, VocabularyProp, ValidationProp, connectVocabulary, buildProps, handleChange } from '~/views-components/resource-properties-form/property-field-common';
 import { TAG_VALUE_VALIDATION } from '~/validators/validators';
 import { escapeRegExp } from '~/common/regexp.ts';
 
 interface PropertyKeyProp {
-    propertyKey: string;
+    propertyKeyId: string;
+    propertyKeyName: string;
 }
 
-type PropertyValueFieldProps = VocabularyProp & PropertyKeyProp & ValidationProp;
+interface PropertyValueInputProp {
+    disabled: boolean;
+}
+
+type PropertyValueFieldProps = VocabularyProp & PropertyKeyProp & ValidationProp & PropertyValueInputProp;
 
 export const PROPERTY_VALUE_FIELD_NAME = 'value';
 export const PROPERTY_VALUE_FIELD_ID = 'valueID';
 
 const connectVocabularyAndPropertyKey = compose(
     connectVocabulary,
-    formValues({ propertyKey: PROPERTY_KEY_FIELD_ID }),
+    formValues({
+        propertyKeyId: PROPERTY_KEY_FIELD_ID,
+        propertyKeyName: PROPERTY_KEY_FIELD_NAME,
+    }),
 );
 
 export const PropertyValueField = connectVocabularyAndPropertyKey(
@@ -33,29 +41,31 @@ export const PropertyValueField = connectVocabularyAndPropertyKey(
             name={PROPERTY_VALUE_FIELD_NAME}
             component={PropertyValueInput}
             validate={skipValidation ? undefined : getValidation(props)}
-            {...props} />
+            {...{...props, disabled: !props.propertyKeyName}} />
         </span>
 );
 
-const PropertyValueInput = ({ vocabulary, propertyKey, ...props }: WrappedFieldProps & PropertyValueFieldProps) =>
+const PropertyValueInput = ({ vocabulary, propertyKeyId, propertyKeyName, ...props }: WrappedFieldProps & PropertyValueFieldProps) =>
     <FormName children={data => (
         <Autocomplete
             label='Value'
-            suggestions={getSuggestions(props.input.value, propertyKey, vocabulary)}
+            disabled={props.disabled}
+            suggestions={getSuggestions(props.input.value, propertyKeyId, vocabulary)}
             onSelect={handleSelect(PROPERTY_VALUE_FIELD_ID, data.form, props.input, props.meta)}
-            onBlur={handleBlur(PROPERTY_VALUE_FIELD_ID, data.form, props.meta, props.input, getTagValueID(propertyKey, props.input.value, vocabulary))}
+            onBlur={handleBlur(PROPERTY_VALUE_FIELD_ID, data.form, props.meta, props.input, getTagValueID(propertyKeyId, props.input.value, vocabulary))}
+            onChange={handleChange(PROPERTY_VALUE_FIELD_ID, data.form, props.input, props.meta)}
             {...buildProps(props)}
         />
     )} />;
 
 const getValidation = (props: PropertyValueFieldProps) =>
-    isStrictTag(props.propertyKey, props.vocabulary)
+    isStrictTag(props.propertyKeyId, props.vocabulary)
         ? [...TAG_VALUE_VALIDATION, matchTagValues(props)]
         : TAG_VALUE_VALIDATION;
 
-const matchTagValues = ({ vocabulary, propertyKey }: PropertyValueFieldProps) =>
+const matchTagValues = ({ vocabulary, propertyKeyId }: PropertyValueFieldProps) =>
     (value: string) =>
-        getTagValues(propertyKey, vocabulary).find(v => v.label === value)
+        getTagValues(propertyKeyId, vocabulary).find(v => v.label === value)
             ? undefined
             : 'Incorrect value';
 

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list