Merge branch '17308-property-editor-fixes'
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Wed, 3 Feb 2021 19:50:00 +0000 (16:50 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Wed, 3 Feb 2021 19:50:00 +0000 (16:50 -0300)
Closes #17308

Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

cypress/integration/collection.spec.js [moved from cypress/integration/collection-panel.spec.js with 93% similarity]
cypress/integration/project.spec.js [new file with mode: 0644]
cypress/integration/side-panel.spec.js
src/store/collection-panel/collection-panel-action.ts
src/store/projects/project-create-actions.ts
src/views-components/project-properties/create-project-properties-form.tsx
src/views-components/project-properties/create-project-properties-list.tsx
src/views-components/resource-properties-form/property-chip.tsx
src/views-components/resource-properties-form/resource-properties-form.tsx

similarity index 93%
rename from cypress/integration/collection-panel.spec.js
rename to cypress/integration/collection.spec.js
index 44e55c34e7740c150773c53e03f9523905be807a..8db574ce9ff473201adc5b7e81f4e2002cc6a8f9 100644 (file)
@@ -38,7 +38,7 @@ describe('Collection panel tests', function() {
             cy.doSearch(`${this.testCollection.uuid}`);
 
             // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
-            cy.get('[data-cy=collection-properties-form]').within(() => {
+            cy.get('[data-cy=resource-properties-form]').within(() => {
                 cy.get('[data-cy=property-field-key]').within(() => {
                     cy.get('input').type('Color');
                 });
@@ -111,7 +111,7 @@ describe('Collection panel tests', function() {
                         .and('not.contain', 'anotherValue')
                     if (isWritable === true) {
                         // Check that properties can be added.
-                        cy.get('[data-cy=collection-properties-form]').within(() => {
+                        cy.get('[data-cy=resource-properties-form]').within(() => {
                             cy.get('[data-cy=property-field-key]').within(() => {
                                 cy.get('input').type('anotherKey');
                             });
@@ -125,7 +125,7 @@ describe('Collection panel tests', function() {
                             .and('contain', 'anotherValue')
                     } else {
                         // Properties form shouldn't be displayed.
-                        cy.get('[data-cy=collection-properties-form]').should('not.exist');
+                        cy.get('[data-cy=resource-properties-form]').should('not.exist');
                     }
                     // Check that the file listing show both read & write operations
                     cy.get('[data-cy=collection-files-panel]').within(() => {
@@ -472,4 +472,30 @@ describe('Collection panel tests', function() {
                 .should('contain', 'foo').and('contain', 'bar');
         });
     });
+
+    it('creates new collection on home project', function() {
+        cy.loginAs(activeUser);
+        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
+        cy.get('[data-cy=side-panel-button]').click();
+        cy.get('[data-cy=side-panel-new-collection]').click();
+        const collName = `Test collection (${Math.floor(999999 * Math.random())})`;
+        cy.get('[data-cy=form-dialog]')
+            .should('contain', 'New collection')
+            .within(() => {
+                cy.get('[data-cy=parent-field]').within(() => {
+                    cy.get('input').should('have.value', 'Home project');
+                })
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input').type(collName);
+                })
+            })
+        cy.get('[data-cy=form-submit-btn]').click();
+        // Confirm that the user was taken to the newly created thing
+        cy.get('[data-cy=form-dialog]').should('not.exist');
+        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
+        cy.get('[data-cy=breadcrumb-last]').should('contain', collName);
+    });
 })
diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js
new file mode 100644 (file)
index 0000000..69809b2
--- /dev/null
@@ -0,0 +1,108 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+describe('Project tests', function() {
+    let activeUser;
+    let adminUser;
+
+    before(function() {
+        // Only set up common users once. These aren't set up as aliases because
+        // aliases are cleaned up after every test. Also it doesn't make sense
+        // to set the same users on beforeEach() over and over again, so we
+        // separate a little from Cypress' 'Best Practices' here.
+        cy.getUser('admin', 'Admin', 'User', true, true)
+            .as('adminUser').then(function() {
+                adminUser = this.adminUser;
+            }
+        );
+        cy.getUser('user', 'Active', 'User', false, true)
+            .as('activeUser').then(function() {
+                activeUser = this.activeUser;
+            }
+        );
+    });
+
+    beforeEach(function() {
+        cy.clearCookies();
+        cy.clearLocalStorage();
+    });
+
+    it('adds creates a new project with properties', function() {
+        const projName = `Test project (${Math.floor(999999 * Math.random())})`;
+        cy.loginAs(activeUser);
+        cy.get('[data-cy=side-panel-button]').click();
+        cy.get('[data-cy=side-panel-new-project]').click();
+        cy.get('[data-cy=form-dialog]')
+            .should('contain', 'New project')
+            .within(() => {
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input').type(projName);
+                });
+
+            });
+        // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
+        cy.get('[data-cy=resource-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=form-dialog]').should('contain', 'Color: Magenta');
+
+        // Create project and confirm the properties' real values.
+        cy.get('[data-cy=form-submit-btn]').click();
+        cy.get('[data-cy=breadcrumb-last]').should('contain', projName);
+        cy.doRequest('GET', '/arvados/v1/groups', null, {
+            filters: `[["name", "=", "${projName}"], ["group_class", "=", "project"]]`,
+        })
+        .its('body.items').as('projects')
+        .then(function() {
+            expect(this.projects).to.have.lengthOf(1);
+            expect(this.projects[0].properties).to.deep.equal(
+                {IDTAGCOLORS: 'IDVALCOLORS3'});
+        });
+    });
+
+    it('creates new project on home project and then a subproject inside it', function() {
+        const createProject = function(name, parentName) {
+            cy.get('[data-cy=side-panel-button]').click();
+            cy.get('[data-cy=side-panel-new-project]').click();
+            cy.get('[data-cy=form-dialog]')
+                .should('contain', 'New project')
+                .within(() => {
+                    cy.get('[data-cy=parent-field]').within(() => {
+                        cy.get('input').invoke('val').then((val) => {
+                            expect(val).to.include(parentName);
+                        });
+                    });
+                    cy.get('[data-cy=name-field]').within(() => {
+                        cy.get('input').type(name);
+                    });
+                });
+            cy.get('[data-cy=form-submit-btn]').click();
+        }
+
+        cy.loginAs(activeUser);
+        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
+        const projName = `Test project (${Math.floor(999999 * Math.random())})`;
+        createProject(projName, 'Home project');
+        // Confirm that the user was taken to the newly created thing
+        cy.get('[data-cy=form-dialog]').should('not.exist');
+        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
+        cy.get('[data-cy=breadcrumb-last]').should('contain', projName);
+        // Create a subproject
+        const subProjName = `Test project (${Math.floor(999999 * Math.random())})`;
+        createProject(subProjName, projName);
+        cy.get('[data-cy=form-dialog]').should('not.exist');
+        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
+        cy.get('[data-cy=breadcrumb-last]').should('contain', subProjName);
+    });
+})
\ No newline at end of file
index fe9d840010350c216a4d7f1e730f9685544cea55..309037ec58a9536112f62133cb2b46fa84936f69 100644 (file)
@@ -75,68 +75,4 @@ describe('Side panel tests', function() {
                 .and('be.disabled');
         })
     })
-
-    it('creates new collection on home project', function() {
-        cy.loginAs(activeUser);
-        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
-        cy.get('[data-cy=side-panel-button]').click();
-        cy.get('[data-cy=side-panel-new-collection]').click();
-        const collName = `Test collection (${Math.floor(999999 * Math.random())})`;
-        cy.get('[data-cy=form-dialog]')
-            .should('contain', 'New collection')
-            .within(() => {
-                cy.get('[data-cy=parent-field]').within(() => {
-                    cy.get('input').should('have.value', 'Home project');
-                })
-                cy.get('[data-cy=name-field]').within(() => {
-                    cy.get('input').type(collName);
-                })
-            })
-        cy.get('[data-cy=form-submit-btn]').click();
-        // Confirm that the user was taken to the newly created thing
-        cy.get('[data-cy=form-dialog]').should('not.exist');
-        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
-        cy.get('[data-cy=breadcrumb-last]').should('contain', collName);
-    })
-
-    it('creates new project on home project and then a subproject inside it', function() {
-        const createProject = function(name, parentName) {
-            cy.get('[data-cy=side-panel-button]').click();
-            cy.get('[data-cy=side-panel-new-project]').click();
-            cy.get('[data-cy=form-dialog]')
-                .should('contain', 'New project')
-                .within(() => {
-                    cy.get('[data-cy=parent-field]').within(() => {
-                        cy.get('input').invoke('val').then((val) => {
-                            expect(val).to.include(parentName);
-                        })
-                    })
-                    cy.get('[data-cy=name-field]').within(() => {
-                        cy.get('input').type(name);
-                    })
-                })
-            cy.get('[data-cy=form-submit-btn]').click();
-        }
-
-        cy.loginAs(activeUser);
-        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
-        const projName = `Test project (${Math.floor(999999 * Math.random())})`;
-        createProject(projName, 'Home project');
-        // Confirm that the user was taken to the newly created thing
-        cy.get('[data-cy=form-dialog]').should('not.exist');
-        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
-        cy.get('[data-cy=breadcrumb-last]').should('contain', projName);
-        // Create a subproject
-        const subProjName = `Test project (${Math.floor(999999 * Math.random())})`;
-        createProject(subProjName, projName);
-        cy.get('[data-cy=form-dialog]').should('not.exist');
-        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
-        cy.get('[data-cy=breadcrumb-last]').should('contain', subProjName);
-    })
 })
\ No newline at end of file
index 7881d6723854242525c4b5c88a907d72772e870c..ff89ca38853946dde94c818e151ea067ee51e994 100644 (file)
@@ -61,7 +61,7 @@ export const createCollectionTag = (data: TagProperty) =>
             dispatch(collectionPanelActions.SET_COLLECTION(updatedCollection));
             dispatch(resourcesActions.SET_RESOURCES([updatedCollection]));
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Tag has been successfully added.",
+                message: "Property has been successfully added.",
                 hideDuration: 2000,
                 kind: SnackbarKind.SUCCESS }));
             dispatch<any>(loadDetailsPanel(updatedCollection.uuid));
index 583a4bd6978237bfc695fd8fe8f1cdae93bd41a1..3599378ceec836abbd81f9b9f9054413cc7a39c2 100644 (file)
@@ -13,6 +13,7 @@ import { ServiceRepository } from '~/services/services';
 import { matchProjectRoute, matchRunProcessRoute } from '~/routes/routes';
 import { ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form';
 import { RouterState } from "react-router-redux";
+import { addProperty, deleteProperty } from "~/lib/resource-properties";
 
 export interface ProjectCreateFormDialogData {
     ownerUuid: string;
@@ -22,7 +23,7 @@ export interface ProjectCreateFormDialogData {
 }
 
 export interface ProjectProperties {
-    [key: string]: string;
+    [key: string]: string | string[];
 }
 
 export const PROJECT_CREATE_FORM_NAME = 'projectCreateFormName';
@@ -69,13 +70,19 @@ export const createProject = (project: Partial<ProjectResource>) =>
 export const addPropertyToCreateProjectForm = (data: ResourcePropertiesFormData) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') };
-        properties[data.key] = data.value;
-        dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties));
+        const key = data.keyID || data.key;
+        const value =  data.valueID || data.value;
+        dispatch(change(
+            PROJECT_CREATE_FORM_NAME,
+            'properties',
+            addProperty(properties, key, value)));
     };
 
-export const removePropertyFromCreateProjectForm = (key: string) =>
+export const removePropertyFromCreateProjectForm = (key: string, value: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') };
-        delete properties[key];
-        dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties));
+        dispatch(change(
+            PROJECT_CREATE_FORM_NAME,
+            'properties',
+            deleteProperty(properties, key, value)));
     };
index 385afff7684c1ae8cca710422580de008f29c243..648547733409ea4eef8f1e25164cd457f77f326d 100644 (file)
@@ -2,48 +2,26 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as React from 'react';
-import { reduxForm, reset, InjectedFormProps } from 'redux-form';
-import { PROJECT_CREATE_PROPERTIES_FORM_NAME, addPropertyToCreateProjectForm } from '~/store/projects/project-create-actions';
-import { ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form';
-import { StyleRulesCallback, WithStyles, withStyles, Grid } from '@material-ui/core';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { PropertyKeyField } from '~/views-components/resource-properties-form/property-key-field';
-import { PropertyValueField } from '~/views-components/resource-properties-form/property-value-field';
-import { Button } from '~/views-components/resource-properties-form/resource-properties-form';
+import { reduxForm, reset } from 'redux-form';
+import { withStyles } from '@material-ui/core';
+import {
+    PROJECT_CREATE_PROPERTIES_FORM_NAME,
+    addPropertyToCreateProjectForm
+} from '~/store/projects/project-create-actions';
+import {
+    ResourcePropertiesForm,
+    ResourcePropertiesFormData
+} from '~/views-components/resource-properties-form/resource-properties-form';
 
-type CssRules = 'root';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    root: {
-        paddingTop: theme.spacing.unit,
-        margin: 0
-    }
-});
-
-type CreateProjectPropertiesFormProps = InjectedFormProps<ResourcePropertiesFormData> & WithStyles<CssRules>;
-
-const Form = withStyles(styles)(
-    ({ handleSubmit, submitting, invalid, classes }: CreateProjectPropertiesFormProps) =>
-        <Grid container spacing={16} className={classes.root}>
-            <Grid item xs={5}>
-                <PropertyKeyField />
-            </Grid>
-            <Grid item xs={5}>
-                <PropertyValueField />
-            </Grid>
-            <Grid item xs={2}>
-                <Button
-                    disabled={invalid}
-                    loading={submitting}
-                    color='primary'
-                    variant='contained'
-                    onClick={handleSubmit}>
-                    Add
-                </Button>
-            </Grid>
-        </Grid>
-);
+const Form = withStyles(
+    ({ spacing }) => (
+        { container:
+            {
+                paddingTop: spacing.unit,
+                margin: 0,
+            }
+        })
+    )(ResourcePropertiesForm);
 
 export const CreateProjectPropertiesForm = reduxForm<ResourcePropertiesFormData>({
     form: PROJECT_CREATE_PROPERTIES_FORM_NAME,
index 1d2050fe4c25343f2f903a7ab27a469bd972ec60..b1de75b24d0affddfaebdd38acd4ddf7c2617154 100644 (file)
@@ -5,10 +5,15 @@
 import * as React from 'react';
 import { connect } from 'react-redux';
 import { Dispatch } from 'redux';
-import { withStyles, StyleRulesCallback, WithStyles, Chip } from '@material-ui/core';
+import {
+    withStyles,
+    StyleRulesCallback,
+    WithStyles,
+} from '@material-ui/core';
 import { RootState } from '~/store/store';
 import { removePropertyFromCreateProjectForm, PROJECT_CREATE_FORM_SELECTOR, ProjectProperties } from '~/store/projects/project-create-actions';
 import { ArvadosTheme } from '~/common/custom-theme';
+import { getPropertyChip } from '../resource-properties-form/property-chip';
 
 type CssRules = 'tag';
 
@@ -24,7 +29,7 @@ interface CreateProjectPropertiesListDataProps {
 }
 
 interface CreateProjectPropertiesListActionProps {
-    handleDelete: (key: string) => void;
+    handleDelete: (key: string, value: string) => void;
 }
 
 const mapStateToProps = (state: RootState): CreateProjectPropertiesListDataProps => {
@@ -33,21 +38,28 @@ const mapStateToProps = (state: RootState): CreateProjectPropertiesListDataProps
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): CreateProjectPropertiesListActionProps => ({
-    handleDelete: (key: string) => dispatch<any>(removePropertyFromCreateProjectForm(key))
+    handleDelete: (key: string, value: string) => dispatch<any>(removePropertyFromCreateProjectForm(key, value))
 });
 
-type CreateProjectPropertiesListProps = CreateProjectPropertiesListDataProps & 
+type CreateProjectPropertiesListProps = CreateProjectPropertiesListDataProps &
     CreateProjectPropertiesListActionProps & WithStyles<CssRules>;
 
 const List = withStyles(styles)(
     ({ classes, handleDelete, properties }: CreateProjectPropertiesListProps) =>
         <div>
             {properties &&
-                Object.keys(properties).map(k => {
-                    return <Chip key={k} className={classes.tag}
-                        onDelete={() => handleDelete(k)}
-                        label={`${k}: ${properties[k]}`} />;
-                })}
+                Object.keys(properties).map(k =>
+                    Array.isArray(properties[k])
+                    ? (properties[k] as string[]).map((v: string) =>
+                        getPropertyChip(
+                            k, v,
+                            () => handleDelete(k, v),
+                            classes.tag))
+                    : getPropertyChip(
+                        k, (properties[k] as string),
+                        () => handleDelete(k, (properties[k] as string)),
+                        classes.tag))
+                }
         </div>
 );
 
index 1fba8a40a98b1dbdc1a22386a8f10ba41bdb8e58..b9a13fbbca52dbe3ab1f3543eb9f0ae099b33289 100644 (file)
@@ -51,7 +51,7 @@ export const PropertyChipComponent = connect(mapStateToProps, mapDispatchToProps
     }
 );
 
-export const getPropertyChip = (k:string, v:string, handleDelete:any, className:string) =>
+export const getPropertyChip = (k: string, v: string, handleDelete: any, className: string) =>
     <PropertyChipComponent
         key={`${k}-${v}`} className={className}
         onDelete={handleDelete}
index c8d0959a11f2afec7fd288ee83c8b692a53ef0a7..e8d2fc58e565616e30349d331c1528aad498d53d 100644 (file)
@@ -20,7 +20,7 @@ export interface ResourcePropertiesFormData {
 export type ResourcePropertiesFormProps = InjectedFormProps<ResourcePropertiesFormData> & WithStyles<GridClassKey>;
 
 export const ResourcePropertiesForm = ({ handleSubmit, submitting, invalid, classes }: ResourcePropertiesFormProps ) =>
-    <form data-cy='collection-properties-form' onSubmit={handleSubmit}>
+    <form data-cy='resource-properties-form' onSubmit={handleSubmit}>
         <Grid container spacing={16} classes={classes}>
             <Grid item xs>
                 <PropertyKeyField />