Merge branch '19691-dialog-error-handling'. Closes #19691
authorLucas Di Pentima <lucas.dipentima@curii.com>
Mon, 12 Dec 2022 19:39:47 +0000 (20:39 +0100)
committerLucas Di Pentima <lucas.dipentima@curii.com>
Mon, 12 Dec 2022 19:39:47 +0000 (20:39 +0100)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima@curii.com>

cypress/integration/collection.spec.js
cypress/integration/project.spec.js
src/services/collection-service/collection-service.ts
src/services/common-service/common-resource-service.ts
src/services/common-service/common-service.ts
src/services/project-service/project-service.ts
src/store/collections/collection-create-actions.ts
src/store/collections/collection-update-actions.ts
src/store/projects/project-create-actions.ts
src/store/projects/project-update-actions.ts

index 74506aea21b67533e8318bbae402be3b0497a9ee..06436d1dbdde1d8a9166234b705a1b9c5fc7a361 100644 (file)
@@ -75,6 +75,55 @@ describe('Collection panel tests', function () {
         });
     });
 
+    it('attempts to use a preexisting name creating or updating a collection', function() {
+        const name = `Test collection ${Math.floor(Math.random() * 999999)}`;
+        cy.createCollection(adminUser.token, {
+            name: name,
+            owner_uuid: activeUser.user.uuid,
+            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+        });
+        cy.loginAs(activeUser);
+        cy.goToPath(`/projects/${activeUser.user.uuid}`);
+        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
+        cy.get('[data-cy=breadcrumb-last]').should('not.exist');
+        // Attempt to create new collection with a duplicate name
+        cy.get('[data-cy=side-panel-button]').click();
+        cy.get('[data-cy=side-panel-new-collection]').click();
+        cy.get('[data-cy=form-dialog]')
+            .should('contain', 'New collection')
+            .within(() => {
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input').type(name);
+                });
+                cy.get('[data-cy=form-submit-btn]').click();
+            });
+        // Error message should display, allowing editing the name
+        cy.get('[data-cy=form-dialog]').should('exist')
+            .and('contain', 'Collection with the same name already exists')
+            .within(() => {
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input').type(' renamed');
+                });
+                cy.get('[data-cy=form-submit-btn]').click();
+            });
+        cy.get('[data-cy=form-dialog]').should('not.exist');
+        // Attempt to rename the collection with the duplicate name
+        cy.get('[data-cy=collection-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Edit collection').click();
+        cy.get('[data-cy=form-dialog]')
+            .should('contain', 'Edit Collection')
+            .within(() => {
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input')
+                        .type('{selectall}{backspace}')
+                        .type(name);
+                });
+                cy.get('[data-cy=form-submit-btn]').click();
+            });
+        cy.get('[data-cy=form-dialog]').should('exist')
+            .and('contain', 'Collection with the same name already exists');
+    });
+
     it('uses the property editor (from edit dialog) with vocabulary terms', function () {
         cy.createCollection(adminUser.token, {
             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
index 93e4257b5b55f137eb626f9669e784b2ba7a5172..bbb3571af1e3ed93b8f17e2307ea35133da4f4b8 100644 (file)
@@ -158,6 +158,38 @@ describe('Project tests', function() {
         cy.get('[data-cy=breadcrumb-last]').should('contain', subProjName);
     });
 
+    it('attempts to use a preexisting name creating a project', function() {
+        const name = `Test project ${Math.floor(Math.random() * 999999)}`;
+        cy.createGroup(activeUser.token, {
+            name: name,
+            group_class: 'project',
+        });
+        cy.loginAs(activeUser);
+        cy.goToPath(`/projects/${activeUser.user.uuid}`);
+
+        // Attempt to create new collection with a duplicate name
+        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(name);
+                });
+                cy.get('[data-cy=form-submit-btn]').click();
+            });
+        // Error message should display, allowing editing the name
+        cy.get('[data-cy=form-dialog]').should('exist')
+            .and('contain', 'Project with the same name already exists')
+            .within(() => {
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input').type(' renamed');
+                });
+                cy.get('[data-cy=form-submit-btn]').click();
+            });
+        cy.get('[data-cy=form-dialog]').should('not.exist');
+    });
+
     it('navigates to the parent project after trashing the one being displayed', function() {
         cy.createGroup(activeUser.token, {
             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
@@ -313,12 +345,12 @@ describe('Project tests', function() {
     });
 
     describe('Frozen projects', () => {
-        beforeEach(() => {  
+        beforeEach(() => {
             cy.createGroup(activeUser.token, {
                 name: `Main project ${Math.floor(Math.random() * 999999)}`,
                 group_class: 'project',
             }).as('mainProject');
-    
+
             cy.createGroup(adminUser.token, {
                 name: `Admin project ${Math.floor(Math.random() * 999999)}`,
                 group_class: 'project',
@@ -337,7 +369,7 @@ describe('Project tests', function() {
                     name: `Main collection ${Math.floor(Math.random() * 999999)}`,
                     owner_uuid: mainProject.uuid,
                     manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
-                }).as('mainCollection');        
+                }).as('mainCollection');
             });
         });
 
@@ -398,7 +430,7 @@ describe('Project tests', function() {
                 cy.get('[data-cy=context-menu]').contains('Unfreeze').click();
 
                 cy.get('main').contains(adminProject.name).rightclick();
-                
+
                 cy.get('[data-cy=context-menu]').contains('Freeze').should('exist');
             });
         });
index e2c420d8d8c6e9f4ebbf112b41daa85dbc559662..d08e7899568ea857807c8d302b39980bc2082098 100644 (file)
@@ -35,13 +35,13 @@ export class CollectionService extends TrashableResourceService<CollectionResour
         return super.get(uuid, showErrors, selectParam, session);
     }
 
-    create(data?: Partial<CollectionResource>) {
-        return super.create({ ...data, preserveVersion: true });
+    create(data?: Partial<CollectionResource>, showErrors?: boolean) {
+        return super.create({ ...data, preserveVersion: true }, showErrors);
     }
 
-    update(uuid: string, data: Partial<CollectionResource>) {
+    update(uuid: string, data: Partial<CollectionResource>, showErrors?: boolean) {
         const select = [...Object.keys(data), 'version', 'modifiedAt'];
-        return super.update(uuid, { ...data, preserveVersion: true }, select);
+        return super.update(uuid, { ...data, preserveVersion: true }, showErrors, select);
     }
 
     async files(uuid: string) {
index c6306779a9ee8cef4bb6eff287c485462f5e898a..d9be8dae9f2a402268217cd8704c0e1d5f538a48 100644 (file)
@@ -26,7 +26,7 @@ export class CommonResourceService<T extends Resource> extends CommonService<T>
         ]));
     }
 
-    create(data?: Partial<T>) {
+    create(data?: Partial<T>, showErrors?: boolean) {
         let payload: any;
         if (data !== undefined) {
             this.readOnlyFields.forEach( field => delete data[field] );
@@ -34,10 +34,10 @@ export class CommonResourceService<T extends Resource> extends CommonService<T>
                 [this.resourceType.slice(0, -1)]: CommonService.mapKeys(snakeCase)(data),
             };
         }
-        return super.create(payload);
+        return super.create(payload, showErrors);
     }
 
-    update(uuid: string, data: Partial<T>, select?: string[]) {
+    update(uuid: string, data: Partial<T>, showErrors?: boolean, select?: string[]) {
         let payload: any;
         if (data !== undefined) {
             this.readOnlyFields.forEach( field => delete data[field] );
@@ -48,12 +48,12 @@ export class CommonResourceService<T extends Resource> extends CommonService<T>
                 payload.select = ['uuid', ...select.map(field => snakeCase(field))];
             };
         }
-        return super.update(uuid, payload);
+        return super.update(uuid, payload, showErrors);
     }
 }
 
 export const getCommonResourceServiceError = (errorResponse: any) => {
-    if ('errors' in errorResponse && 'errorToken' in errorResponse) {
+    if ('errors' in errorResponse) {
         const error = errorResponse.errors.join('');
         switch (true) {
             case /UniqueViolation/.test(error):
index b8e7dc679c21222badea95db2b1be7a9bb4e2138..9a5b767306023ce21673d26427a23d721bd785b6 100644 (file)
@@ -175,12 +175,14 @@ export class CommonService<T> {
         }
     }
 
-    update(uuid: string, data: Partial<T>) {
+    update(uuid: string, data: Partial<T>, showErrors?: boolean) {
         this.validateUuid(uuid);
         return CommonService.defaultResponse(
             this.serverApi
                 .put<T>(`/${this.resourceType}/${uuid}`, data && CommonService.mapKeys(snakeCase)(data)),
-            this.actions
+            this.actions,
+            undefined, // mapKeys
+            showErrors
         );
     }
 }
index 07b083fdab34a06ed7890fa001172886ab960451..442a6ab94fc78dae1974459e6fc20c8013af405f 100644 (file)
@@ -9,9 +9,9 @@ import { ListArguments } from "services/common-service/common-service";
 import { FilterBuilder, joinFilters } from "services/api/filter-builder";
 export class ProjectService extends GroupsService<ProjectResource> {
 
-    create(data: Partial<ProjectResource>) {
+    create(data: Partial<ProjectResource>, showErrors?: boolean) {
         const projectData = { ...data, groupClass: GroupClass.PROJECT };
-        return super.create(projectData);
+        return super.create(projectData, showErrors);
     }
 
     list(args: ListArguments = {}) {
index 17fecc1e15b740681b8759d84dd7e01dc5bb0a0f..f3d1fd3b77ebcc71df07e169f8dfdc4c79b1d36a 100644 (file)
@@ -59,7 +59,7 @@ export const createCollection = (data: CollectionCreateFormDialogData) =>
         let newCollection: CollectionResource | undefined;
         try {
             dispatch(progressIndicatorActions.START_WORKING(COLLECTION_CREATE_FORM_NAME));
-            newCollection = await services.collectionService.create(data);
+            newCollection = await services.collectionService.create(data, false);
             await dispatch<any>(uploadCollectionFiles(newCollection.uuid));
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME }));
             dispatch(reset(COLLECTION_CREATE_FORM_NAME));
@@ -68,11 +68,14 @@ export const createCollection = (data: CollectionCreateFormDialogData) =>
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME, { name: 'Collection with the same name already exists.' } as FormErrors));
-            } else if (error === CommonResourceServiceError.NONE) {
+            } else {
                 dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME));
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME }));
+                const errMsg = e.errors
+                    ? e.errors.join('')
+                    : 'There was an error while creating the collection';
                 dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: 'Collection has not been created.',
+                    message: errMsg,
                     hideDuration: 2000,
                     kind: SnackbarKind.ERROR
                 }));
index bf9c64492d79cef6a5f6a75708436866a9700e0b..d955c9478d3d6d3198a2a249e94a142cd8c5a77a 100644 (file)
@@ -52,7 +52,7 @@ export const updateCollection = (collection: CollectionUpdateFormDialogData) =>
             name: collection.name,
             storageClassesDesired: collection.storageClassesDesired,
             description: collection.description,
-            properties: collection.properties }
+            properties: collection.properties }, false
         ).then(updatedCollection => {
             updatedCollection = {...cachedCollection, ...updatedCollection};
             dispatch(collectionPanelActions.SET_COLLECTION(updatedCollection));
@@ -72,8 +72,11 @@ export const updateCollection = (collection: CollectionUpdateFormDialogData) =>
                 dispatch(stopSubmit(COLLECTION_UPDATE_FORM_NAME, { name: 'Collection with the same name already exists.' } as FormErrors));
             } else {
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
+                const errMsg = e.errors
+                    ? e.errors.join('')
+                    : 'There was an error while updating the collection';
                 dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: e.errors.join(''),
+                    message: errMsg,
                     hideDuration: 2000,
                     kind: SnackbarKind.ERROR }));
                 }
index 23eaf7a4a56aaa077083f0f738c37ad5f81ebb6e..c15c37483ed88e9444618ae4ef13cebf5cb91b67 100644 (file)
@@ -20,6 +20,8 @@ import { ServiceRepository } from 'services/services';
 import { matchProjectRoute, matchRunProcessRoute } from 'routes/routes';
 import { RouterState } from "react-router-redux";
 import { GroupClass } from "models/group";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
 
 export interface ProjectCreateFormDialogData {
     ownerUuid: string;
@@ -65,7 +67,8 @@ export const createProject = (project: Partial<ProjectResource>) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(PROJECT_CREATE_FORM_NAME));
         try {
-            const newProject = await services.projectService.create(project);
+            dispatch(progressIndicatorActions.START_WORKING(PROJECT_CREATE_FORM_NAME));
+            const newProject = await services.projectService.create(project, false);
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_CREATE_FORM_NAME }));
             dispatch(reset(PROJECT_CREATE_FORM_NAME));
             return newProject;
@@ -73,7 +76,20 @@ export const createProject = (project: Partial<ProjectResource>) =>
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors));
+            } else {
+                dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME));
+                dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_CREATE_FORM_NAME }));
+                const errMsg = e.errors
+                    ? e.errors.join('')
+                    : 'There was an error while creating the collection';
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: errMsg,
+                    hideDuration: 2000,
+                    kind: SnackbarKind.ERROR
+                }));
             }
             return undefined;
+        } finally {
+            dispatch(progressIndicatorActions.STOP_WORKING(PROJECT_CREATE_FORM_NAME));
         }
     };
index a6e6748535596e26d09c9b3f948ebf1cabe186dd..057c7cfac59794b95dc7259b66c965551df5c28e 100644 (file)
@@ -24,6 +24,7 @@ import { Participant } from "views-components/sharing-dialog/participant-select"
 import { ProjectProperties } from "./project-create-actions";
 import { getResource } from "store/resources/resources";
 import { ProjectResource } from "models/project";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 
 export interface ProjectUpdateFormDialogData {
     uuid: string;
@@ -61,7 +62,8 @@ export const updateProject = (project: ProjectUpdateFormDialogData) =>
                     name: project.name,
                     description: project.description,
                     properties: project.properties,
-                });
+                },
+                false);
             dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(reset(PROJECT_UPDATE_FORM_NAME));
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
@@ -70,7 +72,16 @@ export const updateProject = (project: ProjectUpdateFormDialogData) =>
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors));
+            } else {
+                dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
+                const errMsg = e.errors
+                    ? e.errors.join('')
+                    : 'There was an error while updating the project';
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: errMsg,
+                    hideDuration: 2000,
+                    kind: SnackbarKind.ERROR }));
             }
-            return ;
+            return;
         }
     };