Merge branch '16647-Responsible-person'
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 21 May 2021 22:06:39 +0000 (00:06 +0200)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 21 May 2021 22:06:51 +0000 (00:06 +0200)
closes #16647

Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

20 files changed:
cypress/fixtures/workflow_with_array_fields.yaml [new file with mode: 0644]
cypress/fixtures/workflow_with_default_array_fields.yaml [new file with mode: 0644]
cypress/integration/collection.spec.js
cypress/integration/create-workflow.spec.js
cypress/integration/favorites.spec.js
cypress/integration/project.spec.js
src/components/tree/tree.tsx
src/services/common-service/common-resource-service.test.ts
src/services/common-service/common-resource-service.ts
src/services/common-service/common-service.ts
src/services/project-service/project-service.test.ts
src/store/project-panel/project-panel-middleware-service.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/trash/trash-actions.ts
src/store/tree-picker/tree-picker-actions.ts
src/views-components/side-panel-tree/side-panel-tree.tsx
src/views/run-process-panel/inputs/directory-array-input.tsx
src/views/run-process-panel/inputs/file-array-input.tsx
src/views/run-process-panel/run-process-inputs-form.tsx
version-at-commit.sh

diff --git a/cypress/fixtures/workflow_with_array_fields.yaml b/cypress/fixtures/workflow_with_array_fields.yaml
new file mode 100644 (file)
index 0000000..33f03f6
--- /dev/null
@@ -0,0 +1,24 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+---
+"$graph":
+- class: Workflow
+  cwlVersion: v1.2
+  hints:
+  - acrContainerImage: 7009415fdc959d0c2819ee2e9db96561+261
+    class: http://arvados.org/cwl#WorkflowRunnerResources
+  id: "#main"
+  inputs:
+  - id: "#main/bar"
+    type:
+      items: Directory
+      type: array
+  - id: "#main/foo"
+    type:
+      items: File
+      type: array
+  outputs: []
+  steps: []
+cwlVersion: v1.2
diff --git a/cypress/fixtures/workflow_with_default_array_fields.yaml b/cypress/fixtures/workflow_with_default_array_fields.yaml
new file mode 100644 (file)
index 0000000..fc71cf8
--- /dev/null
@@ -0,0 +1,26 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+---
+"$graph":
+- class: Workflow
+  cwlVersion: v1.2
+  hints:
+  - acrContainerImage: 7009415fdc959d0c2819ee2e9db96561+261
+    class: http://arvados.org/cwl#WorkflowRunnerResources
+  id: "#main"
+  inputs:
+  - default: []
+    id: "#main/bar"
+    type:
+      items: Directory
+      type: array
+  - default: []
+    id: "#main/foo"
+    type:
+      items: File
+      type: array
+  outputs: []
+  steps: []
+cwlVersion: v1.2
index 75be94f9ad2102d9ba1aa92e6573fe1ad09cbb88..797a9d49fedf4838a8dfdc2619600a644dcbbe40 100644 (file)
@@ -267,7 +267,9 @@ describe('Collection panel tests', function () {
                     cy.get('[data-cy=form-dialog]')
                         .should('contain', 'Rename')
                         .within(() => {
-                            cy.get('input').type(`{selectall}{backspace}${to}`);
+                            cy.get('input')
+                                .type('{selectall}{backspace}')
+                                .type(to, { parseSpecialCharSequences: false });
                         });
                     cy.get('[data-cy=form-submit-btn]').click();
                     cy.get('[data-cy=collection-files-panel]')
@@ -565,7 +567,8 @@ describe('Collection panel tests', function () {
         // 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())})`;
+        // Name between brackets tests bugfix #17582
+        const collName = `[Test collection (${Math.floor(999999 * Math.random())})]`;
         cy.get('[data-cy=form-dialog]')
             .should('contain', 'New collection')
             .within(() => {
index 0059499855438f4d140ea624887412003854fe3d..4da747578c8aad449e490b3466d6e8a66c1bae63 100644 (file)
@@ -111,4 +111,102 @@ describe('Multi-file deletion tests', function () {
                 .find('button').contains('Run Process').should('not.be.disabled');
         });
     });
+
+    ['workflow_with_array_fields.yaml', 'workflow_with_default_array_fields.yaml'].forEach((yamlfile) =>
+    it('can select multi files when creating workflow '+yamlfile, () => {
+        cy.createProject({
+            owningUser: activeUser,
+            projectName: 'myProject1',
+            addToFavorites: true
+        });
+
+        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. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:baz\n"
+        })
+            .as('testCollection');
+
+        cy.createCollection(adminUser.token, {
+            name: `Test collection ${Math.floor(Math.random() * 999999)}`,
+            owner_uuid: activeUser.user.uuid,
+            manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:buz\n`
+        })
+            .as('testCollection2');
+
+        cy.getAll('@myProject1', '@testCollection', '@testCollection2')
+            .then(function ([myProject1, testCollection, testCollection2]) {
+                cy.readFile('cypress/fixtures/'+yamlfile).then(workflow => {
+                    cy.createWorkflow(adminUser.token, {
+                        name: `TestWorkflow${Math.floor(Math.random() * 999999)}.cwl`,
+                        definition: workflow,
+                        owner_uuid: myProject1.uuid,
+                    })
+                        .as('testWorkflow');
+                });
+
+                cy.loginAs(activeUser);
+
+                cy.get('main').contains(myProject1.name).click();
+
+                cy.get('[data-cy=side-panel-button]').click();
+
+                cy.get('#aside-menu-list').contains('Run a process').click();
+
+                cy.get('@testWorkflow')
+                    .then((testWorkflow) => {
+                        cy.get('main').contains(testWorkflow.name).click();
+                        cy.get('[data-cy=run-process-next-button]').click();
+
+                        cy.get('label').contains('#main/foo').parent('div').find('input').click();
+                        cy.get('div[role=dialog]')
+                            .within(() => {
+                                cy.get('p').contains('Projects').closest('div[role=button]')
+                                    .within(() => {
+                                        cy.get('svg[role=presentation]')
+                                            .click({ multiple: true });
+                                    });
+
+                                cy.get(`[data-id=${testCollection.uuid}]`)
+                                    .find('i').click();
+
+                                cy.contains('bar').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').click();
+
+                                cy.contains('baz').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').click();
+
+                                cy.get('[data-cy=ok-button]').click();
+                            });
+
+                        cy.get('label').contains('#main/bar').parent('div').find('input').click();
+                        cy.get('div[role=dialog]')
+                            .within(() => {
+                                cy.get('p').contains('Projects').closest('div[role=button]')
+                                    .within(() => {
+                                        cy.get('svg[role=presentation]')
+                                            .click({ multiple: true });
+                                    });
+
+                                cy.get(`[data-id=${testCollection.uuid}]`)
+                                    .find('input[type=checkbox]').click();
+
+                                cy.get(`[data-id=${testCollection2.uuid}]`)
+                                    .find('input[type=checkbox]').click();
+
+                                cy.get('[data-cy=ok-button]').click();
+                            });
+                    });
+
+                cy.get('label').contains('#main/foo').parent('div')
+                    .within(() => {
+                        cy.contains('baz');
+                        cy.contains('bar');
+                    });
+
+                cy.get('label').contains('#main/bar').parent('div')
+                    .within(() => {
+                        cy.contains(testCollection.name);
+                        cy.contains(testCollection2.name);
+                    });
+            });
+    }));
 })
index 22514beb1e73f0b56694942653196b07bb729fb3..6e9091e1b76e63e3329df636af5279cabd9d47a1 100644 (file)
@@ -204,4 +204,4 @@ describe('Favorites tests', function () {
                     });
             });
     });
-});
\ No newline at end of file
+});
index 76a6d0fff6373bfd09044742b03856d8b22ab340..af2d93e3c605ec79f1538c9224ce09800489bbfd 100644 (file)
@@ -105,4 +105,78 @@ describe('Project tests', function() {
         cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
         cy.get('[data-cy=breadcrumb-last]').should('contain', subProjName);
     });
-})
\ No newline at end of file
+
+    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)}`,
+            group_class: 'project',
+        }).as('testRootProject').then(function() {
+            cy.createGroup(activeUser.token, {
+                name : `Test subproject ${Math.floor(Math.random() * 999999)}`,
+                group_class: 'project',
+                owner_uuid: this.testRootProject.uuid,
+            }).as('testSubProject');
+        });
+        cy.getAll('@testRootProject', '@testSubProject').then(function([testRootProject, testSubProject]) {
+            cy.loginAs(activeUser);
+
+            // Go to subproject and trash it.
+            cy.goToPath(`/projects/${testSubProject.uuid}`);
+            cy.get('[data-cy=side-panel-tree]').should('contain', testSubProject.name);
+            cy.get('[data-cy=breadcrumb-last]')
+                .should('contain', testSubProject.name)
+                .rightclick();
+            cy.get('[data-cy=context-menu]').contains('Move to trash').click();
+
+            // Confirm that the parent project should be displayed.
+            cy.get('[data-cy=breadcrumb-last]').should('contain', testRootProject.name);
+            cy.url().should('contain', `/projects/${testRootProject.uuid}`);
+            cy.get('[data-cy=side-panel-tree]').should('not.contain', testSubProject.name);
+
+            // Checks for bugfix #17637.
+            cy.get('[data-cy=not-found-content]').should('not.exist');
+            cy.get('[data-cy=not-found-page]').should('not.exist');
+        });
+    });
+
+    it('navigates to the root project after trashing the parent of the one being displayed', function() {
+        cy.createGroup(activeUser.token, {
+            name: `Test root project ${Math.floor(Math.random() * 999999)}`,
+            group_class: 'project',
+        }).as('testRootProject').then(function() {
+            cy.createGroup(activeUser.token, {
+                name : `Test subproject ${Math.floor(Math.random() * 999999)}`,
+                group_class: 'project',
+                owner_uuid: this.testRootProject.uuid,
+            }).as('testSubProject').then(function() {
+                cy.createGroup(activeUser.token, {
+                    name : `Test sub subproject ${Math.floor(Math.random() * 999999)}`,
+                    group_class: 'project',
+                    owner_uuid: this.testSubProject.uuid,
+                }).as('testSubSubProject');
+            });
+        });
+        cy.getAll('@testRootProject', '@testSubProject', '@testSubSubProject').then(function([testRootProject, testSubProject, testSubSubProject]) {
+            cy.loginAs(activeUser);
+
+            // Go to innermost project and trash its parent.
+            cy.goToPath(`/projects/${testSubSubProject.uuid}`);
+            cy.get('[data-cy=side-panel-tree]').should('contain', testSubSubProject.name);
+            cy.get('[data-cy=breadcrumb-last]').should('contain', testSubSubProject.name);
+            cy.get('[data-cy=side-panel-tree]')
+                .contains(testSubProject.name)
+                .rightclick();
+            cy.get('[data-cy=context-menu]').contains('Move to trash').click();
+
+            // Confirm that the trashed project's parent should be displayed.
+            cy.get('[data-cy=breadcrumb-last]').should('contain', testRootProject.name);
+            cy.url().should('contain', `/projects/${testRootProject.uuid}`);
+            cy.get('[data-cy=side-panel-tree]').should('not.contain', testSubProject.name);
+            cy.get('[data-cy=side-panel-tree]').should('not.contain', testSubSubProject.name);
+
+            // Checks for bugfix #17637.
+            cy.get('[data-cy=not-found-content]').should('not.exist');
+            cy.get('[data-cy=not-found-page]').should('not.exist');
+        });
+    });
+});
\ No newline at end of file
index cf4d708daaabab31eddd615baac1b82a298b88a7..e84e0f3afc12680f47410636b33572d75fffc3ba 100644 (file)
@@ -155,6 +155,9 @@ interface FlatTreeProps {
     getProperArrowAnimation: Function;
     itemsMap?: Map<string, TreeItem<any>>;
     classes: any;
+    showSelection: any;
+    useRadioButtons?: boolean;
+    handleCheckboxChange: Function;
 }
 
 const FLAT_TREE_ACTIONS = {
@@ -163,7 +166,7 @@ const FLAT_TREE_ACTIONS = {
     toggleActive: 'TOGGLE_ACTIVE',
 };
 
-const ItemIcon = React.memo(({type, kind, active, groupClass, classes}: any) => {
+const ItemIcon = React.memo(({ type, kind, active, groupClass, classes }: any) => {
     let Icon = ProjectIcon;
 
     if (groupClass === GroupClass.FILTER) {
@@ -184,7 +187,7 @@ const ItemIcon = React.memo(({type, kind, active, groupClass, classes}: any) =>
     }
 
     if (kind) {
-        switch(kind) {
+        switch (kind) {
             case ResourceKind.COLLECTION:
                 Icon = CollectionIcon;
                 break;
@@ -231,6 +234,17 @@ const FlatTree = (props: FlatTreeProps) =>
                             {props.getProperArrowAnimation(item.status, item.items!)}
                         </ListItemIcon>
                     </i>
+                    {props.showSelection(item) && !props.useRadioButtons &&
+                        <Checkbox
+                            checked={item.selected}
+                            className={props.classes.checkbox}
+                            color="primary"
+                            onClick={props.handleCheckboxChange(item)} />}
+                    {props.showSelection(item) && props.useRadioButtons &&
+                        <Radio
+                            checked={item.selected}
+                            className={props.classes.checkbox}
+                            color="primary" />}
                     <div data-action={FLAT_TREE_ACTIONS.toggleActive} className={props.classes.renderContainer}>
                         <span style={{ display: 'flex', alignItems: 'center' }}>
                             <ItemIcon type={item.data.type} active={item.active} kind={item.data.kind} groupClass={item.data.kind === ResourceKind.GROUP ? item.data.groupClass : ''} classes={props.classes} />
@@ -293,29 +307,32 @@ export const Tree = withStyles(styles)(
                         {
                             it.open && it.items && it.items.length > 0 &&
                                 it.flatTree ?
-                                    <FlatTree
-                                        it={it}
-                                        itemsMap={itemsMap}
-                                        classes={this.props.classes}
-                                        levelIndentation={levelIndentation}
+                                <FlatTree
+                                    it={it}
+                                    itemsMap={itemsMap}
+                                    showSelection={showSelection}
+                                    classes={this.props.classes}
+                                    useRadioButtons={useRadioButtons}
+                                    levelIndentation={levelIndentation}
+                                    handleCheckboxChange={this.handleCheckboxChange}
+                                    onContextMenu={this.props.onContextMenu}
+                                    handleToggleItemOpen={this.handleToggleItemOpen}
+                                    toggleItemActive={this.props.toggleItemActive}
+                                    getToggableIconClassNames={this.getToggableIconClassNames}
+                                    getProperArrowAnimation={this.getProperArrowAnimation}
+                                /> :
+                                <Collapse in={it.open} timeout="auto" unmountOnExit>
+                                    <Tree
+                                        showSelection={this.props.showSelection}
+                                        items={it.items}
+                                        render={render}
+                                        disableRipple={disableRipple}
+                                        toggleItemOpen={toggleItemOpen}
+                                        toggleItemActive={toggleItemActive}
+                                        level={level + 1}
                                         onContextMenu={this.props.onContextMenu}
-                                        handleToggleItemOpen={this.handleToggleItemOpen}
-                                        toggleItemActive={this.props.toggleItemActive}
-                                        getToggableIconClassNames={this.getToggableIconClassNames}
-                                        getProperArrowAnimation={this.getProperArrowAnimation}
-                                    /> :
-                                    <Collapse in={it.open} timeout="auto" unmountOnExit>
-                                        <Tree
-                                            showSelection={this.props.showSelection}
-                                            items={it.items}
-                                            render={render}
-                                            disableRipple={disableRipple}
-                                            toggleItemOpen={toggleItemOpen}
-                                            toggleItemActive={toggleItemActive}
-                                            level={level + 1}
-                                            onContextMenu={this.props.onContextMenu}
-                                            toggleItemSelection={this.props.toggleItemSelection} />
-                                    </Collapse>
+                                        toggleItemSelection={this.props.toggleItemSelection} />
+                                </Collapse>
                         }
                     </div>)}
             </List>;
index d00412b8a69baa3941ec0bccca56f882928e0a6a..a7fdac621f18ea9a985a1191f308b7c83a9fbc50 100644 (file)
@@ -32,50 +32,50 @@ describe("CommonResourceService", () => {
 
     it("#create", async () => {
         axiosMock
-            .onPost("/resource")
+            .onPost("/resources")
             .reply(200, { owner_uuid: "ownerUuidValue" });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
         const resource = await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
         expect(resource).toEqual({ ownerUuid: "ownerUuidValue" });
     });
 
     it("#create maps request params to snake case", async () => {
         axiosInstance.post = jest.fn(() => Promise.resolve({data: {}}));
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
         await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
-        expect(axiosInstance.post).toHaveBeenCalledWith("/resource", {owner_uuid: "ownerUuidValue"});
+        expect(axiosInstance.post).toHaveBeenCalledWith("/resources", {resource: {owner_uuid: "ownerUuidValue"}});
     });
 
     it("#create ignores fields listed as readonly", async () => {
         axiosInstance.post = jest.fn(() => Promise.resolve({data: {}}));
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
         // UUID fields are read-only on all resources.
         await commonResourceService.create({ uuid: "this should be ignored", ownerUuid: "ownerUuidValue" });
-        expect(axiosInstance.post).toHaveBeenCalledWith("/resource", {owner_uuid: "ownerUuidValue"});
+        expect(axiosInstance.post).toHaveBeenCalledWith("/resources", {resource: {owner_uuid: "ownerUuidValue"}});
     });
 
     it("#update ignores fields listed as readonly", async () => {
         axiosInstance.put = jest.fn(() => Promise.resolve({data: {}}));
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
         // UUID fields are read-only on all resources.
         await commonResourceService.update('resource-uuid', { uuid: "this should be ignored", ownerUuid: "ownerUuidValue" });
-        expect(axiosInstance.put).toHaveBeenCalledWith("/resource/resource-uuid", {owner_uuid: "ownerUuidValue"});
+        expect(axiosInstance.put).toHaveBeenCalledWith("/resources/resource-uuid", {resource:  {owner_uuid: "ownerUuidValue"}});
     });
 
     it("#delete", async () => {
         axiosMock
-            .onDelete("/resource/uuid")
+            .onDelete("/resources/uuid")
             .reply(200, { deleted_at: "now" });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
         const resource = await commonResourceService.delete("uuid");
         expect(resource).toEqual({ deletedAt: "now" });
     });
 
     it("#get", async () => {
         axiosMock
-            .onGet("/resource/uuid")
+            .onGet("/resources/uuid")
             .reply(200, {
                 modified_at: "now",
                 properties: {
@@ -83,7 +83,7 @@ describe("CommonResourceService", () => {
                 }
             });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
         const resource = await commonResourceService.get("uuid");
         // Only first level keys are mapped to camel case
         expect(resource).toEqual({
@@ -96,7 +96,7 @@ describe("CommonResourceService", () => {
 
     it("#list", async () => {
         axiosMock
-            .onGet("/resource")
+            .onGet("/resources")
             .reply(200, {
                 kind: "kind",
                 offset: 2,
@@ -110,7 +110,7 @@ describe("CommonResourceService", () => {
                 items_available: 20
             });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
         const resource = await commonResourceService.list({ limit: 10, offset: 1 });
         // First level keys are mapped to camel case inside "items" arrays
         expect(resource).toEqual({
@@ -129,10 +129,10 @@ describe("CommonResourceService", () => {
 
     it("#list using POST when query string is too big", async () => {
         axiosMock
-            .onAny("/resource")
+            .onAny("/resources")
             .reply(200);
         const tooBig = 'x'.repeat(1500);
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
         await commonResourceService.list({ filters: tooBig });
         expect(axiosMock.history.get.length).toBe(0);
         expect(axiosMock.history.post.length).toBe(1);
@@ -142,10 +142,10 @@ describe("CommonResourceService", () => {
 
     it("#list using GET when query string is not too big", async () => {
         axiosMock
-            .onAny("/resource")
+            .onAny("/resources")
             .reply(200);
         const notTooBig = 'x'.repeat(1480);
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resources", actions);
         await commonResourceService.list({ filters: notTooBig });
         expect(axiosMock.history.post.length).toBe(0);
         expect(axiosMock.history.get.length).toBe(1);
index bc24f22796b21001bce09b358e1efe6c657d6248..83af1e13acdc6af6b9a4133e17fbcd1741df3e1d 100644 (file)
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { AxiosInstance } from "axios";
+import * as _ from "lodash";
 import { Resource } from "src/models/resource";
 import { ApiActions } from "~/services/api/api-actions";
 import { CommonService } from "~/services/common-service/common-service";
@@ -26,17 +27,25 @@ export class CommonResourceService<T extends Resource> extends CommonService<T>
     }
 
     create(data?: Partial<T>) {
+        let payload: any;
         if (data !== undefined) {
             this.readOnlyFields.forEach( field => delete data[field] );
+            payload = {
+                [this.resourceType.slice(0, -1)]: CommonService.mapKeys(_.snakeCase)(data),
+            };
         }
-        return super.create(data);
+        return super.create(payload);
     }
 
     update(uuid: string, data: Partial<T>) {
+        let payload: any;
         if (data !== undefined) {
             this.readOnlyFields.forEach( field => delete data[field] );
+            payload = {
+                [this.resourceType.slice(0, -1)]: CommonService.mapKeys(_.snakeCase)(data),
+            };
         }
-        return super.update(uuid, data);
+        return super.update(uuid, payload);
     }
 }
 
index e43f9f8f136a7404af40b9ffb0626cffd650d9ad..34bcb4c3bb72708915d618ade9bff5ef7e724f63 100644 (file)
@@ -42,7 +42,7 @@ export class CommonService<T> {
 
     constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions, readOnlyFields: string[] = []) {
         this.serverApi = serverApi;
-        this.resourceType = '/' + resourceType;
+        this.resourceType = resourceType;
         this.actions = actions;
         this.readOnlyFields = readOnlyFields;
     }
@@ -97,7 +97,7 @@ export class CommonService<T> {
     create(data?: Partial<T>, showErrors?: boolean) {
         return CommonService.defaultResponse(
             this.serverApi
-                .post<T>(this.resourceType, data && CommonService.mapKeys(_.snakeCase)(data)),
+                .post<T>(`/${this.resourceType}`, data && CommonService.mapKeys(_.snakeCase)(data)),
             this.actions,
             true, // mapKeys
             showErrors
@@ -108,7 +108,7 @@ export class CommonService<T> {
         this.validateUuid(uuid);
         return CommonService.defaultResponse(
             this.serverApi
-                .delete(this.resourceType + '/' + uuid),
+                .delete(`/${this.resourceType}/${uuid}`),
             this.actions
         );
     }
@@ -117,7 +117,7 @@ export class CommonService<T> {
         this.validateUuid(uuid);
         return CommonService.defaultResponse(
             this.serverApi
-                .get<T>(this.resourceType + '/' + uuid),
+                .get<T>(`/${this.resourceType}/${uuid}`),
             this.actions,
             true, // mapKeys
             showErrors
@@ -134,7 +134,7 @@ export class CommonService<T> {
 
         if (QueryString.stringify(params).length <= 1500) {
             return CommonService.defaultResponse(
-                this.serverApi.get(this.resourceType, { params }),
+                this.serverApi.get(`/${this.resourceType}`, { params }),
                 this.actions
             );
         } else {
@@ -147,7 +147,7 @@ export class CommonService<T> {
                 }
             });
             return CommonService.defaultResponse(
-                this.serverApi.post(this.resourceType, formData, {
+                this.serverApi.post(`/${this.resourceType}`, formData, {
                     params: {
                         _method: 'GET'
                     }
@@ -161,7 +161,7 @@ export class CommonService<T> {
         this.validateUuid(uuid);
         return CommonService.defaultResponse(
             this.serverApi
-                .put<T>(this.resourceType + '/' + uuid, data && CommonService.mapKeys(_.snakeCase)(data)),
+                .put<T>(`/${this.resourceType}/${uuid}`, data && CommonService.mapKeys(_.snakeCase)(data)),
             this.actions
         );
     }
index 3634b8cba60a3fc84621b4f12ef87c56ad9b53b6..71e8b6d086843bbaf983cd8f9f70791f1705ab45 100644 (file)
@@ -19,8 +19,10 @@ describe("CommonResourceService", () => {
         const projectService = new ProjectService(axiosInstance, actions);
         const resource = await projectService.create({ name: "nameValue" });
         expect(axiosInstance.post).toHaveBeenCalledWith("/groups", {
-            name: "nameValue",
-            group_class: "project"
+            group: {
+                name: "nameValue",
+                group_class: "project"
+            }
         });
     });
 
index b889f7cbc0ad7277337fa6a90e147d2a26180149..3a37be71faf9d338140e43c8543aa7440a03e07f 100644 (file)
@@ -23,7 +23,7 @@ import { ProjectResource } from "~/models/project";
 import { updateResources } from "~/store/resources/resources-actions";
 import { getProperty } from "~/store/properties/properties";
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
-import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
 import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
 import { ListResults } from '~/services/common-service/common-service';
 import { loadContainers } from '~/store/processes/processes-actions';
index 6152b99f1edde80cae53d1d3dc8d2d0bf8b8170c..ede4c00c7605794d4fc7f89cfb41c870fb751ec3 100644 (file)
@@ -97,7 +97,7 @@ export const loadSidePanelTreeProjects = (projectUuid: string) =>
         const node = treePicker ? getNode(projectUuid)(treePicker) : undefined;
         if (projectUuid === SidePanelTreeCategory.SHARED_WITH_ME) {
             await dispatch<any>(loadSharedRoot);
-        } else if (node || projectUuid === '') {
+        } else if (node || projectUuid !== '') {
             await dispatch<any>(loadProject(projectUuid));
         }
     };
index b810b1e490a8aa61fc9a6abf792dbd92cc285954..38587deb2fed6b4e61a6650b5b94c1ed38509556 100644 (file)
@@ -10,73 +10,71 @@ import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
 import { activateSidePanelTreeItem, loadSidePanelTreeProjects } from "~/store/side-panel-tree/side-panel-tree-actions";
 import { projectPanelActions } from "~/store/project-panel/project-panel-action";
 import { ResourceKind } from "~/models/resource";
-import { navigateToTrash } from '~/store/navigation/navigation-action';
+import { navigateTo, navigateToTrash } from '~/store/navigation/navigation-action';
 import { matchCollectionRoute } from '~/routes/routes';
 
 export const toggleProjectTrashed = (uuid: string, ownerUuid: string, isTrashed: boolean) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+        let errorMessage = '';
+        let successMessage = '';
         try {
             if (isTrashed) {
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash...", kind: SnackbarKind.INFO }));
+                errorMessage = "Could not restore project from trash";
+                successMessage = "Restored from trash";
                 await services.groupsService.untrash(uuid);
+                dispatch<any>(navigateTo(uuid));
                 dispatch<any>(activateSidePanelTreeItem(uuid));
-                dispatch(trashPanelActions.REQUEST_ITEMS());
-                dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: "Restored from trash",
-                    hideDuration: 2000,
-                    kind: SnackbarKind.SUCCESS
-                }));
             } else {
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash...", kind: SnackbarKind.INFO }));
+                errorMessage = "Could not move project to trash";
+                successMessage = "Added to trash";
                 await services.groupsService.trash(uuid);
-                dispatch(projectPanelActions.REQUEST_ITEMS());
                 dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
-                dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: "Added to trash",
-                    hideDuration: 2000,
-                    kind: SnackbarKind.SUCCESS
-                }));
+                dispatch<any>(navigateTo(ownerUuid));
             }
         } catch (e) {
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Could not move project to trash",
+                message: errorMessage,
                 kind: SnackbarKind.ERROR
             }));
         }
+        dispatch(snackbarActions.OPEN_SNACKBAR({
+            message: successMessage,
+            hideDuration: 2000,
+            kind: SnackbarKind.SUCCESS
+        }));
     };
 
 export const toggleCollectionTrashed = (uuid: string, isTrashed: boolean) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+        let errorMessage = '';
+        let successMessage = '';
         try {
             if (isTrashed) {
                 const { location } = getState().router;
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash...", kind: SnackbarKind.INFO }));
+                errorMessage = "Could not restore collection from trash";
+                successMessage = "Restored from trash";
                 await services.collectionService.untrash(uuid);
                 if (matchCollectionRoute(location ? location.pathname : '')) {
                     dispatch(navigateToTrash);
                 }
                 dispatch(trashPanelActions.REQUEST_ITEMS());
-                dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: "Restored from trash",
-                    hideDuration: 2000,
-                    kind: SnackbarKind.SUCCESS
-                }));
             } else {
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash...", kind: SnackbarKind.INFO }));
+                errorMessage = "Could not move collection to trash";
+                successMessage = "Added to trash";
                 await services.collectionService.trash(uuid);
                 dispatch(projectPanelActions.REQUEST_ITEMS());
-                dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: "Added to trash",
-                    hideDuration: 2000,
-                    kind: SnackbarKind.SUCCESS
-                }));
             }
         } catch (e) {
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Could not move collection to trash",
+                message: errorMessage,
                 kind: SnackbarKind.ERROR
             }));
         }
+        dispatch(snackbarActions.OPEN_SNACKBAR({
+            message: successMessage,
+            hideDuration: 2000,
+            kind: SnackbarKind.SUCCESS
+        }));
     };
 
 export const toggleTrashed = (kind: ResourceKind, uuid: string, ownerUuid: string, isTrashed: boolean) =>
index 5d12b419ebe898e2666131e9d5bb85b4adcb98b0..5157394d16a317f6348791c0c276863dba49e070 100644 (file)
@@ -149,7 +149,6 @@ export const loadCollection = (id: string, pickerId: string) =>
 
             const node = getNode(id)(picker);
             if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
-
                 const files = await services.collectionService.files(node.value.portableDataHash);
                 const tree = createCollectionFilesTree(files);
                 const sorted = sortFilesTree(tree);
index 4c6f01a146c910885de60244924529aaed575f59..bd6762e510c9162f2345dd3304458a11c6beeeb2 100644 (file)
@@ -41,7 +41,9 @@ const mapDispatchToProps = (dispatch: Dispatch, props: SidePanelTreeProps): Side
 
 export const SidePanelTree = connect(undefined, mapDispatchToProps)(
     (props: SidePanelTreeActionProps) =>
-        <TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />);
+        <span data-cy="side-panel-tree">
+        <TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />
+        </span>);
 
 const renderSidePanelItem = (item: TreeItem<ProjectResource>) => {
     const name = typeof item.data === 'string' ? item.data : item.data.name;
index 8b03a12340386a370efd1ada6653ceea1fb62fe8..73e946f6bdd2a161a0d0b55212cfa8a6017f8c6c 100644 (file)
@@ -30,6 +30,7 @@ import { ResourceKind } from '~/models/resource';
 
 export interface DirectoryArrayInputProps {
     input: DirectoryArrayCommandInputParameter;
+    options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
 }
 
 export const DirectoryArrayInput = ({ input }: DirectoryArrayInputProps) =>
@@ -58,7 +59,7 @@ const parse = (directory: CollectionResource): Directory => ({
 });
 
 const formatDirectories = (directories: Directory[] = []) =>
-    directories.map(format);
+    directories ? directories.map(format) : [];
 
 const format = ({ location = '', basename = '' }: Directory): FormattedDirectory => ({
     portableDataHash: location.replace('keep:', ''),
@@ -93,7 +94,9 @@ const mapStateToProps = createStructuredSelector({
 });
 
 const DirectoryArrayInputComponent = connect(mapStateToProps)(
-    class DirectoryArrayInputComponent extends React.Component<DirectoryArrayInputComponentProps & GenericInputProps & DispatchProp, DirectoryArrayInputComponentState> {
+    class DirectoryArrayInputComponent extends React.Component<DirectoryArrayInputComponentProps & GenericInputProps & DispatchProp & {
+        options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
+    }, DirectoryArrayInputComponentState> {
         state: DirectoryArrayInputComponentState = {
             open: false,
             directories: [],
@@ -157,7 +160,7 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
                 .reduce((directories, { value }) =>
                     'kind' in value &&
                         value.kind === ResourceKind.COLLECTION &&
-                        formattedDirectories.find(({ portableDataHash }) => value.portableDataHash === portableDataHash)
+                        formattedDirectories.find(({ portableDataHash, name }) => value.portableDataHash === portableDataHash && value.name === name)
                         ? directories.concat(value)
                         : directories, initialDirectories);
 
@@ -179,7 +182,7 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
             });
 
             const orderedDirectories = formattedDirectories.reduce((dirs, formattedDir) => {
-                const dir = directories.find(({ portableDataHash }) => portableDataHash === formattedDir.portableDataHash);
+                const dir = directories.find(({ portableDataHash, name }) => portableDataHash === formattedDir.portableDataHash && name === formattedDir.name);
                 return dir
                     ? [...dirs, dir]
                     : dirs;
@@ -241,6 +244,7 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
                 <DialogActions>
                     <Button onClick={this.closeDialog}>Cancel</Button>
                     <Button
+                        data-cy='ok-button'
                         variant='contained'
                         color='primary'
                         onClick={this.submit}>Ok</Button>
@@ -276,6 +280,7 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
                             pickerId={this.props.commandInput.id}
                             includeCollections
                             showSelection
+                            options={this.props.options}
                             toggleItemSelection={this.refreshDirectories} />
                     </div>
                     <Divider />
@@ -294,6 +299,3 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
     });
 
 type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';
-
-
-
index 14d16824966cfaa6fcd4e4d8ee67753e5e4619d3..b82fd004151a96ed84d011b3b39f315a5840e2c1 100644 (file)
@@ -29,6 +29,7 @@ import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withSty
 
 export interface FileArrayInputProps {
     input: FileArrayCommandInputParameter;
+    options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
 }
 export const FileArrayInput = ({ input }: FileArrayInputProps) =>
     <Field
@@ -52,7 +53,7 @@ const parse = (file: CollectionFile): File => ({
 });
 
 const formatFiles = (files: File[] = []) =>
-    files.map(format);
+    files ? files.map(format) : [];
 
 const format = (file: File): CollectionFile => ({
     id: file.location
@@ -92,7 +93,9 @@ const mapStateToProps = createStructuredSelector({
 });
 
 const FileArrayInputComponent = connect(mapStateToProps)(
-    class FileArrayInputComponent extends React.Component<FileArrayInputComponentProps & GenericInputProps & DispatchProp, FileArrayInputComponentState> {
+    class FileArrayInputComponent extends React.Component<FileArrayInputComponentProps & GenericInputProps & DispatchProp & {
+        options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
+    }, FileArrayInputComponentState> {
         state: FileArrayInputComponentState = {
             open: false,
             files: [],
@@ -167,7 +170,6 @@ const FileArrayInputComponent = connect(mapStateToProps)(
             });
 
             this.setFiles(files);
-
         }
 
         refreshFiles = () => {
@@ -223,6 +225,7 @@ const FileArrayInputComponent = connect(mapStateToProps)(
                 <DialogActions>
                     <Button onClick={this.closeDialog}>Cancel</Button>
                     <Button
+                        data-cy='ok-button'
                         variant='contained'
                         color='primary'
                         onClick={this.submit}>Ok</Button>
@@ -259,6 +262,7 @@ const FileArrayInputComponent = connect(mapStateToProps)(
                             includeCollections
                             includeFiles
                             showSelection
+                            options={this.props.options}
                             toggleItemSelection={this.refreshFiles} />
                     </div>
                     <Divider />
@@ -277,6 +281,3 @@ const FileArrayInputComponent = connect(mapStateToProps)(
     });
 
 type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';
-
-
-
index e6a504dbb8d2e9852cde01920d9b4398096f2c36..196655e08283b593f04b693abb32d5bd3807c87f 100644 (file)
@@ -111,10 +111,10 @@ const getInputComponent = (input: CommandInputParameter) => {
             return <FloatArrayInput input={input as FloatArrayCommandInputParameter} />;
 
         case isArrayOfType(input, CWLType.FILE):
-            return <FileArrayInput input={input as FileArrayCommandInputParameter} />;
+            return <FileArrayInput options={{ showOnlyOwned: false, showOnlyWritable: false }} input={input as FileArrayCommandInputParameter} />;
 
         case isArrayOfType(input, CWLType.DIRECTORY):
-            return <DirectoryArrayInput input={input as DirectoryArrayCommandInputParameter} />;
+            return <DirectoryArrayInput options={{ showOnlyOwned: false, showOnlyWritable: false }} input={input as DirectoryArrayCommandInputParameter} />;
 
         default:
             return null;
index 89684cf2abdb32b8b6b749a22cf03caf2bba5bcf..84d7d0b4ae3faa0f96f0291c0202616b6c1a0534 100755 (executable)
@@ -1,4 +1,7 @@
 #!/bin/bash
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
 
 set -e -o pipefail
 commit="$1"
@@ -35,9 +38,10 @@ else
 
     if git merge-base --is-ancestor "$nearest_tag" "$merge_base" ; then
         # x.(y+1).0.devTIMESTAMP, where x.y.z is the newest version that does not contain $commit
-       # grep reads the list of tags (-f) that contain $commit and filters them out (-v)
-       # this prevents a newer tag from retroactively changing the versions of everything before it
-        v=$(git tag | grep -vFf <(git tag --contains "$commit") | sort -Vr | head -n1 | perl -pe 's/\.(\d+)\.\d+/".".($1+1).".0"/e')
+        # grep reads the list of tags (-f) that contain $commit and filters them out (-v)
+        # this prevents a newer tag from retroactively changing the versions of everything before it
+        # We also filter out four-digit faux semantic versioning tags, they gum up the works.
+        v=$(git tag | grep -vFf <(git tag --contains "$commit") |grep -v -P '\d.\d.\d.\d' | sort -Vr | head -n1 | perl -pe 's/\.(\d+)\.\d+/".".($1+1).".0"/e')
     else
         # x.y.(z+1).devTIMESTAMP, where x.y.z is the latest released ancestor of $commit
         v=$(echo $nearest_tag | perl -pe 's/(\d+)$/$1+1/e')