20225: Add subdirectory selection support to directory array picker
authorStephen Smith <stephen@curii.com>
Fri, 22 Sep 2023 14:12:42 +0000 (10:12 -0400)
committerStephen Smith <stephen@curii.com>
Fri, 22 Sep 2023 14:37:09 +0000 (10:37 -0400)
Adds cascade flag to tree picker to disable recursive directory selection

Reworks initProjectsTreePicker to support initializing multiple selections.
Loads each selection's ancestor tree in parallel, combines updates to same tree
pickers before inserting the ancestor tree, then loads necessary collections in
parallel

Changes checkbox visibility logic to show collection selection checkbox even
when collection is not loaded/expanded when cascade mode is off - since the
selection won't cascade there is no need to require the collection to be opened

Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

14 files changed:
src/components/data-table-filters/data-table-filters-tree.tsx
src/models/tree.ts
src/store/resource-type-filters/resource-type-filters.test.ts
src/store/tree-picker/tree-picker-actions.ts
src/store/tree-picker/tree-picker-reducer.test.ts
src/store/tree-picker/tree-picker-reducer.ts
src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx
src/views-components/projects-tree-picker/projects-tree-picker.tsx
src/views-components/projects-tree-picker/tree-picker-field.tsx
src/views/run-process-panel/inputs/directory-array-input.tsx
src/views/run-process-panel/inputs/directory-input.tsx
src/views/run-process-panel/inputs/file-array-input.tsx
src/views/run-process-panel/inputs/file-input.tsx
src/views/run-process-panel/inputs/project-input.tsx

index 7b97865bba4b546085d6c90ab7cb68a7b5821e45..d52b58f5ae30ac6f09ed533a0e52e4213c13a8c7 100644 (file)
@@ -59,14 +59,14 @@ export class DataTableFiltersTree extends React.Component<DataTableFilterProps>
         if (item.selected) { return; }
 
         // Otherwise select this node and deselect the others
-        const filters = selectNode(item.id)(this.props.filters);
+        const filters = selectNode(item.id, true)(this.props.filters);
         const toDeselect = Object.keys(this.props.filters).filter((id) => (id !== item.id));
-        onChange(deselectNodes(toDeselect)(filters));
+        onChange(deselectNodes(toDeselect, true)(filters));
     }
 
     toggleFilter = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
         const { onChange = noop } = this.props;
-        onChange(toggleNodeSelection(item.id)(this.props.filters));
+        onChange(toggleNodeSelection(item.id, true)(this.props.filters));
     }
 
     toggleOpen = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
index 996f98a465865ee5fd0861bcc391a505735ef115..aeb415411e8102a66acfcf11fbc51c6cb42d29ac 100644 (file)
@@ -138,6 +138,11 @@ export const deactivateNode = <T>(tree: Tree<T>) =>
 export const expandNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
     mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree);
 
+export const expandNodeAncestors = (...ids: string[]) => <T>(tree: Tree<T>) => {
+    const ancestors = ids.reduce((acc, id): string[] => ([...acc, ...getNodeAncestorsIds(id)(tree)]), [] as string[]);
+    return mapTree((node: TreeNode<T>) => ancestors.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree);
+}
+
 export const collapseNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
     mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree);
 
@@ -151,37 +156,40 @@ export const setNodeStatus = (id: string) => (status: TreeNodeStatus) => <T>(tre
         : tree;
 };
 
-export const toggleNodeSelection = (id: string) => <T>(tree: Tree<T>) => {
+export const toggleNodeSelection = (id: string, cascade: boolean) => <T>(tree: Tree<T>) => {
     const node = getNode(id)(tree);
+
     return node
-        ? pipe(
-            setNode({ ...node, selected: !node.selected }),
-            toggleAncestorsSelection(id),
-            toggleDescendantsSelection(id))(tree)
+        ? cascade
+            ? pipe(
+                setNode({ ...node, selected: !node.selected }),
+                toggleAncestorsSelection(id),
+                toggleDescendantsSelection(id))(tree)
+            : setNode({ ...node, selected: !node.selected })(tree)
         : tree;
 };
 
-export const selectNode = (id: string) => <T>(tree: Tree<T>) => {
+export const selectNode = (id: string, cascade: boolean) => <T>(tree: Tree<T>) => {
     const node = getNode(id)(tree);
     return node && node.selected
         ? tree
-        : toggleNodeSelection(id)(tree);
+        : toggleNodeSelection(id, cascade)(tree);
 };
 
-export const selectNodes = (id: string | string[]) => <T>(tree: Tree<T>) => {
+export const selectNodes = (id: string | string[], cascade: boolean) => <T>(tree: Tree<T>) => {
     const ids = typeof id === 'string' ? [id] : id;
-    return ids.reduce((tree, id) => selectNode(id)(tree), tree);
+    return ids.reduce((tree, id) => selectNode(id, cascade)(tree), tree);
 };
-export const deselectNode = (id: string) => <T>(tree: Tree<T>) => {
+export const deselectNode = (id: string, cascade: boolean) => <T>(tree: Tree<T>) => {
     const node = getNode(id)(tree);
     return node && node.selected
-        ? toggleNodeSelection(id)(tree)
+        ? toggleNodeSelection(id, cascade)(tree)
         : tree;
 };
 
-export const deselectNodes = (id: string | string[]) => <T>(tree: Tree<T>) => {
+export const deselectNodes = (id: string | string[], cascade: boolean) => <T>(tree: Tree<T>) => {
     const ids = typeof id === 'string' ? [id] : id;
-    return ids.reduce((tree, id) => deselectNode(id)(tree), tree);
+    return ids.reduce((tree, id) => deselectNode(id, cascade)(tree), tree);
 };
 
 export const getSelectedNodes = <T>(tree: Tree<T>) =>
index de231d66b45a526a9c9b50f24b1c653dc84dece1..216a59c72c2236ab3fcbb820e4d2ec836b0ecdf7 100644 (file)
@@ -35,7 +35,7 @@ describe("serializeResourceTypeFilters", () => {
     });
 
     it("should serialize all but collection filters", () => {
-        const filters = deselectNode(ObjectTypeFilter.COLLECTION)(getInitialResourceTypeFilters());
+        const filters = deselectNode(ObjectTypeFilter.COLLECTION, true)(getInitialResourceTypeFilters());
         const serializedFilters = serializeResourceTypeFilters(filters);
         expect(serializedFilters)
             .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.WORKFLOW}","${ResourceKind.PROCESS}"]],["container_requests.requesting_container_uuid","=",null]`);
@@ -44,11 +44,11 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize output collections and projects", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.DEFINITION),
-            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
-            deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
-            deselectNode(CollectionTypeFilter.LOG_COLLECTION),
-            deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION),
+            deselectNode(ObjectTypeFilter.DEFINITION, true),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+            deselectNode(CollectionTypeFilter.GENERAL_COLLECTION, true),
+            deselectNode(CollectionTypeFilter.LOG_COLLECTION, true),
+            deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION, true),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -59,11 +59,11 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize output collections and projects", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.DEFINITION),
-            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
-            deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
-            deselectNode(CollectionTypeFilter.LOG_COLLECTION),
-            deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION),
+            deselectNode(ObjectTypeFilter.DEFINITION, true),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+            deselectNode(CollectionTypeFilter.GENERAL_COLLECTION, true),
+            deselectNode(CollectionTypeFilter.LOG_COLLECTION, true),
+            deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION, true),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -74,10 +74,10 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize general collections", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.PROJECT),
-            deselectNode(ObjectTypeFilter.DEFINITION),
-            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
-            deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION)
+            deselectNode(ObjectTypeFilter.PROJECT, true),
+            deselectNode(ObjectTypeFilter.DEFINITION, true),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+            deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION, true)
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -88,10 +88,10 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize only main processes", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.PROJECT),
-            deselectNode(ProcessTypeFilter.CHILD_PROCESS),
-            deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ObjectTypeFilter.PROJECT, true),
+            deselectNode(ProcessTypeFilter.CHILD_PROCESS, true),
+            deselectNode(ObjectTypeFilter.COLLECTION, true),
+            deselectNode(ObjectTypeFilter.DEFINITION, true),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -102,12 +102,12 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize only child processes", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.PROJECT),
-            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
-            deselectNode(ObjectTypeFilter.DEFINITION),
-            deselectNode(ObjectTypeFilter.COLLECTION),
+            deselectNode(ObjectTypeFilter.PROJECT, true),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+            deselectNode(ObjectTypeFilter.DEFINITION, true),
+            deselectNode(ObjectTypeFilter.COLLECTION, true),
 
-            selectNode(ProcessTypeFilter.CHILD_PROCESS),
+            selectNode(ProcessTypeFilter.CHILD_PROCESS, true),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -118,9 +118,9 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize all project types", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.DEFINITION),
-            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
+            deselectNode(ObjectTypeFilter.COLLECTION, true),
+            deselectNode(ObjectTypeFilter.DEFINITION, true),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -131,10 +131,10 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize filter groups", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(GroupTypeFilter.PROJECT),
-            deselectNode(ObjectTypeFilter.DEFINITION),
-            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
-            deselectNode(ObjectTypeFilter.COLLECTION),
+            deselectNode(GroupTypeFilter.PROJECT, true),
+            deselectNode(ObjectTypeFilter.DEFINITION, true),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+            deselectNode(ObjectTypeFilter.COLLECTION, true),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -145,10 +145,10 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize projects (normal)", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(GroupTypeFilter.FILTER_GROUP),
-            deselectNode(ObjectTypeFilter.DEFINITION),
-            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
-            deselectNode(ObjectTypeFilter.COLLECTION),
+            deselectNode(GroupTypeFilter.FILTER_GROUP, true),
+            deselectNode(ObjectTypeFilter.DEFINITION, true),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+            deselectNode(ObjectTypeFilter.COLLECTION, true),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
index 18385e31ef14da66aec69302574793e00c96c22e..7b526710713738192f27e99457f9c17f37c29a2e 100644 (file)
@@ -25,6 +25,7 @@ import { GroupClass, GroupResource } from "models/group";
 import { CollectionResource } from "models/collection";
 import { getResource } from "store/resources/resources";
 import { updateResources } from "store/resources/resources-actions";
+import { SnackbarKind, snackbarActions } from "store/snackbar/snackbar-actions";
 
 export const treePickerActions = unionize({
     LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
@@ -32,11 +33,12 @@ export const treePickerActions = unionize({
     APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
     TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
     EXPAND_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
+    EXPAND_TREE_PICKER_NODE_ANCESTORS: ofType<{ id: string, pickerId: string }>(),
     ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
     DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
-    TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
-    SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
-    DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
+    TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string, cascade: boolean }>(),
+    SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string, cascade: boolean }>(),
+    DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string, cascade: boolean }>(),
     EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
     RESET_TREE_PICKER: ofType<{ pickerId: string }>()
 });
@@ -91,7 +93,14 @@ export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Va
 export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
     getAllNodes<Value>(pickerId, node => node.selected)(state);
 
-export const initProjectsTreePicker = (pickerId: string, selectedItemUuid?: string) =>
+interface TreePickerPreloadParams {
+    selectedItemUuids: string[];
+    includeDirectories: boolean;
+    includeFiles: boolean;
+    multi: boolean;
+}
+
+export const initProjectsTreePicker = (pickerId: string, preloadParams?: TreePickerPreloadParams) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
         dispatch<any>(initUserProject(home));
@@ -100,8 +109,14 @@ export const initProjectsTreePicker = (pickerId: string, selectedItemUuid?: stri
         dispatch<any>(initPublicFavoritesProject(publicFavorites));
         dispatch<any>(initSearchProject(search));
 
-        if (selectedItemUuid) {
-            dispatch<any>(loadInitialValue(selectedItemUuid, pickerId));
+        if (preloadParams && preloadParams.selectedItemUuids.length) {
+            dispatch<any>(loadInitialValue(
+                preloadParams.selectedItemUuids,
+                pickerId,
+                preloadParams.includeDirectories,
+                preloadParams.includeFiles,
+                preloadParams.multi
+            ));
         }
     };
 
@@ -239,13 +254,15 @@ export const loadCollection = (id: string, pickerId: string, includeDirectories?
                 const sorted = sortFilesTree(tree);
                 const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
 
-                dispatch(
+                // await tree modifications so that consumers can guarantee node presence
+                await dispatch(
                     treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
                         id,
                         pickerId,
                         subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
                     }));
 
+                // Expand collection root node
                 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
             }
         }
@@ -291,35 +308,125 @@ export const initSharedProject = (pickerId: string) =>
         }));
     };
 
-export const loadInitialValue = (initialValue: string, pickerId: string) =>
+type PickerItemPreloadData = {
+    itemId: string;
+    mainItemUuid: string;
+    ancestors: (GroupResource | CollectionResource)[];
+    isHomeProjectItem: boolean;
+}
+
+type PickerTreePreloadData = {
+    tree: Tree<GroupResource | CollectionResource>;
+    pickerTreeId: string;
+    pickerTreeRootUuid: string;
+};
+
+export const loadInitialValue = (pickerItemIds: string[], pickerId: string, includeDirectories: boolean, includeFiles: boolean, multi: boolean,) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const { home, shared } = getProjectsTreePickerIds(pickerId);
         const homeUuid = getUserUuid(getState());
-        const ancestors = (await services.ancestorsService.ancestors(initialValue, ''))
+
+        // 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) {
-            const isUserHomeProject = !!(homeUuid && ancestors.some(item => item.ownerUuid === homeUuid));
-            const pickerTreeId = isUserHomeProject ? home : shared;
-            const pickerTreeRootUuid: string = (homeUuid && isUserHomeProject) ? homeUuid : SHARED_PROJECT_ID;
+            const isHomeProjectItem = !!(homeUuid && ancestors.some(item => item.ownerUuid === homeUuid));
 
-            ancestors[0].ownerUuid = '';
-            const tree = createInitialLocationTree(ancestors, initialValue);
+            return {
+                itemId,
+                mainItemUuid,
+                ancestors,
+                isHomeProjectItem,
+            };
+        })).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 }));
+            }
+            // Filter out any failed promises and map to resulting preload data with ancestors
+            return res.filter((promiseResult): promiseResult is PromiseFulfilledResult<PickerItemPreloadData> => (
+                promiseResult.status === 'fulfilled'
+            )).map(res => res.value)
+        });
+
+        // Group items to preload / ancestor data by home/shared picker and create initial Trees to preload
+        const initialTreePreloadData: PickerTreePreloadData[] = [
+            pickerItemsData.filter((item) => item.isHomeProjectItem),
+            pickerItemsData.filter((item) => !item.isHomeProjectItem),
+        ]
+            .filter((items) => items.length > 0)
+            .map((itemGroup) =>
+                itemGroup.reduce(
+                    (preloadTree, itemData) => ({
+                        tree: createInitialPickerTree(
+                            itemData.ancestors,
+                            itemData.mainItemUuid,
+                            preloadTree.tree
+                        ),
+                        pickerTreeId: getPickerItemTreeId(itemData, homeUuid, pickerId),
+                        pickerTreeRootUuid: getPickerItemRootUuid(itemData, homeUuid),
+                    }),
+                    {
+                        tree: createTree<GroupResource | CollectionResource>(),
+                        pickerTreeId: '',
+                        pickerTreeRootUuid: '',
+                    } as PickerTreePreloadData
+                )
+            );
+
+        // Load initial trees into corresponding picker store
+        await Promise.all(initialTreePreloadData.map(preloadTree => (
             dispatch(
                 treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
-                    id: pickerTreeRootUuid,
-                    pickerId: pickerTreeId,
-                    subtree: tree
-                }));
-            dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: initialValue, pickerId: pickerTreeId }));
-            dispatch(treePickerSearchActions.REFRESH_TREE_PICKER({ pickerId: pickerTreeId }));
-        }
+                    id: preloadTree.pickerTreeRootUuid,
+                    pickerId: preloadTree.pickerTreeId,
+                    subtree: preloadTree.tree,
+                })
+            )
+        )));
+
+        // Await loading collection before attempting to select items
+        await Promise.all(pickerItemsData.map(async itemData => {
+            const pickerTreeId = getPickerItemTreeId(itemData, homeUuid, pickerId);
+
+            // Selected item resides in collection subpath
+            if (itemData.itemId.includes('/')) {
+                // Load collection into tree
+                // loadCollection includes more than dispatched actions and must be awaited
+                await dispatch(loadCollection(itemData.mainItemUuid, pickerTreeId, includeDirectories, includeFiles));
+            }
+            // Expand nodes down to destination
+            dispatch(treePickerActions.EXPAND_TREE_PICKER_NODE_ANCESTORS({ id: itemData.itemId, pickerId: pickerTreeId }));
+        }));
+
+        // Select or activate nodes
+        pickerItemsData.forEach(itemData => {
+            const pickerTreeId = getPickerItemTreeId(itemData, homeUuid, pickerId);
+
+            if (multi) {
+                dispatch(treePickerActions.SELECT_TREE_PICKER_NODE({ id: itemData.itemId, pickerId: pickerTreeId, cascade: false}));
+            } else {
+                dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: itemData.itemId, pickerId: pickerTreeId }));
+            }
+        });
 
+        // Refresh triggers loading in all adjacent items that were not included in the ancestor tree
+        await initialTreePreloadData.map(preloadTree => dispatch(treePickerSearchActions.REFRESH_TREE_PICKER({ pickerId: preloadTree.pickerTreeId })));
     }
 
+const getPickerItemTreeId = (itemData: PickerItemPreloadData, homeUuid: string | undefined, pickerId: string) => {
+    const { home, shared } = getProjectsTreePickerIds(pickerId);
+    return ((itemData.isHomeProjectItem && homeUuid) ? home : shared);
+};
+
+const getPickerItemRootUuid = (itemData: PickerItemPreloadData, homeUuid: string | undefined) => {
+    return (itemData.isHomeProjectItem && homeUuid) ? homeUuid : SHARED_PROJECT_ID;
+};
+
 export const FAVORITES_PROJECT_ID = 'Favorites';
 export const initFavoritesProject = (pickerId: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
@@ -561,18 +668,33 @@ export const getFileOperationLocation = (item: ProjectsTreePickerItem) =>
 
 /**
  * Create an expanded tree picker subtree from array of nested projects/collection
- *   Assumes the root item of the subtree already has an empty string ownerUuid
+ *   First item is assumed to be root and gets empty parent id
+ *   Nodes must be sorted from top down to prevent orphaned nodes
  */
-export const createInitialLocationTree = (data: Array<GroupResource | CollectionResource>, tailUuid: string) => {
-    return data
-        .reduce((tree, item) => setNode({
-            children: [],
-            id: item.uuid,
-            parent: item.ownerUuid,
-            value: item,
-            active: false,
-            selected: false,
-            expanded: false,
-            status: item.uuid !== tailUuid ? TreeNodeStatus.LOADED : TreeNodeStatus.INITIAL,
-        })(tree), createTree<GroupResource | CollectionResource>());
+export const createInitialPickerTree = (sortedAncestors: Array<GroupResource | CollectionResource>, tailUuid: string, initialTree: Tree<GroupResource | CollectionResource>) => {
+    return sortedAncestors
+        .reduce((tree, item, index) => {
+            if (getNode(item.uuid)(tree)) {
+                return tree;
+            } else {
+                return setNode({
+                    children: [],
+                    id: item.uuid,
+                    parent: index === 0 ? '' : item.ownerUuid,
+                    value: item,
+                    active: false,
+                    selected: false,
+                    expanded: false,
+                    status: item.uuid !== tailUuid ? TreeNodeStatus.LOADED : TreeNodeStatus.INITIAL,
+                })(tree);
+            }
+        }, initialTree);
 };
+
+export const fileOperationLocationToPickerId = (location: FileOperationLocation): string => {
+    let id = location.uuid;
+    if (location.subpath.length && location.subpath !== '/') {
+        id = id + location.subpath;
+    }
+    return id;
+}
index 25973bf6b5945e1505bc15a527dc8827feafcb83..2a5229ca5b567707665c9c189452370fcdda221e 100644 (file)
@@ -93,7 +93,7 @@ describe('TreePickerReducer', () => {
         const newState = pipe(
             (state: TreePicker) => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node], pickerId: "projects" })),
             state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '1', nodes: [subNode], pickerId: "projects" })),
-            state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: '1.1', pickerId: "projects" })),
+            state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: '1.1', pickerId: "projects", cascade: true })),
         )({ projects: createTree<{}>() });
         expect(getNode('1')(newState.projects)).toEqual({
             ...initTreeNode({ id: '1', value: '1' }),
index df0ee0ad167376af2eec2a81293ade246714ba96..84d5ed0ca729013f9d9215d1c7dcffd21f1730ed 100644 (file)
@@ -5,7 +5,7 @@
 import {
     createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus,
     expandNode, deactivateNode, selectNodes, deselectNodes,
-    activateNode, getNode, toggleNodeCollapse, toggleNodeSelection, appendSubtree
+    activateNode, getNode, toggleNodeCollapse, toggleNodeSelection, appendSubtree, expandNodeAncestors
 } from 'models/tree';
 import { TreePicker } from "./tree-picker";
 import { treePickerActions, treePickerSearchActions, TreePickerAction, TreePickerSearchAction, LoadProjectParams } from "./tree-picker-actions";
@@ -29,6 +29,9 @@ export const treePickerReducer = (state: TreePicker = {}, action: TreePickerActi
         EXPAND_TREE_PICKER_NODE: ({ id, pickerId }) =>
             updateOrCreatePicker(state, pickerId, expandNode(id)),
 
+        EXPAND_TREE_PICKER_NODE_ANCESTORS: ({ id, pickerId }) =>
+            updateOrCreatePicker(state, pickerId, expandNodeAncestors(id)),
+
         ACTIVATE_TREE_PICKER_NODE: ({ id, pickerId, relatedTreePickers = [] }) =>
             pipe(
                 () => relatedTreePickers.reduce(
@@ -41,14 +44,14 @@ export const treePickerReducer = (state: TreePicker = {}, action: TreePickerActi
         DEACTIVATE_TREE_PICKER_NODE: ({ pickerId }) =>
             updateOrCreatePicker(state, pickerId, deactivateNode),
 
-        TOGGLE_TREE_PICKER_NODE_SELECTION: ({ id, pickerId }) =>
-            updateOrCreatePicker(state, pickerId, toggleNodeSelection(id)),
+        TOGGLE_TREE_PICKER_NODE_SELECTION: ({ id, pickerId, cascade }) =>
+            updateOrCreatePicker(state, pickerId, toggleNodeSelection(id, cascade)),
 
-        SELECT_TREE_PICKER_NODE: ({ id, pickerId }) =>
-            updateOrCreatePicker(state, pickerId, selectNodes(id)),
+        SELECT_TREE_PICKER_NODE: ({ id, pickerId, cascade }) =>
+            updateOrCreatePicker(state, pickerId, selectNodes(id, cascade)),
 
-        DESELECT_TREE_PICKER_NODE: ({ id, pickerId }) =>
-            updateOrCreatePicker(state, pickerId, deselectNodes(id)),
+        DESELECT_TREE_PICKER_NODE: ({ id, pickerId, cascade }) =>
+            updateOrCreatePicker(state, pickerId, deselectNodes(id, cascade)),
 
         RESET_TREE_PICKER: ({ pickerId }) =>
             updateOrCreatePicker(state, pickerId, createTree),
index 1ed2a5511def60b3cd00cdce63fbfce9c5070ba9..70797f3165eace32a1a6cef81d409f753fc4fe5d 100644 (file)
@@ -21,6 +21,7 @@ import { CollectionFileType } from 'models/collection-file';
 type PickedTreePickerProps = Pick<TreePickerProps<ProjectsTreePickerItem>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
 
 export interface ProjectsTreePickerDataProps {
+    cascadeSelection: boolean;
     includeCollections?: boolean;
     includeDirectories?: boolean;
     includeFiles?: boolean;
@@ -35,9 +36,9 @@ export interface ProjectsTreePickerDataProps {
 
 export type ProjectsTreePickerProps = ProjectsTreePickerDataProps & Partial<PickedTreePickerProps>;
 
-const mapStateToProps = (_: any, { rootItemIcon, showSelection }: ProjectsTreePickerProps) => ({
+const mapStateToProps = (_: any, { rootItemIcon, showSelection, cascadeSelection }: ProjectsTreePickerProps) => ({
     render: renderTreeItem(rootItemIcon),
-    showSelection: isSelectionVisible(showSelection),
+    showSelection: isSelectionVisible(showSelection, cascadeSelection),
 });
 
 const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollections, includeDirectories, includeFiles, relatedTreePickers, options, ...props }: ProjectsTreePickerProps): PickedTreePickerProps => ({
@@ -71,7 +72,7 @@ const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollectio
         }
     },
     toggleItemSelection: (event, item, pickerId) => {
-        dispatch<any>(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: item.id, pickerId }));
+        dispatch<any>(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: item.id, pickerId, cascade: props.cascadeSelection }));
         if (props.toggleItemSelection) {
             props.toggleItemSelection(event, item, pickerId);
         }
@@ -108,11 +109,14 @@ const getProjectPickerIcon = ({ data }: TreeItem<ProjectsTreePickerItem>, rootIc
     }
 };
 
-const isSelectionVisible = (shouldBeVisible?: boolean) =>
-    ({ status, items }: TreeItem<ProjectsTreePickerItem>): boolean => {
+const isSelectionVisible = (shouldBeVisible: boolean | undefined, cascadeSelection: boolean) =>
+    ({ status, items, data }: TreeItem<ProjectsTreePickerItem>): boolean => {
         if (shouldBeVisible) {
-            if (items && items.length > 0) {
-                return items.every(isSelectionVisible(shouldBeVisible));
+            if (!cascadeSelection && 'kind' in data && data.kind === ResourceKind.COLLECTION) {
+                // In non-casecade mode collections are selectable without being loaded
+                return true;
+            } else if (items && items.length > 0) {
+                return items.every(isSelectionVisible(shouldBeVisible, cascadeSelection));
             }
             return status === TreeItemStatus.LOADED;
         }
index 773230d351fe1e491faa5f4dab49a81f375aff84..16f6cceb71ce44b711c5214157c48ddaff4d3061 100644 (file)
@@ -23,8 +23,9 @@ import { withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
 
 export interface ToplevelPickerProps {
-    currentUuid?: string;
+    currentUuids?: string[];
     pickerId: string;
+    cascadeSelection: boolean;
     includeCollections?: boolean;
     includeDirectories?: boolean;
     includeFiles?: boolean;
@@ -107,7 +108,13 @@ export const ProjectsTreePicker = connect(mapStateToProps, mapDispatchToProps)(
             componentDidMount() {
                 const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(this.props.pickerId);
 
-                this.props.dispatch<any>(initProjectsTreePicker(this.props.pickerId, this.props.currentUuid));
+                const preloadParams = this.props.currentUuids ? {
+                    selectedItemUuids: this.props.currentUuids,
+                    includeDirectories: !!this.props.includeDirectories,
+                    includeFiles: !!this.props.includeFiles,
+                    multi: !!this.props.showSelection,
+                } : undefined;
+                this.props.dispatch<any>(initProjectsTreePicker(this.props.pickerId, preloadParams));
 
                 this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_PROJECT_SEARCH({ pickerId: search, projectSearchValue: "" }));
                 this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: search, collectionFilterValue: "" }));
@@ -135,6 +142,7 @@ export const ProjectsTreePicker = connect(mapStateToProps, mapDispatchToProps)(
                 const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
                 const relatedTreePickers = getRelatedTreePickers(pickerId);
                 const p = {
+                    cascadeSelection: this.props.cascadeSelection,
                     includeCollections: this.props.includeCollections,
                     includeDirectories: this.props.includeDirectories,
                     includeFiles: this.props.includeFiles,
index 793eeaa3e60598261a6b9f63bf5cc2cf8e85b265..75cf40c641bbe195e0c8ef02c4c2875d3adbb625 100644 (file)
@@ -19,6 +19,7 @@ export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp)
             <ProjectsTreePicker
                 pickerId={props.pickerId}
                 toggleItemActive={handleChange(props)}
+                cascadeSelection={false}
                 options={{ showOnlyOwned: false, showOnlyWritable: true }} />
             {props.meta.dirty && props.meta.error &&
                 <Typography variant='caption' color='error'>
@@ -37,6 +38,7 @@ export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdPro
             <ProjectsTreePicker
                 pickerId={props.pickerId}
                 toggleItemActive={handleChange(props)}
+                cascadeSelection={false}
                 options={{ showOnlyOwned: false, showOnlyWritable: true }}
                 includeCollections />
             {props.meta.dirty && props.meta.error &&
@@ -69,9 +71,10 @@ export const DirectoryTreePickerField = connect(null, projectsTreePickerMapDispa
             return <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
                 <div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
                     <ProjectsTreePicker
-                        currentUuid={this.props.input.value.uuid}
+                        currentUuids={[this.props.input.value.uuid]}
                         pickerId={this.props.pickerId}
                         toggleItemActive={this.handleDirectoryChange(this.props)}
+                        cascadeSelection={false}
                         options={{ showOnlyOwned: false, showOnlyWritable: true }}
                         includeCollections
                         includeDirectories />
index 27255bd961e99f9db306ff5e2a637c921352a2b6..dd5bb2f8ea982da4e36b9078fc8e893c092d7bfb 100644 (file)
@@ -15,7 +15,7 @@ import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, Divid
 import { GenericInputProps, GenericInput } from './generic-input';
 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
 import { connect, DispatchProp } from 'react-redux';
-import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds, getAllNodes } from 'store/tree-picker/tree-picker-actions';
+import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds, FileOperationLocation, getFileOperationLocation, fileOperationLocationToPickerId } from 'store/tree-picker/tree-picker-actions';
 import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
 import { createSelector, createStructuredSelector } from 'reselect';
 import { ChipsInput } from 'components/chips-input/chips-input';
@@ -26,8 +26,11 @@ import { RootState } from 'store/store';
 import { Chips } from 'components/chips/chips';
 import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles';
 import { CollectionResource } from 'models/collection';
-import { ResourceKind } from 'models/resource';
+import { PORTABLE_DATA_HASH_PATTERN, ResourceKind } from 'models/resource';
+import { Dispatch } from 'redux';
+import { CollectionDirectory, CollectionFileType } from 'models/collection-file';
 
+const LOCATION_REGEX = new RegExp("^(?:keep:)?(" + PORTABLE_DATA_HASH_PATTERN + ")(/.*)?$");
 export interface DirectoryArrayInputProps {
     input: DirectoryArrayCommandInputParameter;
     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
@@ -45,26 +48,35 @@ export const DirectoryArrayInput = ({ input }: DirectoryArrayInputProps) =>
 interface FormattedDirectory {
     name: string;
     portableDataHash: string;
+    subpath: string;
 }
 
-const parseDirectories = (directories: CollectionResource[] | string) =>
+const parseDirectories = (directories: FileOperationLocation[] | string) =>
     typeof directories === 'string'
         ? undefined
         : directories.map(parse);
 
-const parse = (directory: CollectionResource): Directory => ({
+const parse = (directory: FileOperationLocation): Directory => ({
     class: CWLType.DIRECTORY,
     basename: directory.name,
-    location: `keep:${directory.portableDataHash}`,
+    location: `keep:${directory.pdh}${directory.subpath}`,
 });
 
-const formatDirectories = (directories: Directory[] = []) =>
-    directories ? directories.map(format) : [];
+const formatDirectories = (directories: Directory[] = []): FormattedDirectory[] =>
+    directories ? directories.map(format).filter((dir): dir is FormattedDirectory => Boolean(dir)) : [];
 
-const format = ({ location = '', basename = '' }: Directory): FormattedDirectory => ({
-    portableDataHash: location.replace('keep:', ''),
-    name: basename,
-});
+const format = ({ location = '', basename = '' }: Directory): FormattedDirectory | undefined => {
+    const match = LOCATION_REGEX.exec(location);
+
+    if (match) {
+        return {
+            portableDataHash: match[1],
+            subpath: match[2],
+            name: basename,
+        };
+    }
+    return undefined;
+};
 
 const validationSelector = createSelector(
     isRequiredInput,
@@ -79,11 +91,10 @@ const required = (value?: Directory[]) =>
         : ERROR_MESSAGE;
 interface DirectoryArrayInputComponentState {
     open: boolean;
-    directories: CollectionResource[];
-    prevDirectories: CollectionResource[];
+    directories: FileOperationLocation[];
 }
 
-interface DirectoryArrayInputComponentProps {
+interface DirectoryArrayInputDataProps {
     treePickerState: TreePicker;
 }
 
@@ -93,21 +104,39 @@ const mapStateToProps = createStructuredSelector({
     treePickerState: treePickerSelector,
 });
 
-const DirectoryArrayInputComponent = connect(mapStateToProps)(
-    class DirectoryArrayInputComponent extends React.Component<DirectoryArrayInputComponentProps & GenericInputProps & DispatchProp & {
+interface DirectoryArrayInputActionProps {
+    initProjectsTreePicker: (pickerId: string) => void;
+    selectTreePickerNode: (pickerId: string, id: string | string[]) => void;
+    deselectTreePickerNode: (pickerId: string, id: string | string[]) => void;
+    getFileOperationLocation: (item: ProjectsTreePickerItem) => Promise<FileOperationLocation | undefined>;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): DirectoryArrayInputActionProps => ({
+    initProjectsTreePicker: (pickerId: string) => dispatch<any>(initProjectsTreePicker(pickerId)),
+    selectTreePickerNode: (pickerId: string, id: string | string[]) =>
+        dispatch<any>(treePickerActions.SELECT_TREE_PICKER_NODE({
+            pickerId, id, cascade: false
+        })),
+    deselectTreePickerNode: (pickerId: string, id: string | string[]) =>
+        dispatch<any>(treePickerActions.DESELECT_TREE_PICKER_NODE({
+            pickerId, id, cascade: false
+        })),
+    getFileOperationLocation: (item: ProjectsTreePickerItem) => dispatch<any>(getFileOperationLocation(item)),
+});
+
+const DirectoryArrayInputComponent = connect(mapStateToProps, mapDispatchToProps)(
+    class DirectoryArrayInputComponent extends React.Component<GenericInputProps & DirectoryArrayInputDataProps & DirectoryArrayInputActionProps & DispatchProp & {
         options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
     }, DirectoryArrayInputComponentState> {
         state: DirectoryArrayInputComponentState = {
             open: false,
             directories: [],
-            prevDirectories: [],
         };
 
         directoryRefreshTimeout = -1;
 
         componentDidMount() {
-            this.props.dispatch<any>(
-                initProjectsTreePicker(this.props.commandInput.id));
+            this.props.initProjectsTreePicker(this.props.commandInput.id);
         }
 
         render() {
@@ -118,7 +147,6 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
         }
 
         openDialog = () => {
-            this.setDirectoriesFromProps(this.props.input.value);
             this.setState({ open: true });
         }
 
@@ -131,82 +159,52 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
             this.props.input.onChange(this.state.directories);
         }
 
-        setDirectories = (directories: CollectionResource[]) => {
+        setDirectoriesFromResources = async (directories: (CollectionResource | CollectionDirectory)[]) => {
+            const locations = (await Promise.all(
+                directories.map(directory => (this.props.getFileOperationLocation(directory)))
+            )).filter((location): location is FileOperationLocation => (
+                location !== undefined
+            ));
 
-            const deletedDirectories = this.state.directories
-                .reduce((deletedDirectories, directory) =>
-                    directories.some(({ uuid }) => uuid === directory.uuid)
-                        ? deletedDirectories
-                        : [...deletedDirectories, directory]
-                    , []);
-
-            this.setState({ directories });
-
-            const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
-            ids.forEach(pickerId => {
-                this.props.dispatch(
-                    treePickerActions.DESELECT_TREE_PICKER_NODE({
-                        pickerId, id: deletedDirectories.map(({ uuid }) => uuid),
-                    })
-                );
-            });
+            this.setDirectories(locations);
+        }
 
+        refreshDirectories = () => {
+            clearTimeout(this.directoryRefreshTimeout);
+            this.directoryRefreshTimeout = window.setTimeout(this.setDirectoriesFromTree);
         }
 
-        setDirectoriesFromProps = (formattedDirectories: FormattedDirectory[]) => {
-            const nodes = getAllNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
-            const initialDirectories: CollectionResource[] = [];
+        setDirectoriesFromTree = () => {
+            const nodes = getSelectedNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
+            const initialDirectories: (CollectionResource | CollectionDirectory)[] = [];
             const directories = nodes
                 .reduce((directories, { value }) =>
-                    'kind' in value &&
-                        value.kind === ResourceKind.COLLECTION &&
-                        formattedDirectories.find(({ portableDataHash, name }) => value.portableDataHash === portableDataHash && value.name === name)
+                    (('kind' in value && value.kind === ResourceKind.COLLECTION) ||
+                    ('type' in value && value.type === CollectionFileType.DIRECTORY))
                         ? directories.concat(value)
                         : directories, initialDirectories);
+            this.setDirectoriesFromResources(directories);
+        }
+
+        setDirectories = (locations: FileOperationLocation[]) => {
+            const deletedDirectories = this.state.directories
+                .reduce((deletedDirectories, directory) =>
+                    locations.some(({ uuid, subpath }) => uuid === directory.uuid && subpath === directory.subpath)
+                        ? deletedDirectories
+                        : [...deletedDirectories, directory]
+                    , [] as FileOperationLocation[]);
 
-            const addedDirectories = directories
-                .reduce((addedDirectories, directory) =>
-                    this.state.directories.find(({ uuid }) =>
-                        uuid === directory.uuid)
-                        ? addedDirectories
-                        : [...addedDirectories, directory]
-                    , []);
+            this.setState({ directories: locations });
 
             const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
             ids.forEach(pickerId => {
-                this.props.dispatch(
-                    treePickerActions.SELECT_TREE_PICKER_NODE({
-                        pickerId, id: addedDirectories.map(({ uuid }) => uuid),
-                    })
+                this.props.deselectTreePickerNode(
+                    pickerId,
+                    deletedDirectories.map(fileOperationLocationToPickerId)
                 );
             });
+        };
 
-            const orderedDirectories = formattedDirectories.reduce((dirs, formattedDir) => {
-                const dir = directories.find(({ portableDataHash, name }) => portableDataHash === formattedDir.portableDataHash && name === formattedDir.name);
-                return dir
-                    ? [...dirs, dir]
-                    : dirs;
-            }, []);
-
-            this.setDirectories(orderedDirectories);
-
-        }
-
-        refreshDirectories = () => {
-            clearTimeout(this.directoryRefreshTimeout);
-            this.directoryRefreshTimeout = window.setTimeout(this.setSelectedFiles);
-        }
-
-        setSelectedFiles = () => {
-            const nodes = getSelectedNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
-            const initialDirectories: CollectionResource[] = [];
-            const directories = nodes
-                .reduce((directories, { value }) =>
-                    'kind' in value && value.kind === ResourceKind.COLLECTION
-                        ? directories.concat(value)
-                        : directories, initialDirectories);
-            this.setDirectories(directories);
-        }
         input = () =>
             <GenericInput
                 component={this.chipsInput}
@@ -265,14 +263,17 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
                     onClose={this.closeDialog}
                     fullWidth
                     maxWidth='md' >
-                    <DialogTitle>Choose collections</DialogTitle>
+                    <DialogTitle>Choose directories</DialogTitle>
                     <DialogContent className={classes.root}>
                         <div className={classes.pickerWrapper}>
                             <div className={classes.tree}>
                                 <ProjectsTreePicker
                                     pickerId={this.props.commandInput.id}
+                                    currentUuids={this.state.directories.map(dir => fileOperationLocationToPickerId(dir))}
                                     includeCollections
+                                    includeDirectories
                                     showSelection
+                                    cascadeSelection={false}
                                     options={this.props.options}
                                     toggleItemSelection={this.refreshDirectories} />
                             </div>
index bd9dc67eb810d0872bd4cdf5f5bcf2eb8ca152f3..63c990fa9f2cb513759bc1d87caa52c1b440b220 100644 (file)
@@ -149,6 +149,7 @@ const DirectoryInputComponent = connect(null, mapDispatchToProps)(
                                 pickerId={this.props.commandInput.id}
                                 includeCollections
                                 includeDirectories
+                                cascadeSelection={false}
                                 options={this.props.options}
                                 toggleItemActive={this.setDirectory} />
                         </div>
index 1e1a42998f50d11e3cc0151a241f90f443b069e7..99338738fa5e03c67b62482c4a25f28f68bd5c6e 100644 (file)
@@ -144,7 +144,9 @@ const FileArrayInputComponent = connect(mapStateToProps)(
             ids.forEach(pickerId => {
                 this.props.dispatch(
                     treePickerActions.DESELECT_TREE_PICKER_NODE({
-                        pickerId, id: deletedFiles.map(({ id }) => id),
+                        pickerId,
+                        id: deletedFiles.map(({ id }) => id),
+                        cascade: true,
                     })
                 );
             });
@@ -164,7 +166,9 @@ const FileArrayInputComponent = connect(mapStateToProps)(
             ids.forEach(pickerId => {
                 this.props.dispatch(
                     treePickerActions.SELECT_TREE_PICKER_NODE({
-                        pickerId, id: addedFiles.map(({ id }) => id),
+                        pickerId,
+                        id: addedFiles.map(({ id }) => id),
+                        cascade: true,
                     })
                 );
             });
@@ -257,6 +261,7 @@ const FileArrayInputComponent = connect(mapStateToProps)(
                                     includeDirectories
                                     includeFiles
                                     showSelection
+                                    cascadeSelection={true}
                                     options={this.props.options}
                                     toggleItemSelection={this.refreshFiles} />
                             </div>
index 5f48f83784bf1d9196baf36835bf20027f8cbfde..6970e2a5b531c9cb1af50a410075845ded643578 100644 (file)
@@ -144,6 +144,7 @@ const FileInputComponent = connect()(
                                 includeCollections
                                 includeDirectories
                                 includeFiles
+                                cascadeSelection={false}
                                 options={this.props.options}
                                 toggleItemActive={this.setFile} />
                         </div>
index d91a6b8483f2265f2685e372c09585ab1286071c..438bbe8e7e40163b55b8d363a53b82dc5f23ab01 100644 (file)
@@ -140,6 +140,7 @@ export const ProjectInputComponent = connect(mapStateToProps)(
                         <div className={classes.pickerWrapper}>
                             <ProjectsTreePicker
                                 pickerId={this.props.commandInput.id}
+                                cascadeSelection={false}
                                 options={this.props.options}
                                 toggleItemActive={this.setProject} />
                         </div>