[arvados-workbench2] updated: 2.6.0-9-g0de2dbbd

git repository hosting git at public.arvados.org
Thu Apr 27 16:20:17 UTC 2023


Summary of changes:
 .../ancestors-service/ancestors-service.ts         |  11 ++-
 src/services/services.ts                           |   3 +-
 .../collections/collection-partial-copy-actions.ts |  87 +++++++++++++++--
 .../collections/collection-partial-move-actions.ts |  89 ++++++++++++++++--
 src/store/tree-picker/tree-picker-actions.ts       |  95 +++++++++++++++++--
 src/store/tree-picker/tree-picker-middleware.ts    | 104 +++++++++++----------
 .../action-sets/collection-files-action-set.ts     |  17 +++-
 ...lection-partial-copy-to-existing-collection.tsx |   1 +
 ...ction-partial-copy-to-separate-collections.tsx} |  14 +--
 .../partial-copy-to-separate-collections-dialog.ts |  21 +++++
 .../partial-move-to-separate-collections-dialog.ts |  21 +++++
 ...lection-partial-move-to-existing-collection.tsx |   1 +
 ...ction-partial-move-to-separate-collections.tsx} |  14 ++-
 .../form-fields/collection-form-fields.tsx         |   2 +-
 .../projects-tree-picker/projects-tree-picker.tsx  |   3 +-
 .../projects-tree-picker/tree-picker-field.tsx     |  20 +---
 src/views/workbench/workbench.tsx                  |   4 +
 17 files changed, 398 insertions(+), 109 deletions(-)
 copy src/views-components/dialog-copy/{dialog-collection-partial-copy-to-existing-collection.tsx => dialog-collection-partial-copy-to-separate-collections.tsx} (59%)
 create mode 100644 src/views-components/dialog-forms/partial-copy-to-separate-collections-dialog.ts
 create mode 100644 src/views-components/dialog-forms/partial-move-to-separate-collections-dialog.ts
 copy src/views-components/dialog-move/{dialog-collection-partial-move-to-new-collection.tsx => dialog-collection-partial-move-to-separate-collections.tsx} (54%)

       via  0de2dbbdaa1c0906e105cfc685affdb3d03dc9e7 (commit)
       via  39bdc716190a21ee7c70d9450fc121a0513ff8ba (commit)
       via  54c77552a4ff0403eb627408ed38a9d8b60606b3 (commit)
       via  8822913c680fa37634b9ab103bc962b77ebde830 (commit)
       via  0a61c946e683e8a44db3212656aa4b3da82ffd8e (commit)
      from  bc0041ffcbc42f92edc6bde05c8ba3ce392873fc (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 0de2dbbdaa1c0906e105cfc685affdb3d03dc9e7
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Apr 27 10:36:19 2023 -0400

    20031: Preselect current collection in move/copy to existing collection tree picker
    
    * Add collection support to ancestor service
    * Initialize copy/move to existing collection form with current collection
    * Use initial form value and ancestor service to preload tree picker with initial selection
    * Add refresh action to tree picker search actions to populate initial expanded tree picker items after preselection
    * Allow pristine copy/move to existing form to be submitted with initial value (relies on form validation to disable submit if no initial value)
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/services/ancestors-service/ancestors-service.ts b/src/services/ancestors-service/ancestors-service.ts
index 90a0bf84..188c233e 100644
--- a/src/services/ancestors-service/ancestors-service.ts
+++ b/src/services/ancestors-service/ancestors-service.ts
@@ -7,18 +7,21 @@ import { UserService } from '../user-service/user-service';
 import { GroupResource } from 'models/group';
 import { UserResource } from 'models/user';
 import { extractUuidObjectType, ResourceObjectType } from "models/resource";
+import { CollectionService } from "services/collection-service/collection-service";
+import { CollectionResource } from "models/collection";
 
 export class AncestorService {
     constructor(
         private groupsService: GroupsService,
-        private userService: UserService
+        private userService: UserService,
+        private collectionService: CollectionService,
     ) { }
 
-    async ancestors(startUuid: string, endUuid: string): Promise<Array<UserResource | GroupResource>> {
+    async ancestors(startUuid: string, endUuid: string): Promise<Array<UserResource | GroupResource | CollectionResource>> {
         return this._ancestors(startUuid, endUuid);
     }
 
-    private async _ancestors(startUuid: string, endUuid: string, previousUuid = ''): Promise<Array<UserResource | GroupResource>> {
+    private async _ancestors(startUuid: string, endUuid: string, previousUuid = ''): Promise<Array<UserResource | GroupResource | CollectionResource>> {
 
         if (startUuid === previousUuid) {
             return [];
@@ -49,6 +52,8 @@ export class AncestorService {
                 return this.groupsService;
             case ResourceObjectType.USER:
                 return this.userService;
+            case ResourceObjectType.COLLECTION:
+                return this.collectionService;
             default:
                 return undefined;
         }
diff --git a/src/services/services.ts b/src/services/services.ts
index 4e4a682e..be6f16b6 100644
--- a/src/services/services.ts
+++ b/src/services/services.ts
@@ -75,13 +75,12 @@ export const createServices = (config: Config, actions: ApiActions, useApiClient
     const workflowService = new WorkflowService(apiClient, actions);
     const linkAccountService = new LinkAccountService(apiClient, actions);
 
-    const ancestorsService = new AncestorService(groupsService, userService);
-
     const idleTimeout = (config && config.clusterConfig && config.clusterConfig.Workbench.IdleTimeout) || '0s';
     const authService = new AuthService(apiClient, config.rootUrl, actions,
         (parse(idleTimeout, 's') || 0) > 0);
 
     const collectionService = new CollectionService(apiClient, webdavClient, authService, actions);
+    const ancestorsService = new AncestorService(groupsService, userService, collectionService);
     const favoriteService = new FavoriteService(linkService, groupsService);
     const tagService = new TagService(linkService);
     const searchService = new SearchService();
diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts
index f9ecb5ca..5da7c8f1 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -109,7 +109,7 @@ export const openCollectionPartialCopyToExistingCollectionDialog = () =>
         const currentCollection = getState().collectionPanel.item;
         if (currentCollection) {
             const initialData = {
-                destination: {uuid: '', destinationPath: ''}
+                destination: {uuid: currentCollection.uuid, destinationPath: ''}
             };
             dispatch(initialize(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION, initialData));
             dispatch<any>(resetPickerProjectTree());
diff --git a/src/store/collections/collection-partial-move-actions.ts b/src/store/collections/collection-partial-move-actions.ts
index 44e6a9bf..8b4492ef 100644
--- a/src/store/collections/collection-partial-move-actions.ts
+++ b/src/store/collections/collection-partial-move-actions.ts
@@ -105,7 +105,7 @@ export const openCollectionPartialMoveToExistingCollectionDialog = () =>
         const currentCollection = getState().collectionPanel.item;
         if (currentCollection) {
             const initialData = {
-                destination: {uuid: '', path: ''}
+                destination: {uuid: currentCollection.uuid, path: ''}
             };
             dispatch(initialize(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION, initialData));
             dispatch<any>(resetPickerProjectTree());
@@ -119,7 +119,7 @@ export const moveCollectionPartialToExistingCollection = ({ destination }: Colle
         // Get current collection
         const sourceCollection = state.collectionPanel.item;
 
-        if (sourceCollection && destination.uuid) {
+        if (sourceCollection && destination && destination.uuid) {
             try {
                 dispatch(startSubmit(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION));
                 dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION));
diff --git a/src/store/tree-picker/tree-picker-actions.ts b/src/store/tree-picker/tree-picker-actions.ts
index 505e0622..bf40394c 100644
--- a/src/store/tree-picker/tree-picker-actions.ts
+++ b/src/store/tree-picker/tree-picker-actions.ts
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from "common/unionize";
-import { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, TreePickerId, Tree } from 'models/tree';
+import { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, TreePickerId, Tree, setNode, createTree } from 'models/tree';
 import { CollectionFileType, createCollectionFilesTree, getCollectionResourceCollectionUuid } from "models/collection-file";
 import { Dispatch } from 'redux';
 import { RootState } from 'store/store';
@@ -22,6 +22,7 @@ import { LinkResource, LinkClass } from "models/link";
 import { mapTreeValues } from "models/tree";
 import { sortFilesTree } from "services/collection-service/collection-service-files-response";
 import { GroupClass, GroupResource } from "models/group";
+import { CollectionResource } from "models/collection";
 
 export const treePickerActions = unionize({
     LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
@@ -52,6 +53,7 @@ export const treePickerSearchActions = unionize({
     SET_TREE_PICKER_PROJECT_SEARCH: ofType<{ pickerId: string, projectSearchValue: string }>(),
     SET_TREE_PICKER_COLLECTION_FILTER: ofType<{ pickerId: string, collectionFilterValue: string }>(),
     SET_TREE_PICKER_LOAD_PARAMS: ofType<{ pickerId: string, params: LoadProjectParams }>(),
+    REFRESH_TREE_PICKER: ofType<{ pickerId: string }>(),
 });
 
 export type TreePickerSearchAction = UnionOf<typeof treePickerSearchActions>;
@@ -87,14 +89,18 @@ 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) =>
-    async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
+export const initProjectsTreePicker = (pickerId: string, selectedItemUuid?: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
         dispatch<any>(initUserProject(home));
         dispatch<any>(initSharedProject(shared));
         dispatch<any>(initFavoritesProject(favorites));
         dispatch<any>(initPublicFavoritesProject(publicFavorites));
         dispatch<any>(initSearchProject(search));
+
+        if (selectedItemUuid) {
+            dispatch<any>(loadInitialValue(selectedItemUuid, pickerId));
+        }
     };
 
 interface ReceiveTreePickerDataParams<T> {
@@ -242,7 +248,7 @@ export const loadCollection = (id: string, pickerId: string, includeDirectories?
         }
     };
 
-
+export const HOME_PROJECT_ID = 'Home Projects';
 export const initUserProject = (pickerId: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const uuid = getUserUuid(getState());
@@ -250,7 +256,7 @@ export const initUserProject = (pickerId: string) =>
             dispatch(receiveTreePickerData({
                 id: '',
                 pickerId,
-                data: [{ uuid, name: 'Home Projects' }],
+                data: [{ uuid, name: HOME_PROJECT_ID }],
                 extractNodeData: value => ({
                     id: value.uuid,
                     status: TreeNodeStatus.INITIAL,
@@ -282,6 +288,36 @@ export const initSharedProject = (pickerId: string) =>
         }));
     };
 
+export const loadInitialValue = (initialValue: string, pickerId: string) =>
+    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        const { home, shared } = getProjectsTreePickerIds(pickerId);
+        const homeUuid = getUserUuid(getState());
+        const ancestors = (await services.ancestorsService.ancestors(initialValue, ''))
+            .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;
+
+            ancestors[0].ownerUuid = '';
+            const tree = createInitialLocationTree(ancestors, initialValue);
+            dispatch(
+                treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
+                    id: pickerTreeRootUuid,
+                    pickerId: pickerTreeId,
+                    subtree: tree
+                }));
+            dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({ ids: [pickerTreeRootUuid], pickerId: pickerTreeId }));
+            dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: initialValue, pickerId: pickerTreeId }));
+            dispatch(treePickerSearchActions.REFRESH_TREE_PICKER({ pickerId: pickerTreeId }));
+        }
+
+    }
+
 export const FAVORITES_PROJECT_ID = 'Favorites';
 export const initFavoritesProject = (pickerId: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
@@ -511,3 +547,21 @@ export const getFileOperationLocation = (item: ProjectsTreePickerItem): FileOper
         return undefined;
     }
 };
+
+/**
+ * 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
+ */
+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: item.uuid !== tailUuid,
+            status: item.uuid !== tailUuid ? TreeNodeStatus.LOADED : TreeNodeStatus.INITIAL,
+        })(tree), createTree<GroupResource | CollectionResource>());
+};
diff --git a/src/store/tree-picker/tree-picker-middleware.ts b/src/store/tree-picker/tree-picker-middleware.ts
index 8fa3ee4a..6f748a99 100644
--- a/src/store/tree-picker/tree-picker-middleware.ts
+++ b/src/store/tree-picker/tree-picker-middleware.ts
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Dispatch } from 'redux';
+import { Dispatch, MiddlewareAPI } from 'redux';
 import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
 import { Middleware } from "redux";
@@ -37,6 +37,8 @@ export const treePickerSearchMiddleware: Middleware = store => next => action =>
             isSearchAction = true;
             searchChanged = store.getState().treePickerSearch.collectionFilterValues[pickerId] !== collectionFilterValue;
         },
+
+        REFRESH_TREE_PICKER: refreshPickers(store),
         default: () => { }
     });
 
@@ -62,57 +64,59 @@ export const treePickerSearchMiddleware: Middleware = store => next => action =>
                 }
             }),
 
-        SET_TREE_PICKER_COLLECTION_FILTER: ({ pickerId }) =>
-            store.dispatch<any>((dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-                const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
-                if (picker) {
-                    const loadParams = getState().treePickerSearch.loadProjectParams[pickerId];
-                    getNodeDescendantsIds('')(picker)
-                        .map(id => {
-                            const node = getNode(id)(picker);
-                            if (node && node.status !== TreeNodeStatus.INITIAL) {
-                                if (node.id.substring(6, 11) === 'tpzed' || node.id.substring(6, 11) === 'j7d0g') {
-                                    dispatch<any>(loadProject({
-                                        ...loadParams,
-                                        id: node.id,
-                                        pickerId: pickerId,
-                                    }));
-                                }
-                                if (node.id === SHARED_PROJECT_ID) {
-                                    dispatch<any>(loadProject({
-                                        ...loadParams,
-                                        id: node.id,
-                                        pickerId: pickerId,
-                                        loadShared: true
-                                    }));
-                                }
-                                if (node.id === SEARCH_PROJECT_ID) {
-                                    dispatch<any>(loadProject({
-                                        ...loadParams,
-                                        id: node.id,
-                                        pickerId: pickerId,
-                                        searchProjects: true
-                                    }));
-                                }
-                                if (node.id === FAVORITES_PROJECT_ID) {
-                                    dispatch<any>(loadFavoritesProject({
-                                        ...loadParams,
-                                        pickerId: pickerId,
-                                    }));
-                                }
-                                if (node.id === PUBLIC_FAVORITES_PROJECT_ID) {
-                                    dispatch<any>(loadPublicFavoritesProject({
-                                        ...loadParams,
-                                        pickerId: pickerId,
-                                    }));
-                                }
-                            }
-                            return id;
-                        });
-                }
-            }),
+        SET_TREE_PICKER_COLLECTION_FILTER: refreshPickers(store),
         default: () => { }
     });
 
     return r;
 }
+
+const refreshPickers = (store: MiddlewareAPI) => ({ pickerId }) =>
+    store.dispatch<any>((dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
+        if (picker) {
+            const loadParams = getState().treePickerSearch.loadProjectParams[pickerId];
+            getNodeDescendantsIds('')(picker)
+                .map(id => {
+                    const node = getNode(id)(picker);
+                    if (node && node.status !== TreeNodeStatus.INITIAL) {
+                        if (node.id.substring(6, 11) === 'tpzed' || node.id.substring(6, 11) === 'j7d0g') {
+                            dispatch<any>(loadProject({
+                                ...loadParams,
+                                id: node.id,
+                                pickerId: pickerId,
+                            }));
+                        }
+                        if (node.id === SHARED_PROJECT_ID) {
+                            dispatch<any>(loadProject({
+                                ...loadParams,
+                                id: node.id,
+                                pickerId: pickerId,
+                                loadShared: true
+                            }));
+                        }
+                        if (node.id === SEARCH_PROJECT_ID) {
+                            dispatch<any>(loadProject({
+                                ...loadParams,
+                                id: node.id,
+                                pickerId: pickerId,
+                                searchProjects: true
+                            }));
+                        }
+                        if (node.id === FAVORITES_PROJECT_ID) {
+                            dispatch<any>(loadFavoritesProject({
+                                ...loadParams,
+                                pickerId: pickerId,
+                            }));
+                        }
+                        if (node.id === PUBLIC_FAVORITES_PROJECT_ID) {
+                            dispatch<any>(loadPublicFavoritesProject({
+                                ...loadParams,
+                                pickerId: pickerId,
+                            }));
+                        }
+                    }
+                    return id;
+                });
+        }
+    })
diff --git a/src/views-components/dialog-copy/dialog-collection-partial-copy-to-existing-collection.tsx b/src/views-components/dialog-copy/dialog-collection-partial-copy-to-existing-collection.tsx
index f6d4db21..eb95d1f2 100644
--- a/src/views-components/dialog-copy/dialog-collection-partial-copy-to-existing-collection.tsx
+++ b/src/views-components/dialog-copy/dialog-collection-partial-copy-to-existing-collection.tsx
@@ -18,6 +18,7 @@ export const DialogCollectionPartialCopyToExistingCollection = (props: DialogCol
         dialogTitle='Copy to existing collection'
         formFields={CollectionPartialCopyFields(props.pickerId)}
         submitLabel='Copy files'
+        enableWhenPristine
         {...props}
     />;
 
diff --git a/src/views-components/dialog-move/dialog-collection-partial-move-to-existing-collection.tsx b/src/views-components/dialog-move/dialog-collection-partial-move-to-existing-collection.tsx
index f95bd24f..5cd4996d 100644
--- a/src/views-components/dialog-move/dialog-collection-partial-move-to-existing-collection.tsx
+++ b/src/views-components/dialog-move/dialog-collection-partial-move-to-existing-collection.tsx
@@ -18,6 +18,7 @@ export const DialogCollectionPartialMoveToExistingCollection = (props: DialogCol
         dialogTitle='Move to existing collection'
         formFields={CollectionPartialMoveFields(props.pickerId)}
         submitLabel='Move files'
+        enableWhenPristine
         {...props}
     />;
 
diff --git a/src/views-components/projects-tree-picker/projects-tree-picker.tsx b/src/views-components/projects-tree-picker/projects-tree-picker.tsx
index 1f036829..773230d3 100644
--- a/src/views-components/projects-tree-picker/projects-tree-picker.tsx
+++ b/src/views-components/projects-tree-picker/projects-tree-picker.tsx
@@ -23,6 +23,7 @@ import { withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
 
 export interface ToplevelPickerProps {
+    currentUuid?: string;
     pickerId: string;
     includeCollections?: boolean;
     includeDirectories?: boolean;
@@ -106,7 +107,7 @@ 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.dispatch<any>(initProjectsTreePicker(this.props.pickerId, this.props.currentUuid));
 
                 this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_PROJECT_SEARCH({ pickerId: search, projectSearchValue: "" }));
                 this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: search, collectionFilterValue: "" }));
diff --git a/src/views-components/projects-tree-picker/tree-picker-field.tsx b/src/views-components/projects-tree-picker/tree-picker-field.tsx
index d1ff0a0d..17417bf5 100644
--- a/src/views-components/projects-tree-picker/tree-picker-field.tsx
+++ b/src/views-components/projects-tree-picker/tree-picker-field.tsx
@@ -53,6 +53,7 @@ export const DirectoryTreePickerField = (props: WrappedFieldProps & PickerIdProp
     <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
         <div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
             <ProjectsTreePicker
+                currentUuid={props.input.value.uuid}
                 pickerId={props.pickerId}
                 toggleItemActive={handleDirectoryChange(props)}
                 options={{ showOnlyOwned: false, showOnlyWritable: true }}

commit 39bdc716190a21ee7c70d9450fc121a0513ff8ba
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Apr 25 10:52:10 2023 -0400

    20031: Add type for collection file operation location for form field
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts
index 08b96655..f9ecb5ca 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -12,6 +12,7 @@ import { filterCollectionFilesBySelection } from '../collection-panel/collection
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { getCommonResourceServiceError, CommonResourceServiceError } from 'services/common-service/common-resource-service';
 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
+import { FileOperationLocation } from "store/tree-picker/tree-picker-actions";
 import { updateResources } from 'store/resources/resources-actions';
 import { navigateTo } from 'store/navigation/navigation-action';
 
@@ -26,7 +27,7 @@ export interface CollectionPartialCopyToNewCollectionFormData {
 }
 
 export interface CollectionPartialCopyToExistingCollectionFormData {
-    destination: {uuid: string, path?: string};
+    destination: FileOperationLocation;
 }
 
 export interface CollectionPartialCopyToSeparateCollectionsFormData {
@@ -122,7 +123,7 @@ export const copyCollectionPartialToExistingCollection = ({ destination }: Colle
         // Get current collection
         const sourceCollection = state.collectionPanel.item;
 
-        if (sourceCollection && destination.uuid) {
+        if (sourceCollection && destination && destination.uuid) {
             try {
                 dispatch(startSubmit(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
                 dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
diff --git a/src/store/collections/collection-partial-move-actions.ts b/src/store/collections/collection-partial-move-actions.ts
index dbf0a96a..44e6a9bf 100644
--- a/src/store/collections/collection-partial-move-actions.ts
+++ b/src/store/collections/collection-partial-move-actions.ts
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import { FormErrors, initialize, startSubmit, stopSubmit } from "redux-form";
+import { initialize, startSubmit } from "redux-form";
 import { CommonResourceServiceError, getCommonResourceServiceError } from "services/common-service/common-resource-service";
 import { ServiceRepository } from "services/services";
 import { filterCollectionFilesBySelection } from "store/collection-panel/collection-panel-files/collection-panel-files-state";
@@ -14,6 +14,7 @@ import { resetPickerProjectTree } from "store/project-tree-picker/project-tree-p
 import { updateResources } from "store/resources/resources-actions";
 import { SnackbarKind, snackbarActions } from "store/snackbar/snackbar-actions";
 import { RootState } from "store/store";
+import { FileOperationLocation } from "store/tree-picker/tree-picker-actions";
 
 export const COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION = 'COLLECTION_PARTIAL_MOVE_TO_NEW_DIALOG';
 export const COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION = 'COLLECTION_PARTIAL_MOVE_TO_SELECTED_DIALOG';
@@ -26,7 +27,7 @@ export interface CollectionPartialMoveToNewCollectionFormData {
 }
 
 export interface CollectionPartialMoveToExistingCollectionFormData {
-    destination: {uuid: string, path?: string};
+    destination: FileOperationLocation;
 }
 
 export interface CollectionPartialMoveToSeparateCollectionsFormData {
diff --git a/src/store/tree-picker/tree-picker-actions.ts b/src/store/tree-picker/tree-picker-actions.ts
index b8003aa1..505e0622 100644
--- a/src/store/tree-picker/tree-picker-actions.ts
+++ b/src/store/tree-picker/tree-picker-actions.ts
@@ -4,7 +4,7 @@
 
 import { unionize, ofType, UnionOf } from "common/unionize";
 import { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, TreePickerId, Tree } from 'models/tree';
-import { CollectionFileType, createCollectionFilesTree } from "models/collection-file";
+import { CollectionFileType, createCollectionFilesTree, getCollectionResourceCollectionUuid } from "models/collection-file";
 import { Dispatch } from 'redux';
 import { RootState } from 'store/store';
 import { getUserUuid } from "common/getuser";
@@ -482,3 +482,32 @@ const buildParams = (ownerUuid: string) => {
             .getOrder()
     };
 };
+
+/**
+ * Given a tree picker item, return collection uuid and path
+ *   if the item represents a valid target/destination location
+ */
+export type FileOperationLocation = {
+    uuid: string;
+    path: string;
+}
+export const getFileOperationLocation = (item: ProjectsTreePickerItem): FileOperationLocation | undefined => {
+    if ('kind' in item && item.kind === ResourceKind.COLLECTION) {
+        return {
+            uuid: item.uuid,
+            path: '/'
+        };
+    } else if ('type' in item && item.type === CollectionFileType.DIRECTORY) {
+        const uuid = getCollectionResourceCollectionUuid(item.id);
+        if (uuid) {
+            return {
+                uuid,
+                path: [item.path, item.name].join('/')
+            };
+        } else {
+            return undefined;
+        }
+    } else {
+        return undefined;
+    }
+};
diff --git a/src/views-components/form-fields/collection-form-fields.tsx b/src/views-components/form-fields/collection-form-fields.tsx
index 0faa59b5..23a44965 100644
--- a/src/views-components/form-fields/collection-form-fields.tsx
+++ b/src/views-components/form-fields/collection-form-fields.tsx
@@ -59,7 +59,7 @@ export const CollectionPickerField = (props: PickerIdProp) =>
         component={CollectionTreePickerField}
         validate={COLLECTION_PROJECT_VALIDATION} />;
 
-const validateDirectory = (val) => (val ? undefined : ERROR_MESSAGE);
+const validateDirectory = (val) => (val && val.uuid ? undefined : ERROR_MESSAGE);
 
 export const DirectoryPickerField = (props: PickerIdProp) =>
     <Field
diff --git a/src/views-components/projects-tree-picker/tree-picker-field.tsx b/src/views-components/projects-tree-picker/tree-picker-field.tsx
index 1414d18f..d1ff0a0d 100644
--- a/src/views-components/projects-tree-picker/tree-picker-field.tsx
+++ b/src/views-components/projects-tree-picker/tree-picker-field.tsx
@@ -9,8 +9,7 @@ import { WrappedFieldProps } from 'redux-form';
 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
 import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
 import { PickerIdProp } from 'store/tree-picker/picker-id';
-import { CollectionFileType, getCollectionResourceCollectionUuid } from "models/collection-file";
-import { ResourceKind } from "models/resource";
+import { getFileOperationLocation } from "store/tree-picker/tree-picker-actions";
 
 export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
     <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
@@ -46,20 +45,8 @@ export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdPro
     </div>;
 
 const handleDirectoryChange = (props: WrappedFieldProps) =>
-    (_: any, data: TreeItem<ProjectsTreePickerItem>) => {
-        if ('kind' in data.data && data.data.kind === ResourceKind.COLLECTION) {
-            props.input.onChange({
-                uuid: data.data.uuid,
-                path: '/'
-            });
-        } else if ('type' in data.data && data.data.type === CollectionFileType.DIRECTORY) {
-            props.input.onChange({
-                uuid: getCollectionResourceCollectionUuid(data.data.id),
-                path: [data.data.path, data.data.name].join('/')
-            });
-        } else {
-            props.input.onChange('');
-        }
+    (_: any, { data }: TreeItem<ProjectsTreePickerItem>) => {
+        props.input.onChange(getFileOperationLocation(data) || '');
     }
 
 export const DirectoryTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>

commit 54c77552a4ff0403eb627408ed38a9d8b60606b3
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Apr 25 10:44:16 2023 -0400

    20031: Remove redundant collection copy/move form initializations
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts
index 75ff4592..08b96655 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -12,7 +12,6 @@ import { filterCollectionFilesBySelection } from '../collection-panel/collection
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { getCommonResourceServiceError, CommonResourceServiceError } from 'services/common-service/common-resource-service';
 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
-import { initProjectsTreePicker } from "store/tree-picker/tree-picker-actions";
 import { updateResources } from 'store/resources/resources-actions';
 import { navigateTo } from 'store/navigation/navigation-action';
 
@@ -46,7 +45,6 @@ export const openCollectionPartialCopyToNewCollectionDialog = () =>
             };
             dispatch(initialize(COLLECTION_PARTIAL_COPY_FORM_NAME, initialData));
             dispatch<any>(resetPickerProjectTree());
-            dispatch<any>(initProjectsTreePicker(COLLECTION_PARTIAL_COPY_FORM_NAME));
             dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME, data: {} }));
         }
     };
@@ -114,7 +112,6 @@ export const openCollectionPartialCopyToExistingCollectionDialog = () =>
             };
             dispatch(initialize(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION, initialData));
             dispatch<any>(resetPickerProjectTree());
-            dispatch<any>(initProjectsTreePicker(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
             dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION, data: {} }));
         }
     };
@@ -165,7 +162,6 @@ export const openCollectionPartialCopyToSeparateCollectionsDialog = () =>
             };
             dispatch(initialize(COLLECTION_PARTIAL_COPY_TO_SEPARATE_COLLECTIONS, initialData));
             dispatch<any>(resetPickerProjectTree());
-            dispatch<any>(initProjectsTreePicker(COLLECTION_PARTIAL_COPY_TO_SEPARATE_COLLECTIONS));
             dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_PARTIAL_COPY_TO_SEPARATE_COLLECTIONS, data: {} }));
         }
     };
diff --git a/src/store/collections/collection-partial-move-actions.ts b/src/store/collections/collection-partial-move-actions.ts
index 59f6100c..dbf0a96a 100644
--- a/src/store/collections/collection-partial-move-actions.ts
+++ b/src/store/collections/collection-partial-move-actions.ts
@@ -14,7 +14,6 @@ import { resetPickerProjectTree } from "store/project-tree-picker/project-tree-p
 import { updateResources } from "store/resources/resources-actions";
 import { SnackbarKind, snackbarActions } from "store/snackbar/snackbar-actions";
 import { RootState } from "store/store";
-import { initProjectsTreePicker } from "store/tree-picker/tree-picker-actions";
 
 export const COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION = 'COLLECTION_PARTIAL_MOVE_TO_NEW_DIALOG';
 export const COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION = 'COLLECTION_PARTIAL_MOVE_TO_SELECTED_DIALOG';
@@ -46,7 +45,6 @@ export const openCollectionPartialMoveToNewCollectionDialog = () =>
             };
             dispatch(initialize(COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION, initialData));
             dispatch<any>(resetPickerProjectTree());
-            dispatch<any>(initProjectsTreePicker(COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION));
             dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION, data: {} }));
         }
     };
@@ -110,7 +108,6 @@ export const openCollectionPartialMoveToExistingCollectionDialog = () =>
             };
             dispatch(initialize(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION, initialData));
             dispatch<any>(resetPickerProjectTree());
-            dispatch<any>(initProjectsTreePicker(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION));
             dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION, data: {} }));
         }
     };
@@ -161,7 +158,6 @@ export const openCollectionPartialMoveToSeparateCollectionsDialog = () =>
             };
             dispatch(initialize(COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS, initialData));
             dispatch<any>(resetPickerProjectTree());
-            dispatch<any>(initProjectsTreePicker(COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS));
             dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS, data: {} }));
         }
     };

commit 8822913c680fa37634b9ab103bc962b77ebde830
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Apr 18 20:44:19 2023 -0400

    20031: Add split files into separate collection move/copy actions
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts
index efdfd961..75ff4592 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -18,6 +18,7 @@ import { navigateTo } from 'store/navigation/navigation-action';
 
 export const COLLECTION_PARTIAL_COPY_FORM_NAME = 'COLLECTION_PARTIAL_COPY_DIALOG';
 export const COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION = 'COLLECTION_PARTIAL_COPY_TO_SELECTED_DIALOG';
+export const COLLECTION_PARTIAL_COPY_TO_SEPARATE_COLLECTIONS = 'COLLECTION_PARTIAL_COPY_TO_SEPARATE_DIALOG';
 
 export interface CollectionPartialCopyToNewCollectionFormData {
     name: string;
@@ -29,6 +30,11 @@ export interface CollectionPartialCopyToExistingCollectionFormData {
     destination: {uuid: string, path?: string};
 }
 
+export interface CollectionPartialCopyToSeparateCollectionsFormData {
+    name: string;
+    projectUuid: string;
+}
+
 export const openCollectionPartialCopyToNewCollectionDialog = () =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const currentCollection = getState().collectionPanel.item;
@@ -148,3 +154,73 @@ export const copyCollectionPartialToExistingCollection = ({ destination }: Colle
             }
         }
     };
+
+export const openCollectionPartialCopyToSeparateCollectionsDialog = () =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const currentCollection = getState().collectionPanel.item;
+        if (currentCollection) {
+            const initialData = {
+                name: currentCollection.name,
+                projectUuid: undefined
+            };
+            dispatch(initialize(COLLECTION_PARTIAL_COPY_TO_SEPARATE_COLLECTIONS, initialData));
+            dispatch<any>(resetPickerProjectTree());
+            dispatch<any>(initProjectsTreePicker(COLLECTION_PARTIAL_COPY_TO_SEPARATE_COLLECTIONS));
+            dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_PARTIAL_COPY_TO_SEPARATE_COLLECTIONS, data: {} }));
+        }
+    };
+
+export const copyCollectionPartialToSeparateCollections = ({ name, projectUuid }: CollectionPartialCopyToSeparateCollectionsFormData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const state = getState();
+        // Get current collection
+        const sourceCollection = state.collectionPanel.item;
+
+        if (sourceCollection) {
+            try {
+                dispatch(startSubmit(COLLECTION_PARTIAL_COPY_FORM_NAME));
+                dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
+
+                // Get selected files
+                const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, true)
+                    .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
+
+                // Copy files
+                const collections = await Promise.all(paths.map((path) =>
+                    services.collectionService.copyFiles(
+                        sourceCollection.portableDataHash,
+                        [path],
+                        {
+                            name: `File split from collection ${name}${path}`,
+                            ownerUuid: projectUuid,
+                            uuid: undefined,
+                        },
+                        '/',
+                        false
+                    )
+                ));
+                dispatch(updateResources(collections));
+
+                dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: 'New collections created.',
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
+            } catch (e) {
+                const error = getCommonResourceServiceError(e);
+                console.log(e, error);
+                if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
+                    dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection from one or more files already exists', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+                } else if (error === CommonResourceServiceError.UNKNOWN) {
+                    dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
+                    dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not create a copy of collection', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+                } else {
+                    dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
+                    dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been copied but may contain incorrect files.', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+                }
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
+            }
+        }
+    };
diff --git a/src/store/collections/collection-partial-move-actions.ts b/src/store/collections/collection-partial-move-actions.ts
index c5760ed8..59f6100c 100644
--- a/src/store/collections/collection-partial-move-actions.ts
+++ b/src/store/collections/collection-partial-move-actions.ts
@@ -18,6 +18,7 @@ import { initProjectsTreePicker } from "store/tree-picker/tree-picker-actions";
 
 export const COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION = 'COLLECTION_PARTIAL_MOVE_TO_NEW_DIALOG';
 export const COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION = 'COLLECTION_PARTIAL_MOVE_TO_SELECTED_DIALOG';
+export const COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS = 'COLLECTION_PARTIAL_MOVE_TO_SEPARATE_DIALOG';
 
 export interface CollectionPartialMoveToNewCollectionFormData {
     name: string;
@@ -29,6 +30,11 @@ export interface CollectionPartialMoveToExistingCollectionFormData {
     destination: {uuid: string, path?: string};
 }
 
+export interface CollectionPartialMoveToSeparateCollectionsFormData {
+    name: string;
+    projectUuid: string;
+}
+
 export const openCollectionPartialMoveToNewCollectionDialog = () =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const currentCollection = getState().collectionPanel.item;
@@ -144,3 +150,73 @@ export const moveCollectionPartialToExistingCollection = ({ destination }: Colle
             }
         }
     };
+
+export const openCollectionPartialMoveToSeparateCollectionsDialog = () =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const currentCollection = getState().collectionPanel.item;
+        if (currentCollection) {
+            const initialData = {
+                name: currentCollection.name,
+                projectUuid: undefined
+            };
+            dispatch(initialize(COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS, initialData));
+            dispatch<any>(resetPickerProjectTree());
+            dispatch<any>(initProjectsTreePicker(COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS));
+            dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS, data: {} }));
+        }
+    };
+
+export const moveCollectionPartialToSeparateCollections = ({ name, projectUuid }: CollectionPartialMoveToSeparateCollectionsFormData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const state = getState();
+        // Get current collection
+        const sourceCollection = state.collectionPanel.item;
+
+        if (sourceCollection) {
+            try {
+                dispatch(startSubmit(COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS));
+                dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS));
+
+                // Get selected files
+                const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, true)
+                    .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
+
+                // Move files
+                const collections = await Promise.all(paths.map((path) =>
+                    services.collectionService.moveFiles(
+                        sourceCollection.uuid,
+                        sourceCollection.portableDataHash,
+                        [path],
+                        {
+                            name: `File split from collection ${name}${path}`,
+                            ownerUuid: projectUuid,
+                            uuid: undefined,
+                        },
+                        '/',
+                        false
+                    )
+                ));
+                dispatch(updateResources(collections));
+
+                dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS }));
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: 'New collections created.',
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS));
+            } catch (e) {
+                const error = getCommonResourceServiceError(e);
+                if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
+                    dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection from one or more files already exists', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+                } else if (error === CommonResourceServiceError.UNKNOWN) {
+                    dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS }));
+                    dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not create a copy of collection', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+                } else {
+                    dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS }));
+                    dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been copied but may contain incorrect files.', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+                }
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS));
+            }
+        }
+    };
diff --git a/src/views-components/context-menu/action-sets/collection-files-action-set.ts b/src/views-components/context-menu/action-sets/collection-files-action-set.ts
index 3e6e1a20..c1c541d3 100644
--- a/src/views-components/context-menu/action-sets/collection-files-action-set.ts
+++ b/src/views-components/context-menu/action-sets/collection-files-action-set.ts
@@ -6,9 +6,10 @@ import { ContextMenuActionSet } from "views-components/context-menu/context-menu
 import { collectionPanelFilesAction, openMultipleFilesRemoveDialog } from "store/collection-panel/collection-panel-files/collection-panel-files-actions";
 import {
     openCollectionPartialCopyToNewCollectionDialog,
-    openCollectionPartialCopyToExistingCollectionDialog
+    openCollectionPartialCopyToExistingCollectionDialog,
+    openCollectionPartialCopyToSeparateCollectionsDialog
 } from 'store/collections/collection-partial-copy-actions';
-import { openCollectionPartialMoveToExistingCollectionDialog, openCollectionPartialMoveToNewCollectionDialog } from "store/collections/collection-partial-move-actions";
+import { openCollectionPartialMoveToExistingCollectionDialog, openCollectionPartialMoveToNewCollectionDialog, openCollectionPartialMoveToSeparateCollectionsDialog } from "store/collections/collection-partial-move-actions";
 
 // These action sets are used on the multi-select actions button.
 export const readOnlyCollectionFilesActionSet: ContextMenuActionSet = [[
@@ -35,6 +36,12 @@ export const readOnlyCollectionFilesActionSet: ContextMenuActionSet = [[
         execute: dispatch => {
             dispatch<any>(openCollectionPartialCopyToExistingCollectionDialog());
         }
+    },
+    {
+        name: "Copy selected into separate collections",
+        execute: dispatch => {
+            dispatch<any>(openCollectionPartialCopyToSeparateCollectionsDialog());
+        }
     }
 ]];
 
@@ -56,5 +63,11 @@ export const collectionFilesActionSet: ContextMenuActionSet = readOnlyCollection
         execute: dispatch => {
             dispatch<any>(openCollectionPartialMoveToExistingCollectionDialog());
         }
+    },
+    {
+        name: "Move selected into separate collections",
+        execute: dispatch => {
+            dispatch<any>(openCollectionPartialMoveToSeparateCollectionsDialog());
+        }
     }
 ]]);
diff --git a/src/views-components/dialog-copy/dialog-collection-partial-copy-to-separate-collections.tsx b/src/views-components/dialog-copy/dialog-collection-partial-copy-to-separate-collections.tsx
new file mode 100644
index 00000000..32f706a2
--- /dev/null
+++ b/src/views-components/dialog-copy/dialog-collection-partial-copy-to-separate-collections.tsx
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { memoize } from "lodash/fp";
+import { FormDialog } from 'components/form-dialog/form-dialog';
+import { CollectionProjectPickerField } from 'views-components/form-fields/collection-form-fields';
+import { WithDialogProps } from 'store/dialog/with-dialog';
+import { InjectedFormProps } from 'redux-form';
+import { CollectionPartialCopyToSeparateCollectionsFormData } from 'store/collections/collection-partial-copy-actions';
+import { PickerIdProp } from "store/tree-picker/picker-id";
+
+type DialogCollectionPartialCopyProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialCopyToSeparateCollectionsFormData>;
+
+export const DialogCollectionPartialCopyToSeparateCollection = (props: DialogCollectionPartialCopyProps & PickerIdProp) =>
+    <FormDialog
+        dialogTitle='Copy to separate collections'
+        formFields={CollectionPartialCopyFields(props.pickerId)}
+        submitLabel='Create collections'
+        {...props}
+    />;
+
+const CollectionPartialCopyFields = memoize(
+    (pickerId: string) =>
+        () =>
+            <>
+                <CollectionProjectPickerField {...{ pickerId }} />
+            </>);
diff --git a/src/views-components/dialog-forms/partial-copy-to-separate-collections-dialog.ts b/src/views-components/dialog-forms/partial-copy-to-separate-collections-dialog.ts
new file mode 100644
index 00000000..e2687cbf
--- /dev/null
+++ b/src/views-components/dialog-forms/partial-copy-to-separate-collections-dialog.ts
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { reduxForm } from 'redux-form';
+import { withDialog, } from 'store/dialog/with-dialog';
+import { COLLECTION_PARTIAL_COPY_TO_SEPARATE_COLLECTIONS, CollectionPartialCopyToSeparateCollectionsFormData, copyCollectionPartialToSeparateCollections } from 'store/collections/collection-partial-copy-actions';
+import { DialogCollectionPartialCopyToSeparateCollection } from "views-components/dialog-copy/dialog-collection-partial-copy-to-separate-collections";
+import { pickerId } from "store/tree-picker/picker-id";
+
+export const PartialCopyToSeparateCollectionsDialog = compose(
+    withDialog(COLLECTION_PARTIAL_COPY_TO_SEPARATE_COLLECTIONS),
+    reduxForm<CollectionPartialCopyToSeparateCollectionsFormData>({
+        form: COLLECTION_PARTIAL_COPY_TO_SEPARATE_COLLECTIONS,
+        onSubmit: (data, dispatch) => {
+            dispatch(copyCollectionPartialToSeparateCollections(data));
+        }
+    }),
+    pickerId(COLLECTION_PARTIAL_COPY_TO_SEPARATE_COLLECTIONS),
+)(DialogCollectionPartialCopyToSeparateCollection);
diff --git a/src/views-components/dialog-forms/partial-move-to-separate-collections-dialog.ts b/src/views-components/dialog-forms/partial-move-to-separate-collections-dialog.ts
new file mode 100644
index 00000000..8346709b
--- /dev/null
+++ b/src/views-components/dialog-forms/partial-move-to-separate-collections-dialog.ts
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { reduxForm } from 'redux-form';
+import { withDialog, } from 'store/dialog/with-dialog';
+import { COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS, CollectionPartialMoveToSeparateCollectionsFormData, moveCollectionPartialToSeparateCollections } from "store/collections/collection-partial-move-actions";
+import { DialogCollectionPartialMoveToSeparateCollections } from "views-components/dialog-move/dialog-collection-partial-move-to-separate-collections";
+import { pickerId } from "store/tree-picker/picker-id";
+
+export const PartialMoveToSeparateCollectionsDialog = compose(
+    withDialog(COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS),
+    reduxForm<CollectionPartialMoveToSeparateCollectionsFormData>({
+        form: COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS,
+        onSubmit: (data, dispatch) => {
+            dispatch(moveCollectionPartialToSeparateCollections(data));
+        }
+    }),
+    pickerId(COLLECTION_PARTIAL_MOVE_TO_SEPARATE_COLLECTIONS),
+)(DialogCollectionPartialMoveToSeparateCollections);
diff --git a/src/views-components/dialog-move/dialog-collection-partial-move-to-separate-collections.tsx b/src/views-components/dialog-move/dialog-collection-partial-move-to-separate-collections.tsx
new file mode 100644
index 00000000..1b716628
--- /dev/null
+++ b/src/views-components/dialog-move/dialog-collection-partial-move-to-separate-collections.tsx
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { memoize } from "lodash/fp";
+import { FormDialog } from 'components/form-dialog/form-dialog';
+import { CollectionProjectPickerField } from 'views-components/form-fields/collection-form-fields';
+import { WithDialogProps } from 'store/dialog/with-dialog';
+import { InjectedFormProps } from 'redux-form';
+import { CollectionPartialMoveToSeparateCollectionsFormData } from "store/collections/collection-partial-move-actions";
+import { PickerIdProp } from "store/tree-picker/picker-id";
+
+type DialogCollectionPartialMoveProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialMoveToSeparateCollectionsFormData>;
+
+export const DialogCollectionPartialMoveToSeparateCollections = (props: DialogCollectionPartialMoveProps & PickerIdProp) =>
+    <FormDialog
+        dialogTitle='Move to separate collections'
+        formFields={CollectionPartialMoveFields(props.pickerId)}
+        submitLabel='Create collections'
+        {...props}
+    />;
+
+const CollectionPartialMoveFields = memoize(
+    (pickerId: string) =>
+        () =>
+            <>
+                <CollectionProjectPickerField {...{ pickerId }} />
+            </>);
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 5b1fff39..ce930746 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -34,8 +34,10 @@ import { MoveCollectionDialog } from 'views-components/dialog-forms/move-collect
 import { FilesUploadCollectionDialog } from 'views-components/dialog-forms/files-upload-collection-dialog';
 import { PartialCopyToNewCollectionDialog } from 'views-components/dialog-forms/partial-copy-to-new-collection-dialog';
 import { PartialCopyToExistingCollectionDialog } from 'views-components/dialog-forms/partial-copy-to-existing-collection-dialog';
+import { PartialCopyToSeparateCollectionsDialog } from 'views-components/dialog-forms/partial-copy-to-separate-collections-dialog';
 import { PartialMoveToNewCollectionDialog } from 'views-components/dialog-forms/partial-move-to-new-collection-dialog';
 import { PartialMoveToExistingCollectionDialog } from 'views-components/dialog-forms/partial-move-to-existing-collection-dialog';
+import { PartialMoveToSeparateCollectionsDialog } from 'views-components/dialog-forms/partial-move-to-separate-collections-dialog';
 import { RemoveProcessDialog } from 'views-components/process-remove-dialog/process-remove-dialog';
 import { MainContentBar } from 'views-components/main-content-bar/main-content-bar';
 import { Grid } from '@material-ui/core';
@@ -268,8 +270,10 @@ export const WorkbenchPanel =
             <PublicKeyDialog />
             <PartialCopyToNewCollectionDialog />
             <PartialCopyToExistingCollectionDialog />
+            <PartialCopyToSeparateCollectionsDialog />
             <PartialMoveToNewCollectionDialog />
             <PartialMoveToExistingCollectionDialog />
+            <PartialMoveToSeparateCollectionsDialog />
             <ProcessInputDialog />
             <RestoreCollectionVersionDialog />
             <RemoveApiClientAuthorizationDialog />

commit 0a61c946e683e8a44db3212656aa4b3da82ffd8e
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Apr 18 20:12:11 2023 -0400

    20031: Navigate to new collections when copying/moving to new collection
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts
index 2a3fd5de..efdfd961 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -14,6 +14,7 @@ import { getCommonResourceServiceError, CommonResourceServiceError } from 'servi
 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
 import { initProjectsTreePicker } from "store/tree-picker/tree-picker-actions";
 import { updateResources } from 'store/resources/resources-actions';
+import { navigateTo } from 'store/navigation/navigation-action';
 
 export const COLLECTION_PARTIAL_COPY_FORM_NAME = 'COLLECTION_PARTIAL_COPY_DIALOG';
 export const COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION = 'COLLECTION_PARTIAL_COPY_TO_SELECTED_DIALOG';
@@ -73,6 +74,7 @@ export const copyCollectionPartialToNewCollection = ({ name, description, projec
                     false
                 );
                 dispatch(updateResources([updatedCollection]));
+                dispatch<any>(navigateTo(updatedCollection.uuid))
 
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
                 dispatch(snackbarActions.OPEN_SNACKBAR({
diff --git a/src/store/collections/collection-partial-move-actions.ts b/src/store/collections/collection-partial-move-actions.ts
index 6d3c45f3..c5760ed8 100644
--- a/src/store/collections/collection-partial-move-actions.ts
+++ b/src/store/collections/collection-partial-move-actions.ts
@@ -8,6 +8,7 @@ import { CommonResourceServiceError, getCommonResourceServiceError } from "servi
 import { ServiceRepository } from "services/services";
 import { filterCollectionFilesBySelection } from "store/collection-panel/collection-panel-files/collection-panel-files-state";
 import { dialogActions } from "store/dialog/dialog-actions";
+import { navigateTo } from "store/navigation/navigation-action";
 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
 import { resetPickerProjectTree } from "store/project-tree-picker/project-tree-picker-actions";
 import { updateResources } from "store/resources/resources-actions";
@@ -74,6 +75,7 @@ export const moveCollectionPartialToNewCollection = ({ name, description, projec
                     false
                 );
                 dispatch(updateResources([updatedCollection]));
+                dispatch<any>(navigateTo(updatedCollection.uuid))
 
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION }));
                 dispatch(snackbarActions.OPEN_SNACKBAR({

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list