[arvados-workbench2] updated: 2.7.0-8-g162665e0

git repository hosting git at public.arvados.org
Thu Oct 12 18:55:23 UTC 2023


Summary of changes:
 ...y_fields.yaml => workflow_directory_array.yaml} |   6 +-
 cypress/integration/create-workflow.spec.js        |  77 +++++++++
 src/store/tree-picker/tree-picker-actions.test.ts  | 185 +++++++++++++++++++++
 src/store/tree-picker/tree-picker-actions.ts       | 118 +++++++------
 4 files changed, 331 insertions(+), 55 deletions(-)
 copy cypress/fixtures/{workflow_with_array_fields.yaml => workflow_directory_array.yaml} (82%)
 create mode 100644 src/store/tree-picker/tree-picker-actions.test.ts

       via  162665e037ec2de3203e8ed34991b1f443462382 (commit)
       via  bc01781d2a6cbfed6d2bdf94397f97c60308eb64 (commit)
       via  084ea0c0c8e078dc006e19d2aa851a30817b01c8 (commit)
       via  aa7fdb114d208a401980f3001e7adcb252bdc95c (commit)
      from  25c0de8aa20f801212dda2dff23216289487d08f (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 162665e037ec2de3203e8ed34991b1f443462382
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Oct 12 14:53:53 2023 -0400

    20225: Add unit test to verify tree picker init / loadInitialValue preselects
    existing values on initialization
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/tree-picker/tree-picker-actions.test.ts b/src/store/tree-picker/tree-picker-actions.test.ts
new file mode 100644
index 00000000..b1c42409
--- /dev/null
+++ b/src/store/tree-picker/tree-picker-actions.test.ts
@@ -0,0 +1,185 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository, createServices } from "services/services";
+import { configureStore, RootStore } from "../store";
+import { createBrowserHistory } from "history";
+import { mockConfig } from 'common/config';
+import { ApiActions } from "services/api/api-actions";
+import Axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import { ResourceKind } from 'models/resource';
+import { SHARED_PROJECT_ID, initProjectsTreePicker } from "./tree-picker-actions";
+import { CollectionResource } from "models/collection";
+import { GroupResource } from "models/group";
+import { CollectionDirectory, CollectionFile, CollectionFileType } from "models/collection-file";
+
+describe('tree-picker-actions', () => {
+    const axiosInst = Axios.create({ headers: {} });
+    const axiosMock = new MockAdapter(axiosInst);
+
+    let store: RootStore;
+    let services: ServiceRepository;
+    const config: any = {
+
+
+    };
+    const actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => { },
+        errorFn: (id: string, message: string) => { }
+    };
+    let importMocks: any[];
+
+    beforeEach(() => {
+        axiosMock.reset();
+        services = createServices(mockConfig({}), actions, axiosInst);
+        store = configureStore(createBrowserHistory(), services, config);
+        localStorage.clear();
+        importMocks = [];
+    });
+
+    afterEach(() => {
+        importMocks.map(m => m.restore());
+    });
+
+    it('initializes preselected tree picker nodes', async () => {
+        const dispatchMock = jest.fn();
+        const dispatchWrapper = (action: any) => {
+            dispatchMock(action);
+            return store.dispatch(action);
+        };
+
+        const emptyCollectionUuid = "zzzzz-4zz18-000000000000000";
+        const collectionUuid = "zzzzz-4zz18-111111111111111";
+        const parentProjectUuid = "zzzzz-j7d0g-000000000000000";
+        const childCollectionUuid = "zzzzz-4zz18-222222222222222";
+
+        const fakeResources = {
+            [emptyCollectionUuid]: {
+                kind: ResourceKind.COLLECTION,
+                ownerUuid: '',
+                files: [],
+            },
+            [collectionUuid]: {
+                kind: ResourceKind.COLLECTION,
+                ownerUuid: '',
+                files: [{
+                    id: `${collectionUuid}/directory`,
+                    name: "directory",
+                    path: "",
+                    type: CollectionFileType.DIRECTORY,
+                    url: `/c=${collectionUuid}/directory/`,
+                }]
+            },
+            [parentProjectUuid]: {
+                kind: ResourceKind.GROUP,
+                ownerUuid: '',
+            },
+            [childCollectionUuid]: {
+                kind: ResourceKind.COLLECTION,
+                ownerUuid: parentProjectUuid,
+                files: [
+                    {
+                        id: `${childCollectionUuid}/mainDir`,
+                        name: "mainDir",
+                        path: "",
+                        type: CollectionFileType.DIRECTORY,
+                        url: `/c=${childCollectionUuid}/mainDir/`,
+                    },
+                    {
+                        id: `${childCollectionUuid}/mainDir/subDir`,
+                        name: "subDir",
+                        path: "/mainDir",
+                        type: CollectionFileType.DIRECTORY,
+                        url: `/c=${childCollectionUuid}/mainDir/subDir`,
+                    }
+                ],
+            },
+        };
+
+        services.ancestorsService.ancestors = jest.fn(async (startUuid, endUuid) => {
+            let ancestors: (GroupResource | CollectionResource)[] = [];
+            let uuid = startUuid;
+            while (uuid?.length && fakeResources[uuid]) {
+                const resource = fakeResources[uuid];
+                if (resource.kind === ResourceKind.COLLECTION) {
+                    ancestors.unshift({
+                        uuid, kind: resource.kind,
+                        ownerUuid: resource.ownerUuid,
+                    } as CollectionResource);
+                } else if (resource.kind === ResourceKind.GROUP) {
+                    ancestors.unshift({
+                        uuid, kind: resource.kind,
+                        ownerUuid: resource.ownerUuid,
+                    } as GroupResource);
+                }
+                uuid = resource.ownerUuid;
+            }
+            return ancestors;
+        });
+
+        services.collectionService.files = jest.fn(async (uuid): Promise<(CollectionDirectory | CollectionFile)[]> => {
+            return fakeResources[uuid]?.files || [];
+        });
+
+        const pickerId = "pickerId";
+
+        // When collection preselected
+        await initProjectsTreePicker(pickerId, {
+            selectedItemUuids: [emptyCollectionUuid],
+            includeDirectories: true,
+            includeFiles: false,
+            multi: true,
+        })(dispatchWrapper, store.getState, services);
+
+        // Expect ancestor service to be called
+        expect(services.ancestorsService.ancestors).toHaveBeenCalledWith(emptyCollectionUuid, '');
+        // Expect top level to be expanded and node to be selected
+        expect(store.getState().treePicker["pickerId_shared"][SHARED_PROJECT_ID].expanded).toBe(true);
+        expect(store.getState().treePicker["pickerId_shared"][emptyCollectionUuid].selected).toBe(true);
+
+
+        // When collection subdirectory is preselected
+        await initProjectsTreePicker(pickerId, {
+            selectedItemUuids: [`${collectionUuid}/directory`],
+            includeDirectories: true,
+            includeFiles: false,
+            multi: true,
+        })(dispatchWrapper, store.getState, services);
+
+        // Expect ancestor service to be called
+        expect(services.ancestorsService.ancestors).toHaveBeenCalledWith(collectionUuid, '');
+        // Expect top level to be expanded and node to be selected
+        expect(store.getState().treePicker["pickerId_shared"][SHARED_PROJECT_ID].expanded).toBe(true);
+        expect(store.getState().treePicker["pickerId_shared"][collectionUuid].expanded).toBe(true);
+        expect(store.getState().treePicker["pickerId_shared"][collectionUuid].selected).toBe(false);
+        expect(store.getState().treePicker["pickerId_shared"][`${collectionUuid}/directory`].selected).toBe(true);
+
+
+        // When subdirectory of collection inside project is preselected
+        await initProjectsTreePicker(pickerId, {
+            selectedItemUuids: [`${childCollectionUuid}/mainDir/subDir`],
+            includeDirectories: true,
+            includeFiles: false,
+            multi: true,
+        })(dispatchWrapper, store.getState, services);
+
+        // Expect ancestor service to be called
+        expect(services.ancestorsService.ancestors).toHaveBeenCalledWith(childCollectionUuid, '');
+        // Expect parent project and collection to be expanded
+        expect(store.getState().treePicker["pickerId_shared"][SHARED_PROJECT_ID].expanded).toBe(true);
+        expect(store.getState().treePicker["pickerId_shared"][parentProjectUuid].expanded).toBe(true);
+        expect(store.getState().treePicker["pickerId_shared"][parentProjectUuid].selected).toBe(false);
+        expect(store.getState().treePicker["pickerId_shared"][childCollectionUuid].expanded).toBe(true);
+        expect(store.getState().treePicker["pickerId_shared"][childCollectionUuid].selected).toBe(false);
+        // Expect main directory to be expanded
+        expect(store.getState().treePicker["pickerId_shared"][`${childCollectionUuid}/mainDir`].expanded).toBe(true);
+        expect(store.getState().treePicker["pickerId_shared"][`${childCollectionUuid}/mainDir`].selected).toBe(false);
+        // Expect sub directory to be selected
+        expect(store.getState().treePicker["pickerId_shared"][`${childCollectionUuid}/mainDir/subDir`].expanded).toBe(false);
+        expect(store.getState().treePicker["pickerId_shared"][`${childCollectionUuid}/mainDir/subDir`].selected).toBe(true);
+
+
+    });
+});

commit bc01781d2a6cbfed6d2bdf94397f97c60308eb64
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Oct 12 14:53:23 2023 -0400

    20225: Add cypress test to verify tree picker collection subdirectory selection
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/fixtures/workflow_directory_array.yaml b/cypress/fixtures/workflow_directory_array.yaml
new file mode 100644
index 00000000..fbdbd32c
--- /dev/null
+++ b/cypress/fixtures/workflow_directory_array.yaml
@@ -0,0 +1,20 @@
+# 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/directoryInputName"
+    type:
+      items: Directory
+      type: array
+  outputs: []
+  steps: []
+cwlVersion: v1.2
diff --git a/cypress/integration/create-workflow.spec.js b/cypress/integration/create-workflow.spec.js
index df50a875..28e85b51 100644
--- a/cypress/integration/create-workflow.spec.js
+++ b/cypress/integration/create-workflow.spec.js
@@ -204,4 +204,81 @@ describe('Create workflow tests', function () {
                     });
             });
     }));
+
+    it('allows selecting collection subdirectories and reselects existing selections', () => {
+        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: "./subdir/dir1 d41d8cd98f00b204e9800998ecf8427e+0+A8f0be20f1a6e28cf4e2c034dc3d4a02a49bebe7e at 653aae59 0:0:\\056\n./subdir/dir2 d41d8cd98f00b204e9800998ecf8427e+0+A8f0be20f1a6e28cf4e2c034dc3d4a02a49bebe7e at 653aae59 0:0:\\056\n"
+        })
+            .as('testCollection');
+
+        cy.getAll('@myProject1', '@testCollection')
+            .then(function ([myProject1, testCollection]) {
+                cy.readFile('cypress/fixtures/workflow_directory_array.yaml').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 workflow').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('directoryInputName').parent('div').find('input').click();
+                        cy.get('div[role=dialog]')
+                            .within(() => {
+                                // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
+                                cy.get('p').contains('Home Projects').closest('ul')
+                                    .find('i')
+                                    .then(el => el.click());
+
+                                cy.get(`[data-id=${testCollection.uuid}]`)
+                                    .find('i').click();
+
+                                cy.get(`[data-id="${testCollection.uuid}/subdir"]`)
+                                    .find('i').click();
+
+                                cy.contains('dir1').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').click();
+                                cy.contains('dir2').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').click();
+
+                                cy.get('[data-cy=ok-button]').click();
+                            });
+
+                        // Verify subdirectories were selected
+                        cy.get('label').contains('directoryInputName').parent('div')
+                            .within(() => {
+                                cy.contains('dir1');
+                                cy.contains('dir2');
+                            });
+
+                        // Reopen tree picker and verify subdirectories are preselected
+                        cy.get('label').contains('directoryInputName').parent('div').find('input').click();
+                        cy.waitForDom().get('div[role=dialog]')
+                            .within(() => {
+                                cy.contains('dir1').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').should('be.checked');
+                                cy.contains('dir2').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').should('be.checked');
+                            });
+                    });
+
+            });
+    })
 })

commit 084ea0c0c8e078dc006e19d2aa851a30817b01c8
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Oct 12 14:51:14 2023 -0400

    20225: Trigger error if tree picker loadInitialValue fails to get any ancestors
    from ancestor service, also log the failed uuid in the console.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/tree-picker/tree-picker-actions.ts b/src/store/tree-picker/tree-picker-actions.ts
index 9474cf4a..883847d8 100644
--- a/src/store/tree-picker/tree-picker-actions.ts
+++ b/src/store/tree-picker/tree-picker-actions.ts
@@ -337,12 +337,17 @@ export const loadInitialValue = (pickerItemIds: string[], pickerId: string, incl
         // Request ancestor trees in paralell and save home project status
         const pickerItemsData: PickerItemPreloadData[] = await Promise.allSettled(pickerItemIds.map(async itemId => {
             const mainItemUuid = itemId.includes('/') ? itemId.split('/')[0] : itemId;
+
             const ancestors = (await services.ancestorsService.ancestors(mainItemUuid, ''))
             .filter(item =>
                 item.kind === ResourceKind.GROUP ||
                 item.kind === ResourceKind.COLLECTION
             ) as (GroupResource | CollectionResource)[];
 
+            if (ancestors.length === 0) {
+                return Promise.reject({item: itemId});
+            }
+
             const isHomeProjectItem = !!(homeUuid && ancestors.some(item => item.ownerUuid === homeUuid));
 
             return {
@@ -353,8 +358,12 @@ export const loadInitialValue = (pickerItemIds: string[], pickerId: string, incl
             };
         })).then((res) => {
             // Show toast if any selections failed to restore
-            if (res.find((promiseResult): promiseResult is PromiseRejectedResult => (promiseResult.status === 'rejected'))) {
-                dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Some selections failed to load and were removed`, kind: SnackbarKind.ERROR }));
+            const rejectedPromises = res.filter((promiseResult): promiseResult is PromiseRejectedResult => (promiseResult.status === 'rejected'));
+            if (rejectedPromises.length) {
+                rejectedPromises.forEach(item => {
+                    console.error("The following item failed to load into the tree picker", item.reason);
+                });
+                dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Some selections failed to load and were removed. See console for details.`, kind: SnackbarKind.ERROR }));
             }
             // Filter out any failed promises and map to resulting preload data with ancestors
             return res.filter((promiseResult): promiseResult is PromiseFulfilledResult<PickerItemPreloadData> => (

commit aa7fdb114d208a401980f3001e7adcb252bdc95c
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Oct 12 14:50:02 2023 -0400

    20225: Catch tree picker loadproject errors and show toast / console error with project uuid
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/tree-picker/tree-picker-actions.ts b/src/store/tree-picker/tree-picker-actions.ts
index 7b526710..9474cf4a 100644
--- a/src/store/tree-picker/tree-picker-actions.ts
+++ b/src/store/tree-picker/tree-picker-actions.ts
@@ -110,7 +110,7 @@ export const initProjectsTreePicker = (pickerId: string, preloadParams?: TreePic
         dispatch<any>(initSearchProject(search));
 
         if (preloadParams && preloadParams.selectedItemUuids.length) {
-            dispatch<any>(loadInitialValue(
+            await dispatch<any>(loadInitialValue(
                 preloadParams.selectedItemUuids,
                 pickerId,
                 preloadParams.includeDirectories,
@@ -145,6 +145,10 @@ interface LoadProjectParamsWithId extends LoadProjectParams {
     searchProjects?: boolean;
 }
 
+/**
+ * loadProject is used to load or refresh a project node in a tree picker
+ *   Errors are caught and a toast is shown if the project fails to load
+ */
 export const loadProject = (params: LoadProjectParamsWithId) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const {
@@ -183,57 +187,62 @@ export const loadProject = (params: LoadProjectParamsWithId) =>
 
         const itemLimit = 200;
 
-        const { items, itemsAvailable } = await services.groupsService.contents((loadShared || searchProjects) ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: itemLimit });
-        dispatch<any>(updateResources(items));
-
-        if (itemsAvailable > itemLimit) {
-            items.push({
-                uuid: "more-items-available",
-                kind: ResourceKind.WORKFLOW,
-                name: `*** Not all items listed (${items.length} out of ${itemsAvailable}), reduce item count with search or filter ***`,
-                description: "",
-                definition: "",
-                ownerUuid: "",
-                createdAt: "",
-                modifiedByClientUuid: "",
-                modifiedByUserUuid: "",
-                modifiedAt: "",
-                href: "",
-                etag: ""
-            });
-        }
-
-        dispatch<any>(receiveTreePickerData<GroupContentsResource>({
-            id,
-            pickerId,
-            data: items.filter((item) => {
-                if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
-                    return false;
-                }
+        try {
+            const { items, itemsAvailable } = await services.groupsService.contents((loadShared || searchProjects) ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: itemLimit });
+            dispatch<any>(updateResources(items));
+
+            if (itemsAvailable > itemLimit) {
+                items.push({
+                    uuid: "more-items-available",
+                    kind: ResourceKind.WORKFLOW,
+                    name: `*** Not all items listed (${items.length} out of ${itemsAvailable}), reduce item count with search or filter ***`,
+                    description: "",
+                    definition: "",
+                    ownerUuid: "",
+                    createdAt: "",
+                    modifiedByClientUuid: "",
+                    modifiedByUserUuid: "",
+                    modifiedAt: "",
+                    href: "",
+                    etag: ""
+                });
+            }
 
-                if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
-                    return false;
-                }
+            dispatch<any>(receiveTreePickerData<GroupContentsResource>({
+                id,
+                pickerId,
+                data: items.filter((item) => {
+                    if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
+                        return false;
+                    }
 
-                return true;
-            }),
-            extractNodeData: item => (
-                item.uuid === "more-items-available" ?
-                    {
-                        id: item.uuid,
-                        value: item,
-                        status: TreeNodeStatus.LOADED
+                    if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
+                        return false;
                     }
-                    : {
-                        id: item.uuid,
-                        value: item,
-                        status: item.kind === ResourceKind.PROJECT
-                            ? TreeNodeStatus.INITIAL
-                            : includeDirectories || includeFiles
+
+                    return true;
+                }),
+                extractNodeData: item => (
+                    item.uuid === "more-items-available" ?
+                        {
+                            id: item.uuid,
+                            value: item,
+                            status: TreeNodeStatus.LOADED
+                        }
+                        : {
+                            id: item.uuid,
+                            value: item,
+                            status: item.kind === ResourceKind.PROJECT
                                 ? TreeNodeStatus.INITIAL
-                                : TreeNodeStatus.LOADED
-                    }),
-        }));
+                                : includeDirectories || includeFiles
+                                    ? TreeNodeStatus.INITIAL
+                                    : TreeNodeStatus.LOADED
+                        }),
+            }));
+        } catch(e) {
+            console.error("Failed to load project into tree picker:", e);;
+            dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to load project`, kind: SnackbarKind.ERROR }));
+        }
     };
 
 export const loadCollection = (id: string, pickerId: string, includeDirectories?: boolean, includeFiles?: boolean) =>

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list