[arvados-workbench2] created: 2.6.0-4-gbc0041ff

git repository hosting git at public.arvados.org
Mon Apr 17 19:46:05 UTC 2023


        at  bc0041ffcbc42f92edc6bde05c8ba3ce392873fc (commit)


commit bc0041ffcbc42f92edc6bde05c8ba3ce392873fc
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Apr 17 15:39:53 2023 -0400

    20031: Add includeDirectories flag to ProjectsTreePicker and add collection/directory only picker
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/models/collection-file.ts b/src/models/collection-file.ts
index 91008d1f..3688557a 100644
--- a/src/models/collection-file.ts
+++ b/src/models/collection-file.ts
@@ -71,10 +71,10 @@ export const createCollectionFilesTree = (data: Array<CollectionDirectory | Coll
 
 const getParentId = (item: CollectionDirectory | CollectionFile) =>
     item.path
-        ? join('', [getCollectionId(item.id), item.path])
+        ? join('', [getCollectionResourceCollectionUuid(item.id), item.path])
         : item.path;
 
-const getCollectionId = pipe(
+export const getCollectionResourceCollectionUuid = pipe(
     split('/'),
     head,
-);
\ No newline at end of file
+);
diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts
index 359b6b83..2a3fd5de 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -25,7 +25,7 @@ export interface CollectionPartialCopyToNewCollectionFormData {
 }
 
 export interface CollectionPartialCopyToExistingCollectionFormData {
-    collectionUuid: string;
+    destination: {uuid: string, path?: string};
 }
 
 export const openCollectionPartialCopyToNewCollectionDialog = () =>
@@ -102,7 +102,7 @@ export const openCollectionPartialCopyToExistingCollectionDialog = () =>
         const currentCollection = getState().collectionPanel.item;
         if (currentCollection) {
             const initialData = {
-                collectionUuid: ''
+                destination: {uuid: '', destinationPath: ''}
             };
             dispatch(initialize(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION, initialData));
             dispatch<any>(resetPickerProjectTree());
@@ -111,13 +111,13 @@ export const openCollectionPartialCopyToExistingCollectionDialog = () =>
         }
     };
 
-export const copyCollectionPartialToExistingCollection = ({ collectionUuid }: CollectionPartialCopyToExistingCollectionFormData) =>
+export const copyCollectionPartialToExistingCollection = ({ destination }: CollectionPartialCopyToExistingCollectionFormData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const state = getState();
         // Get current collection
         const sourceCollection = state.collectionPanel.item;
 
-        if (sourceCollection) {
+        if (sourceCollection && destination.uuid) {
             try {
                 dispatch(startSubmit(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
                 dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
@@ -126,7 +126,7 @@ export const copyCollectionPartialToExistingCollection = ({ collectionUuid }: Co
                     .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
 
                 // Copy files
-                const updatedCollection = await services.collectionService.copyFiles(sourceCollection.portableDataHash, paths, {uuid: collectionUuid}, '/', false);
+                const updatedCollection = await services.collectionService.copyFiles(sourceCollection.portableDataHash, paths, {uuid: destination.uuid}, destination.path || '/', false);
                 dispatch(updateResources([updatedCollection]));
 
                 dispatch(dialogActions.CLOSE_DIALOG({ id: 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 de64d99a..6d3c45f3 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 { initialize, startSubmit } from "redux-form";
+import { FormErrors, initialize, startSubmit, stopSubmit } 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";
@@ -15,8 +15,8 @@ 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_COLLECTION_DIALOG';
-export const COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION = 'COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION_DIALOG';
+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 interface CollectionPartialMoveToNewCollectionFormData {
     name: string;
@@ -25,7 +25,7 @@ export interface CollectionPartialMoveToNewCollectionFormData {
 }
 
 export interface CollectionPartialMoveToExistingCollectionFormData {
-    collectionUuid: string;
+    destination: {uuid: string, path?: string};
 }
 
 export const openCollectionPartialMoveToNewCollectionDialog = () =>
@@ -98,7 +98,7 @@ export const openCollectionPartialMoveToExistingCollectionDialog = () =>
         const currentCollection = getState().collectionPanel.item;
         if (currentCollection) {
             const initialData = {
-                collectionUuid: ''
+                destination: {uuid: '', path: ''}
             };
             dispatch(initialize(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION, initialData));
             dispatch<any>(resetPickerProjectTree());
@@ -107,13 +107,13 @@ export const openCollectionPartialMoveToExistingCollectionDialog = () =>
         }
     };
 
-export const moveCollectionPartialToExistingCollection = ({ collectionUuid }: CollectionPartialMoveToExistingCollectionFormData) =>
+export const moveCollectionPartialToExistingCollection = ({ destination }: CollectionPartialMoveToExistingCollectionFormData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const state = getState();
         // Get current collection
         const sourceCollection = state.collectionPanel.item;
 
-        if (sourceCollection) {
+        if (sourceCollection && destination.uuid) {
             try {
                 dispatch(startSubmit(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION));
                 dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION));
@@ -122,7 +122,7 @@ export const moveCollectionPartialToExistingCollection = ({ collectionUuid }: Co
                     .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
 
                 // Move files
-                const updatedCollection = await services.collectionService.moveFiles(sourceCollection.uuid, sourceCollection.portableDataHash, paths, {uuid: collectionUuid}, '/', false);
+                const updatedCollection = await services.collectionService.moveFiles(sourceCollection.uuid, sourceCollection.portableDataHash, paths, {uuid: destination.uuid}, destination.path || '/', false);
                 dispatch(updateResources([updatedCollection]));
 
                 dispatch(dialogActions.CLOSE_DIALOG({ id: 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 460a23e3..b8003aa1 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 { createCollectionFilesTree } from "models/collection-file";
+import { CollectionFileType, createCollectionFilesTree } from "models/collection-file";
 import { Dispatch } from 'redux';
 import { RootState } from 'store/store';
 import { getUserUuid } from "common/getuser";
@@ -42,6 +42,7 @@ export type TreePickerAction = UnionOf<typeof treePickerActions>;
 
 export interface LoadProjectParams {
     includeCollections?: boolean;
+    includeDirectories?: boolean;
     includeFiles?: boolean;
     includeFilterGroups?: boolean;
     options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
@@ -123,7 +124,17 @@ interface LoadProjectParamsWithId extends LoadProjectParams {
 
 export const loadProject = (params: LoadProjectParamsWithId) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false, options, searchProjects = false } = params;
+        const {
+            id,
+            pickerId,
+            includeCollections = false,
+            includeDirectories = false,
+            includeFiles = false,
+            includeFilterGroups = false,
+            loadShared = false,
+            options,
+            searchProjects = false
+        } = params;
 
         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
 
@@ -194,14 +205,14 @@ export const loadProject = (params: LoadProjectParamsWithId) =>
                         value: item,
                         status: item.kind === ResourceKind.PROJECT
                             ? TreeNodeStatus.INITIAL
-                            : includeFiles
+                            : includeDirectories || includeFiles
                                 ? TreeNodeStatus.INITIAL
                                 : TreeNodeStatus.LOADED
                     }),
         }));
     };
 
-export const loadCollection = (id: string, pickerId: string) =>
+export const loadCollection = (id: string, pickerId: string, includeDirectories?: boolean, includeFiles?: boolean) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
 
@@ -210,7 +221,11 @@ export const loadCollection = (id: string, pickerId: string) =>
 
             const node = getNode(id)(picker);
             if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
-                const files = await services.collectionService.files(node.value.uuid);
+                const files = (await services.collectionService.files(node.value.uuid))
+                    .filter((file) => (
+                        (includeFiles) ||
+                        (includeDirectories && file.type === CollectionFileType.DIRECTORY)
+                    ));
                 const tree = createCollectionFilesTree(files);
                 const sorted = sortFilesTree(tree);
                 const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
@@ -244,11 +259,11 @@ export const initUserProject = (pickerId: string) =>
             }));
         }
     };
-export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) =>
+export const loadUserProject = (pickerId: string, includeCollections = false, includeDirectories = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const uuid = getUserUuid(getState());
         if (uuid) {
-            dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles, options }));
+            dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeDirectories, includeFiles, options }));
         }
     };
 
@@ -316,6 +331,7 @@ export const initSearchProject = (pickerId: string) =>
 interface LoadFavoritesProjectParams {
     pickerId: string;
     includeCollections?: boolean;
+    includeDirectories?: boolean;
     includeFiles?: boolean;
     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
 }
@@ -323,7 +339,7 @@ interface LoadFavoritesProjectParams {
 export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
     options: { showOnlyOwned: boolean, showOnlyWritable: boolean } = { showOnlyOwned: true, showOnlyWritable: false }) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const { pickerId, includeCollections = false, includeFiles = false } = params;
+        const { pickerId, includeCollections = false, includeDirectories = false, includeFiles = false } = params;
         const uuid = getUserUuid(getState());
         if (uuid) {
             const filters = pipe(
@@ -354,7 +370,7 @@ export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
                     value: item,
                     status: item.kind === ResourceKind.PROJECT
                         ? TreeNodeStatus.INITIAL
-                        : includeFiles
+                        : includeDirectories || includeFiles
                             ? TreeNodeStatus.INITIAL
                             : TreeNodeStatus.LOADED
                 }),
@@ -364,7 +380,7 @@ export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
 
 export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const { pickerId, includeCollections = false, includeFiles = false } = params;
+        const { pickerId, includeCollections = false, includeDirectories = false, includeFiles = false } = params;
         const uuidPrefix = getState().auth.config.uuidPrefix;
         const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
 
@@ -395,7 +411,7 @@ export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =
                 value: item,
                 status: item.headKind === ResourceKind.PROJECT
                     ? TreeNodeStatus.INITIAL
-                    : includeFiles
+                    : includeDirectories || includeFiles
                         ? TreeNodeStatus.INITIAL
                         : TreeNodeStatus.LOADED
             }),
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 1af9dfee..f6d4db21 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
@@ -9,7 +9,7 @@ import { WithDialogProps } from 'store/dialog/with-dialog';
 import { InjectedFormProps } from 'redux-form';
 import { CollectionPartialCopyToExistingCollectionFormData } from 'store/collections/collection-partial-copy-actions';
 import { PickerIdProp } from "store/tree-picker/picker-id";
-import { CollectionPickerField } from 'views-components/form-fields/collection-form-fields';
+import { DirectoryPickerField } from 'views-components/form-fields/collection-form-fields';
 
 type DialogCollectionPartialCopyProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialCopyToExistingCollectionFormData>;
 
@@ -25,5 +25,5 @@ const CollectionPartialCopyFields = memoize(
     (pickerId: string) =>
         () =>
             <>
-                <CollectionPickerField {...{ pickerId }}/>
+                <DirectoryPickerField {...{ pickerId }}/>
             </>);
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 e8081037..f95bd24f 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
@@ -9,7 +9,7 @@ import { WithDialogProps } from 'store/dialog/with-dialog';
 import { InjectedFormProps } from 'redux-form';
 import { CollectionPartialMoveToExistingCollectionFormData } from "store/collections/collection-partial-move-actions";
 import { PickerIdProp } from "store/tree-picker/picker-id";
-import { CollectionPickerField } from 'views-components/form-fields/collection-form-fields';
+import { DirectoryPickerField } from 'views-components/form-fields/collection-form-fields';
 
 type DialogCollectionPartialMoveProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialMoveToExistingCollectionFormData>;
 
@@ -25,5 +25,5 @@ const CollectionPartialMoveFields = memoize(
     (pickerId: string) =>
         () =>
             <>
-                <CollectionPickerField {...{ pickerId }}/>
+                <DirectoryPickerField {...{ pickerId }}/>
             </>);
diff --git a/src/views-components/form-fields/collection-form-fields.tsx b/src/views-components/form-fields/collection-form-fields.tsx
index 7e18111a..0faa59b5 100644
--- a/src/views-components/form-fields/collection-form-fields.tsx
+++ b/src/views-components/form-fields/collection-form-fields.tsx
@@ -9,12 +9,13 @@ import {
     COLLECTION_NAME_VALIDATION, COLLECTION_NAME_VALIDATION_ALLOW_SLASH,
     COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION
 } from "validators/validators";
-import { ProjectTreePickerField, CollectionTreePickerField } from "views-components/projects-tree-picker/tree-picker-field";
+import { ProjectTreePickerField, CollectionTreePickerField, DirectoryTreePickerField } from "views-components/projects-tree-picker/tree-picker-field";
 import { PickerIdProp } from 'store/tree-picker/picker-id';
 import { connect } from "react-redux";
 import { RootState } from "store/store";
 import { MultiCheckboxField } from "components/checkbox-field/checkbox-field";
 import { getStorageClasses } from "common/config";
+import { ERROR_MESSAGE } from "validators/require";
 
 interface CollectionNameFieldProps {
     validate: Validator[];
@@ -58,6 +59,15 @@ export const CollectionPickerField = (props: PickerIdProp) =>
         component={CollectionTreePickerField}
         validate={COLLECTION_PROJECT_VALIDATION} />;
 
+const validateDirectory = (val) => (val ? undefined : ERROR_MESSAGE);
+
+export const DirectoryPickerField = (props: PickerIdProp) =>
+    <Field
+        name="destination"
+        pickerId={props.pickerId}
+        component={DirectoryTreePickerField}
+        validate={validateDirectory} />;
+
 interface StorageClassesProps {
     items: string[];
     defaultClasses?: string[];
@@ -78,4 +88,4 @@ export const CollectionStorageClassesField = connect(
             defaultValues={props.defaultClasses}
             helperText='At least one class should be selected'
             component={MultiCheckboxField}
-            items={props.items} />);
\ No newline at end of file
+            items={props.items} />);
diff --git a/src/views-components/projects-tree-picker/favorites-tree-picker.tsx b/src/views-components/projects-tree-picker/favorites-tree-picker.tsx
index 6ab2b42d..7e63152b 100644
--- a/src/views-components/projects-tree-picker/favorites-tree-picker.tsx
+++ b/src/views-components/projects-tree-picker/favorites-tree-picker.tsx
@@ -11,7 +11,7 @@ import { loadFavoritesProject } from 'store/tree-picker/tree-picker-actions';
 export const FavoritesTreePicker = connect(() => ({
     rootItemIcon: FavoriteIcon,
 }), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
-    loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
-        dispatch<any>(loadFavoritesProject({ pickerId, includeCollections, includeFiles }, options));
+    loadRootItem: (_, pickerId, includeCollections, includeDirectories, includeFiles, options) => {
+        dispatch<any>(loadFavoritesProject({ pickerId, includeCollections, includeDirectories, includeFiles }, options));
     },
-}))(ProjectsTreePicker);
\ No newline at end of file
+}))(ProjectsTreePicker);
diff --git a/src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx b/src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx
index 11b51caa..1ed2a551 100644
--- a/src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx
+++ b/src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx
@@ -22,6 +22,7 @@ type PickedTreePickerProps = Pick<TreePickerProps<ProjectsTreePickerItem>, 'onCo
 
 export interface ProjectsTreePickerDataProps {
     includeCollections?: boolean;
+    includeDirectories?: boolean;
     includeFiles?: boolean;
     rootItemIcon: IconType;
     showSelection?: boolean;
@@ -29,7 +30,7 @@ export interface ProjectsTreePickerDataProps {
     disableActivation?: string[];
     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
     loadRootItem: (item: TreeItem<ProjectsTreePickerRootItem>, pickerId: string,
-        includeCollections?: boolean, includeFiles?: boolean, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) => void;
+        includeCollections?: boolean, includeDirectories?: boolean, includeFiles?: boolean, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) => void;
 }
 
 export type ProjectsTreePickerProps = ProjectsTreePickerDataProps & Partial<PickedTreePickerProps>;
@@ -39,7 +40,7 @@ const mapStateToProps = (_: any, { rootItemIcon, showSelection }: ProjectsTreePi
     showSelection: isSelectionVisible(showSelection),
 });
 
-const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollections, includeFiles, relatedTreePickers, options, ...props }: ProjectsTreePickerProps): PickedTreePickerProps => ({
+const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollections, includeDirectories, includeFiles, relatedTreePickers, options, ...props }: ProjectsTreePickerProps): PickedTreePickerProps => ({
     onContextMenu: () => { return; },
     toggleItemActive: (event, item, pickerId) => {
 
@@ -59,11 +60,11 @@ const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollectio
             if ('kind' in data) {
                 dispatch<any>(
                     data.kind === ResourceKind.COLLECTION
-                        ? loadCollection(id, pickerId)
-                        : loadProject({ id, pickerId, includeCollections, includeFiles, options })
+                        ? loadCollection(id, pickerId, includeDirectories, includeFiles)
+                        : loadProject({ id, pickerId, includeCollections, includeDirectories, includeFiles, options })
                 );
             } else if (!('type' in data) && loadRootItem) {
-                loadRootItem(item as TreeItem<ProjectsTreePickerRootItem>, pickerId, includeCollections, includeFiles, options);
+                loadRootItem(item as TreeItem<ProjectsTreePickerRootItem>, pickerId, includeCollections, includeDirectories, includeFiles, options);
             }
         } else if (status === TreeItemStatus.LOADED) {
             dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
diff --git a/src/views-components/projects-tree-picker/home-tree-picker.tsx b/src/views-components/projects-tree-picker/home-tree-picker.tsx
index 3133c5db..3f71a58e 100644
--- a/src/views-components/projects-tree-picker/home-tree-picker.tsx
+++ b/src/views-components/projects-tree-picker/home-tree-picker.tsx
@@ -11,7 +11,7 @@ import { ProjectsIcon } from 'components/icon/icon';
 export const HomeTreePicker = connect(() => ({
     rootItemIcon: ProjectsIcon,
 }), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
-    loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
-        dispatch<any>(loadUserProject(pickerId, includeCollections, includeFiles, options));
+    loadRootItem: (_, pickerId, includeCollections, includeDirectories, includeFiles, options) => {
+        dispatch<any>(loadUserProject(pickerId, includeCollections, includeDirectories, includeFiles, options));
     },
 }))(ProjectsTreePicker);
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 9ac0b64f..1f036829 100644
--- a/src/views-components/projects-tree-picker/projects-tree-picker.tsx
+++ b/src/views-components/projects-tree-picker/projects-tree-picker.tsx
@@ -25,6 +25,7 @@ import { ArvadosTheme } from 'common/custom-theme';
 export interface ToplevelPickerProps {
     pickerId: string;
     includeCollections?: boolean;
+    includeDirectories?: boolean;
     includeFiles?: boolean;
     showSelection?: boolean;
     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
@@ -55,6 +56,7 @@ const mapDispatchToProps = (dispatch: Dispatch, props: ToplevelPickerProps): (Pr
     const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(props.pickerId);
     const params = {
         includeCollections: props.includeCollections,
+        includeDirectories: props.includeDirectories,
         includeFiles: props.includeFiles,
         options: props.options
     };
@@ -133,6 +135,7 @@ export const ProjectsTreePicker = connect(mapStateToProps, mapDispatchToProps)(
                 const relatedTreePickers = getRelatedTreePickers(pickerId);
                 const p = {
                     includeCollections: this.props.includeCollections,
+                    includeDirectories: this.props.includeDirectories,
                     includeFiles: this.props.includeFiles,
                     showSelection: this.props.showSelection,
                     options: this.props.options,
diff --git a/src/views-components/projects-tree-picker/public-favorites-tree-picker.tsx b/src/views-components/projects-tree-picker/public-favorites-tree-picker.tsx
index 91551c9a..ca03f728 100644
--- a/src/views-components/projects-tree-picker/public-favorites-tree-picker.tsx
+++ b/src/views-components/projects-tree-picker/public-favorites-tree-picker.tsx
@@ -11,7 +11,7 @@ import { loadPublicFavoritesProject } from 'store/tree-picker/tree-picker-action
 export const PublicFavoritesTreePicker = connect(() => ({
     rootItemIcon: PublicFavoriteIcon,
 }), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
-    loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
-        dispatch<any>(loadPublicFavoritesProject({ pickerId, includeCollections, includeFiles, options }));
+    loadRootItem: (_, pickerId, includeCollections, includeDirectories, includeFiles, options) => {
+        dispatch<any>(loadPublicFavoritesProject({ pickerId, includeCollections, includeDirectories, includeFiles, options }));
     },
-}))(ProjectsTreePicker);
\ No newline at end of file
+}))(ProjectsTreePicker);
diff --git a/src/views-components/projects-tree-picker/search-projects-picker.tsx b/src/views-components/projects-tree-picker/search-projects-picker.tsx
index 7bad8ef7..2888050b 100644
--- a/src/views-components/projects-tree-picker/search-projects-picker.tsx
+++ b/src/views-components/projects-tree-picker/search-projects-picker.tsx
@@ -12,7 +12,7 @@ import { SEARCH_PROJECT_ID } from 'store/tree-picker/tree-picker-actions';
 export const SearchProjectsPicker = connect(() => ({
     rootItemIcon: SearchIcon,
 }), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
-    loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
-        dispatch<any>(loadProject({ id: SEARCH_PROJECT_ID, pickerId, includeCollections, includeFiles, searchProjects: true, options }));
+    loadRootItem: (_, pickerId, includeCollections, includeDirectories, includeFiles, options) => {
+        dispatch<any>(loadProject({ id: SEARCH_PROJECT_ID, pickerId, includeCollections, includeDirectories, includeFiles, searchProjects: true, options }));
     },
 }))(ProjectsTreePicker);
diff --git a/src/views-components/projects-tree-picker/shared-tree-picker.tsx b/src/views-components/projects-tree-picker/shared-tree-picker.tsx
index c15df6ba..1914cd9d 100644
--- a/src/views-components/projects-tree-picker/shared-tree-picker.tsx
+++ b/src/views-components/projects-tree-picker/shared-tree-picker.tsx
@@ -12,7 +12,7 @@ import { SHARED_PROJECT_ID } from 'store/tree-picker/tree-picker-actions';
 export const SharedTreePicker = connect(() => ({
     rootItemIcon: ShareMeIcon,
 }), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
-    loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
-        dispatch<any>(loadProject({ id: SHARED_PROJECT_ID, pickerId, includeCollections, includeFiles, loadShared: true, options }));
+    loadRootItem: (_, pickerId, includeCollections, includeDirectories, includeFiles, options) => {
+        dispatch<any>(loadProject({ id: SHARED_PROJECT_ID, pickerId, includeCollections, includeDirectories, includeFiles, loadShared: true, options }));
     },
 }))(ProjectsTreePicker);
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 2afa606e..1414d18f 100644
--- a/src/views-components/projects-tree-picker/tree-picker-field.tsx
+++ b/src/views-components/projects-tree-picker/tree-picker-field.tsx
@@ -9,6 +9,8 @@ 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";
 
 export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
     <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
@@ -42,3 +44,36 @@ export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdPro
                 </Typography>}
         </div>
     </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('');
+        }
+    }
+
+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
+                pickerId={props.pickerId}
+                toggleItemActive={handleDirectoryChange(props)}
+                options={{ showOnlyOwned: false, showOnlyWritable: true }}
+                includeCollections
+                includeDirectories />
+            {props.meta.dirty && props.meta.error &&
+                <Typography variant='caption' color='error'>
+                    {props.meta.error}
+                </Typography>}
+        </div>
+    </div>;
diff --git a/src/views/run-process-panel/inputs/file-array-input.tsx b/src/views/run-process-panel/inputs/file-array-input.tsx
index a2f884e3..1e1a4299 100644
--- a/src/views/run-process-panel/inputs/file-array-input.tsx
+++ b/src/views/run-process-panel/inputs/file-array-input.tsx
@@ -254,6 +254,7 @@ const FileArrayInputComponent = connect(mapStateToProps)(
                                 <ProjectsTreePicker
                                     pickerId={this.props.commandInput.id}
                                     includeCollections
+                                    includeDirectories
                                     includeFiles
                                     showSelection
                                     options={this.props.options}
diff --git a/src/views/run-process-panel/inputs/file-input.tsx b/src/views/run-process-panel/inputs/file-input.tsx
index b0206e14..5f48f837 100644
--- a/src/views/run-process-panel/inputs/file-input.tsx
+++ b/src/views/run-process-panel/inputs/file-input.tsx
@@ -142,6 +142,7 @@ const FileInputComponent = connect()(
                             <ProjectsTreePicker
                                 pickerId={this.props.commandInput.id}
                                 includeCollections
+                                includeDirectories
                                 includeFiles
                                 options={this.props.options}
                                 toggleItemActive={this.setFile} />

commit 80b9537fc9d1ae6f8c907efdf01be0505c693eb8
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Apr 12 09:38:27 2023 -0400

    20031: Add ability to create and update collections during replace_files, moveFiles, copyFiles.
    
    Used to optimize move/copy to new to reduce api calls
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/services/collection-service/collection-service.test.ts b/src/services/collection-service/collection-service.test.ts
index 304cbfd3..c8e907d3 100644
--- a/src/services/collection-service/collection-service.test.ts
+++ b/src/services/collection-service/collection-service.test.ts
@@ -234,7 +234,7 @@ describe('collection-service', () => {
             const destinationPath = '/destinationPath';
 
             // when
-            await collectionService.copyFiles(sourcePdh, filePaths, destinationUuid, destinationPath);
+            await collectionService.copyFiles(sourcePdh, filePaths, {uuid: destinationUuid}, destinationPath);
 
             // then
             expect(serverApi.put).toHaveBeenCalledTimes(1);
@@ -261,7 +261,7 @@ describe('collection-service', () => {
             const destinationUuid = 'zzzzz-4zz18-ywq0rvhwwhkjnfq';
             const destinationPath = '/destinationPath';
 
-            await collectionService.copyFiles(sourcePdh, filePaths, destinationUuid, destinationPath);
+            await collectionService.copyFiles(sourcePdh, filePaths, {uuid: destinationUuid}, destinationPath);
 
             expect(serverApi.put).toHaveBeenCalledTimes(1);
             expect(serverApi.put).toHaveBeenCalledWith(
@@ -285,7 +285,7 @@ describe('collection-service', () => {
             const destinationUuid = 'zzzzz-4zz18-ywq0rvhwwhkjnfq';
             const destinationPath = '/';
 
-            await collectionService.copyFiles(sourcePdh, filePaths, destinationUuid, destinationPath);
+            await collectionService.copyFiles(sourcePdh, filePaths, {uuid: destinationUuid}, destinationPath);
 
             expect(serverApi.put).toHaveBeenCalledTimes(1);
             expect(serverApi.put).toHaveBeenCalledWith(
@@ -313,7 +313,7 @@ describe('collection-service', () => {
             const destinationPath = '/destinationPath';
 
             // when
-            await collectionService.moveFiles(srcCollectionUUID, srcCollectionPdh, filePaths, destinationUuid, destinationPath);
+            await collectionService.moveFiles(srcCollectionUUID, srcCollectionPdh, filePaths, {uuid: destinationUuid}, destinationPath);
 
             // then
             expect(serverApi.put).toHaveBeenCalledTimes(2);
@@ -357,7 +357,7 @@ describe('collection-service', () => {
             const destinationPath = '/destinationPath';
 
             // when
-            await collectionService.moveFiles(srcCollectionUUID, srcCollectionPdh, filePaths, srcCollectionUUID, destinationPath);
+            await collectionService.moveFiles(srcCollectionUUID, srcCollectionPdh, filePaths, {uuid: srcCollectionUUID}, destinationPath);
 
             // then
             expect(serverApi.put).toHaveBeenCalledTimes(1);
@@ -399,7 +399,7 @@ describe('collection-service', () => {
 
             // when
             try {
-                await collectionService.moveFiles(srcCollectionUUID, srcCollectionPdh, filePaths, destinationUuid, destinationPath);
+                await collectionService.moveFiles(srcCollectionUUID, srcCollectionPdh, filePaths, {uuid: destinationUuid}, destinationPath);
             } catch {}
 
             // then
diff --git a/src/services/collection-service/collection-service.ts b/src/services/collection-service/collection-service.ts
index 74cf7595..7d734fa6 100644
--- a/src/services/collection-service/collection-service.ts
+++ b/src/services/collection-service/collection-service.ts
@@ -12,8 +12,11 @@ import { TrashableResourceService } from "services/common-service/trashable-reso
 import { ApiActions } from "services/api/api-actions";
 import { Session } from "models/session";
 import { CommonService } from "services/common-service/common-service";
+import { snakeCase } from "lodash";
 
 export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
+type CollectionPartialUpdateOrCreate =  Partial<CollectionResource> & Pick<CollectionResource, "uuid"> |
+                                        Partial<CollectionResource> & Pick<CollectionResource, "ownerUuid">;
 
 export const emptyCollectionPdh = 'd41d8cd98f00b204e9800998ecf8427e+0';
 
@@ -67,21 +70,33 @@ export class CollectionService extends TrashableResourceService<CollectionResour
         }, "/");
     }
 
-    private replaceFiles(collectionUuid: string, fileMap: {}, showErrors?: boolean) {
+    private replaceFiles(data: CollectionPartialUpdateOrCreate, fileMap: {}, showErrors?: boolean) {
         const payload = {
             collection: {
-                preserve_version: true
+                preserve_version: true,
+                ...CommonService.mapKeys(snakeCase)(data),
+                // Don't send uuid in payload when creating
+                uuid: undefined,
             },
             replace_files: fileMap
         };
-
-        return CommonService.defaultResponse(
-            this.serverApi
-                .put<CollectionResource>(`/${this.resourceType}/${collectionUuid}`, payload),
-            this.actions,
-            true, // mapKeys
-            showErrors
-        );
+        if (data.uuid) {
+            return CommonService.defaultResponse(
+                this.serverApi
+                    .put<CollectionResource>(`/${this.resourceType}/${data.uuid}`, payload),
+                this.actions,
+                true, // mapKeys
+                showErrors
+            );
+        } else {
+            return CommonService.defaultResponse(
+                this.serverApi
+                    .post<CollectionResource>(`/${this.resourceType}`, payload),
+                this.actions,
+                true, // mapKeys
+                showErrors
+            );
+        }
     }
 
     async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress, targetLocation: string = '') {
@@ -94,7 +109,7 @@ export class CollectionService extends TrashableResourceService<CollectionResour
     }
 
     async renameFile(collectionUuid: string, collectionPdh: string, oldPath: string, newPath: string) {
-        return this.replaceFiles(collectionUuid, {
+        return this.replaceFiles({uuid: collectionUuid}, {
             [this.combineFilePath([newPath])]: `${collectionPdh}${this.combineFilePath([oldPath])}`,
             [this.combineFilePath([oldPath])]: '',
         });
@@ -152,10 +167,10 @@ export class CollectionService extends TrashableResourceService<CollectionResour
             }
         }, {})
 
-        return this.replaceFiles(collectionUuid, fileMap, showErrors);
+        return this.replaceFiles({uuid: collectionUuid}, fileMap, showErrors);
     }
 
-    copyFiles(sourcePdh: string, files: string[], destinationCollectionUuid: string, destinationPath: string, showErrors?: boolean) {
+    copyFiles(sourcePdh: string, files: string[], destinationCollection: CollectionPartialUpdateOrCreate, destinationPath: string, showErrors?: boolean) {
         const fileMap = files.reduce((obj, sourceFile) => {
             const sourceFileName = sourceFile.split('/').filter(Boolean).slice(-1).join("");
             return {
@@ -164,11 +179,11 @@ export class CollectionService extends TrashableResourceService<CollectionResour
             };
         }, {});
 
-        return this.replaceFiles(destinationCollectionUuid, fileMap, showErrors);
+        return this.replaceFiles(destinationCollection, fileMap, showErrors);
     }
 
-    moveFiles(sourceUuid: string, sourcePdh: string, files: string[], destinationCollectionUuid: string, destinationPath: string, showErrors?: boolean) {
-        if (sourceUuid === destinationCollectionUuid) {
+    moveFiles(sourceUuid: string, sourcePdh: string, files: string[], destinationCollection: CollectionPartialUpdateOrCreate, destinationPath: string, showErrors?: boolean) {
+        if (sourceUuid === destinationCollection.uuid) {
             const fileMap = files.reduce((obj, sourceFile) => {
                 const sourceFileName = sourceFile.split('/').filter(Boolean).slice(-1).join("");
                 return {
@@ -178,9 +193,9 @@ export class CollectionService extends TrashableResourceService<CollectionResour
                 };
             }, {});
 
-            return this.replaceFiles(sourceUuid, fileMap, showErrors)
+            return this.replaceFiles({uuid: sourceUuid}, fileMap, showErrors)
         } else {
-            return this.copyFiles(sourcePdh, files, destinationCollectionUuid, destinationPath, showErrors)
+            return this.copyFiles(sourcePdh, files, destinationCollection, destinationPath, showErrors)
                 .then(() => {
                     return this.deleteFiles(sourceUuid, files, showErrors);
                 });
@@ -190,7 +205,7 @@ export class CollectionService extends TrashableResourceService<CollectionResour
     createDirectory(collectionUuid: string, path: string, showErrors?: boolean) {
         const fileMap = {[this.combineFilePath([path])]: emptyCollectionPdh};
 
-        return this.replaceFiles(collectionUuid, fileMap, showErrors);
+        return this.replaceFiles({uuid: collectionUuid}, fileMap, showErrors);
     }
 
 }
diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts
index cccd7023..359b6b83 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -55,20 +55,23 @@ export const copyCollectionPartialToNewCollection = ({ name, description, projec
                 dispatch(startSubmit(COLLECTION_PARTIAL_COPY_FORM_NAME));
                 dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
 
-                // Create new collection
-                const newCollection = await services.collectionService.create({
-                    name,
-                    description,
-                    ownerUuid: projectUuid,
-                    uuid: undefined,
-                });
-
                 // Get selected files
                 const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, true)
                     .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
 
                 // Copy files
-                const updatedCollection = await services.collectionService.copyFiles(sourceCollection.portableDataHash, paths, newCollection.uuid, '/', false);
+                const updatedCollection = await services.collectionService.copyFiles(
+                    sourceCollection.portableDataHash,
+                    paths,
+                    {
+                        name,
+                        description,
+                        ownerUuid: projectUuid,
+                        uuid: undefined,
+                    },
+                    '/',
+                    false
+                );
                 dispatch(updateResources([updatedCollection]));
 
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
@@ -123,7 +126,7 @@ export const copyCollectionPartialToExistingCollection = ({ collectionUuid }: Co
                     .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
 
                 // Copy files
-                const updatedCollection = await services.collectionService.copyFiles(sourceCollection.portableDataHash, paths, collectionUuid, '/', false);
+                const updatedCollection = await services.collectionService.copyFiles(sourceCollection.portableDataHash, paths, {uuid: collectionUuid}, '/', false);
                 dispatch(updateResources([updatedCollection]));
 
                 dispatch(dialogActions.CLOSE_DIALOG({ id: 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 19cac52d..de64d99a 100644
--- a/src/store/collections/collection-partial-move-actions.ts
+++ b/src/store/collections/collection-partial-move-actions.ts
@@ -55,20 +55,24 @@ export const moveCollectionPartialToNewCollection = ({ name, description, projec
                 dispatch(startSubmit(COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION));
                 dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION));
 
-                // Create new collection
-                const newCollection = await services.collectionService.create({
-                    name,
-                    description,
-                    ownerUuid: projectUuid,
-                    uuid: undefined,
-                });
-
                 // Get selected files
                 const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, true)
                     .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
 
                 // Move files
-                const updatedCollection = await services.collectionService.moveFiles(sourceCollection.uuid, sourceCollection.portableDataHash, paths, newCollection.uuid, '/', false);
+                const updatedCollection = await services.collectionService.moveFiles(
+                    sourceCollection.uuid,
+                    sourceCollection.portableDataHash,
+                    paths,
+                    {
+                        name,
+                        description,
+                        ownerUuid: projectUuid,
+                        uuid: undefined,
+                    },
+                    '/',
+                    false
+                );
                 dispatch(updateResources([updatedCollection]));
 
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION }));
@@ -118,7 +122,7 @@ export const moveCollectionPartialToExistingCollection = ({ collectionUuid }: Co
                     .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
 
                 // Move files
-                const updatedCollection = await services.collectionService.moveFiles(sourceCollection.uuid, sourceCollection.portableDataHash, paths, collectionUuid, '/', false);
+                const updatedCollection = await services.collectionService.moveFiles(sourceCollection.uuid, sourceCollection.portableDataHash, paths, {uuid: collectionUuid}, '/', false);
                 dispatch(updateResources([updatedCollection]));
 
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION }));

commit b6c145e81728b3916370000a83648280bbfb94d2
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Apr 10 15:29:32 2023 -0400

    20031: Use replace files api for copy 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 73d117c2..cccd7023 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -46,30 +46,31 @@ export const openCollectionPartialCopyToNewCollectionDialog = () =>
 
 export const copyCollectionPartialToNewCollection = ({ name, description, projectUuid }: CollectionPartialCopyToNewCollectionFormData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(startSubmit(COLLECTION_PARTIAL_COPY_FORM_NAME));
         const state = getState();
-        const currentCollection = state.collectionPanel.item;
-        if (currentCollection) {
+        // 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));
-                const collectionManifestText = await services.collectionService.get(currentCollection.uuid, undefined, ['manifestText']);
-                const collectionCopy = {
+
+                // Create new collection
+                const newCollection = await services.collectionService.create({
                     name,
                     description,
                     ownerUuid: projectUuid,
                     uuid: undefined,
-                    manifestText: collectionManifestText.manifestText,
-                };
-                const newCollection = await services.collectionService.create(collectionCopy);
-                const copiedFiles = await services.collectionService.files(newCollection.uuid);
-                const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, true).map(file => file.id);
-                const filesToDelete = copiedFiles.map(({ id }) => id).filter(file => {
-                    return !paths.find(path => path.indexOf(file.replace(newCollection.uuid, '')) > -1);
                 });
-                await services.collectionService.deleteFiles(
-                    newCollection.uuid,
-                    filesToDelete
-                );
+
+                // Get selected files
+                const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, true)
+                    .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
+
+                // Copy files
+                const updatedCollection = await services.collectionService.copyFiles(sourceCollection.portableDataHash, paths, newCollection.uuid, '/', false);
+                dispatch(updateResources([updatedCollection]));
+
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
                 dispatch(snackbarActions.OPEN_SNACKBAR({
                     message: 'New collection created.',

commit c6b3db3a8e2fcb7fe8d44cdd60e12fe7e2be571f
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Apr 10 15:16:39 2023 -0400

    20031: Add collection partial move/copy to new/existing collection
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index 1a66600c..977a28c2 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -922,7 +922,7 @@ describe('Collection panel tests', function () {
                 });
 
                 cy.get('[data-cy=collection-files-panel-options-btn]').click();
-                cy.get('[data-cy=context-menu]').contains('Create a new collection with selected').click();
+                cy.get('[data-cy=context-menu]').contains('Copy selected into new collection').click();
 
                 cy.get('[data-cy=form-dialog]').contains('Projects').click();
 
diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts b/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts
index 0044a66d..405e7eac 100644
--- a/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts
+++ b/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts
@@ -40,5 +40,8 @@ export const filterCollectionFilesBySelection = (tree: CollectionPanelFilesState
     const allFiles = getNodeDescendants('')(tree).map(node => node.value);
     const selectedDirectories = allFiles.filter(file => file.selected === selected && file.type === CollectionFileType.DIRECTORY);
     const selectedFiles = allFiles.filter(file => file.selected === selected && !selectedDirectories.some(dir => dir.id === file.path));
-    return [...selectedDirectories, ...selectedFiles];
+    return [...selectedDirectories, ...selectedFiles]
+        .filter((value, index, array) => (
+            array.indexOf(value) === index
+        ));
 };
diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts
index a0c4e4e4..73d117c2 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from 'redux';
-import { difference } from "lodash";
 import { RootState } from 'store/store';
 import { FormErrors, initialize, startSubmit, stopSubmit } from 'redux-form';
 import { resetPickerProjectTree } from 'store/project-tree-picker/project-tree-picker-actions';
@@ -14,21 +13,22 @@ 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';
 
 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 interface CollectionPartialCopyFormData {
+export interface CollectionPartialCopyToNewCollectionFormData {
     name: string;
     description: string;
     projectUuid: string;
 }
 
-export interface CollectionPartialCopyToSelectedCollectionFormData {
+export interface CollectionPartialCopyToExistingCollectionFormData {
     collectionUuid: string;
 }
 
-export const openCollectionPartialCopyDialog = () =>
+export const openCollectionPartialCopyToNewCollectionDialog = () =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const currentCollection = getState().collectionPanel.item;
         if (currentCollection) {
@@ -44,7 +44,7 @@ export const openCollectionPartialCopyDialog = () =>
         }
     };
 
-export const copyCollectionPartial = ({ name, description, projectUuid }: CollectionPartialCopyFormData) =>
+export const copyCollectionPartialToNewCollection = ({ name, description, projectUuid }: CollectionPartialCopyToNewCollectionFormData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_PARTIAL_COPY_FORM_NAME));
         const state = getState();
@@ -93,7 +93,7 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec
         }
     };
 
-export const openCollectionPartialCopyToSelectedCollectionDialog = () =>
+export const openCollectionPartialCopyToExistingCollectionDialog = () =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const currentCollection = getState().collectionPanel.item;
         if (currentCollection) {
@@ -107,37 +107,24 @@ export const openCollectionPartialCopyToSelectedCollectionDialog = () =>
         }
     };
 
-export const copyCollectionPartialToSelectedCollection = ({ collectionUuid }: CollectionPartialCopyToSelectedCollectionFormData) =>
+export const copyCollectionPartialToExistingCollection = ({ collectionUuid }: CollectionPartialCopyToExistingCollectionFormData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(startSubmit(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
         const state = getState();
-        const currentCollection = state.collectionPanel.item;
-
-        if (currentCollection && !currentCollection.manifestText) {
-            const fetchedCurrentCollection = await services.collectionService.get(currentCollection.uuid, undefined, ['manifestText']);
-            currentCollection.manifestText = fetchedCurrentCollection.manifestText;
-            currentCollection.unsignedManifestText = fetchedCurrentCollection.unsignedManifestText;
-        }
+        // Get current collection
+        const sourceCollection = state.collectionPanel.item;
 
-        if (currentCollection) {
+        if (sourceCollection) {
             try {
+                dispatch(startSubmit(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
                 dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
-                const selectedCollection = await services.collectionService.get(collectionUuid);
-                const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, false).map(file => file.id);
-                const pathsToRemove = paths.filter(path => {
-                    const a = path.split('/');
-                    const fileExistsInSelectedCollection = selectedCollection.manifestText.includes(a[1]);
-                    if (fileExistsInSelectedCollection) {
-                        return path;
-                    } else {
-                        return null;
-                    }
-                });
-                const diffPathToRemove = difference(paths, pathsToRemove);
-                await services.collectionService.deleteFiles(selectedCollection.uuid, pathsToRemove.map(path => path.replace(currentCollection.uuid, collectionUuid)));
-                const collectionWithDeletedFiles = await services.collectionService.get(collectionUuid, undefined, ['uuid', 'manifestText']);
-                await services.collectionService.update(collectionUuid, { manifestText: `${collectionWithDeletedFiles.manifestText}${(currentCollection.manifestText ? currentCollection.manifestText : currentCollection.unsignedManifestText) || ''}` });
-                await services.collectionService.deleteFiles(collectionWithDeletedFiles.uuid, diffPathToRemove.map(path => path.replace(currentCollection.uuid, collectionUuid)));
+                // Get selected files
+                const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, true)
+                    .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
+
+                // Copy files
+                const updatedCollection = await services.collectionService.copyFiles(sourceCollection.portableDataHash, paths, collectionUuid, '/', false);
+                dispatch(updateResources([updatedCollection]));
+
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION }));
                 dispatch(snackbarActions.OPEN_SNACKBAR({
                     message: 'Files has been copied to selected collection.',
@@ -154,4 +141,4 @@ export const copyCollectionPartialToSelectedCollection = ({ collectionUuid }: Co
                 dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
             }
         }
-    };
\ No newline at end of file
+    };
diff --git a/src/store/collections/collection-partial-move-actions.ts b/src/store/collections/collection-partial-move-actions.ts
new file mode 100644
index 00000000..19cac52d
--- /dev/null
+++ b/src/store/collections/collection-partial-move-actions.ts
@@ -0,0 +1,140 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+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";
+import { dialogActions } from "store/dialog/dialog-actions";
+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";
+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_COLLECTION_DIALOG';
+export const COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION = 'COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION_DIALOG';
+
+export interface CollectionPartialMoveToNewCollectionFormData {
+    name: string;
+    description: string;
+    projectUuid: string;
+}
+
+export interface CollectionPartialMoveToExistingCollectionFormData {
+    collectionUuid: string;
+}
+
+export const openCollectionPartialMoveToNewCollectionDialog = () =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const currentCollection = getState().collectionPanel.item;
+        if (currentCollection) {
+            const initialData = {
+                name: `Files moved from: ${currentCollection.name}`,
+                description: currentCollection.description,
+                projectUuid: undefined
+            };
+            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: {} }));
+        }
+    };
+
+export const moveCollectionPartialToNewCollection = ({ name, description, projectUuid }: CollectionPartialMoveToNewCollectionFormData) =>
+    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_NEW_COLLECTION));
+                dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION));
+
+                // Create new collection
+                const newCollection = await services.collectionService.create({
+                    name,
+                    description,
+                    ownerUuid: projectUuid,
+                    uuid: undefined,
+                });
+
+                // Get selected files
+                const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, true)
+                    .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
+
+                // Move files
+                const updatedCollection = await services.collectionService.moveFiles(sourceCollection.uuid, sourceCollection.portableDataHash, paths, newCollection.uuid, '/', false);
+                dispatch(updateResources([updatedCollection]));
+
+                dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION }));
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: 'Files have been moved to selected collection.',
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION));
+            } catch (e) {
+                const error = getCommonResourceServiceError(e);
+                if (error === CommonResourceServiceError.UNKNOWN) {
+                    dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION }));
+                    dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move files to selected collection', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+                }
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION));
+            }
+        }
+    };
+
+export const openCollectionPartialMoveToExistingCollectionDialog = () =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const currentCollection = getState().collectionPanel.item;
+        if (currentCollection) {
+            const initialData = {
+                collectionUuid: ''
+            };
+            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: {} }));
+        }
+    };
+
+export const moveCollectionPartialToExistingCollection = ({ collectionUuid }: CollectionPartialMoveToExistingCollectionFormData) =>
+    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_SELECTED_COLLECTION));
+                dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION));
+                // Get selected files
+                const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, true)
+                    .map(file => file.id.replace(new RegExp(`(^${sourceCollection.uuid})`), ''));
+
+                // Move files
+                const updatedCollection = await services.collectionService.moveFiles(sourceCollection.uuid, sourceCollection.portableDataHash, paths, collectionUuid, '/', false);
+                dispatch(updateResources([updatedCollection]));
+
+                dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION }));
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: 'Files have been moved to selected collection.',
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION));
+            } catch (e) {
+                const error = getCommonResourceServiceError(e);
+                if (error === CommonResourceServiceError.UNKNOWN) {
+                    dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION }));
+                    dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not copy this files to selected collection', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+                }
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION));
+            }
+        }
+    };
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 f34f2868..3e6e1a20 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
@@ -5,10 +5,10 @@
 import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
 import { collectionPanelFilesAction, openMultipleFilesRemoveDialog } from "store/collection-panel/collection-panel-files/collection-panel-files-actions";
 import {
-    openCollectionPartialCopyDialog,
-    // Disabled while addressing #18587
-    // openCollectionPartialCopyToSelectedCollectionDialog
+    openCollectionPartialCopyToNewCollectionDialog,
+    openCollectionPartialCopyToExistingCollectionDialog
 } from 'store/collections/collection-partial-copy-actions';
+import { openCollectionPartialMoveToExistingCollectionDialog, openCollectionPartialMoveToNewCollectionDialog } from "store/collections/collection-partial-move-actions";
 
 // These action sets are used on the multi-select actions button.
 export const readOnlyCollectionFilesActionSet: ContextMenuActionSet = [[
@@ -25,18 +25,17 @@ export const readOnlyCollectionFilesActionSet: ContextMenuActionSet = [[
         }
     },
     {
-        name: "Create a new collection with selected",
+        name: "Copy selected into new collection",
         execute: dispatch => {
-            dispatch<any>(openCollectionPartialCopyDialog());
+            dispatch<any>(openCollectionPartialCopyToNewCollectionDialog());
         }
     },
-    // Disabled while addressing #18587
-    // {
-    //     name: "Copy selected into the collection",
-    //     execute: dispatch => {
-    //         dispatch<any>(openCollectionPartialCopyToSelectedCollectionDialog());
-    //     }
-    // }
+    {
+        name: "Copy selected into existing collection",
+        execute: dispatch => {
+            dispatch<any>(openCollectionPartialCopyToExistingCollectionDialog());
+        }
+    }
 ]];
 
 export const collectionFilesActionSet: ContextMenuActionSet = readOnlyCollectionFilesActionSet.concat([[
@@ -46,4 +45,16 @@ export const collectionFilesActionSet: ContextMenuActionSet = readOnlyCollection
             dispatch(openMultipleFilesRemoveDialog());
         }
     },
+    {
+        name: "Move selected into new collection",
+        execute: dispatch => {
+            dispatch<any>(openCollectionPartialMoveToNewCollectionDialog());
+        }
+    },
+    {
+        name: "Move selected into existing collection",
+        execute: dispatch => {
+            dispatch<any>(openCollectionPartialMoveToExistingCollectionDialog());
+        }
+    }
 ]]);
diff --git a/src/views-components/dialog-copy/dialog-partial-copy-to-collection.tsx b/src/views-components/dialog-copy/dialog-collection-partial-copy-to-existing-collection.tsx
similarity index 76%
copy from src/views-components/dialog-copy/dialog-partial-copy-to-collection.tsx
copy to src/views-components/dialog-copy/dialog-collection-partial-copy-to-existing-collection.tsx
index 4e9dde6a..1af9dfee 100644
--- a/src/views-components/dialog-copy/dialog-partial-copy-to-collection.tsx
+++ b/src/views-components/dialog-copy/dialog-collection-partial-copy-to-existing-collection.tsx
@@ -7,21 +7,21 @@ import { memoize } from "lodash/fp";
 import { FormDialog } from 'components/form-dialog/form-dialog';
 import { WithDialogProps } from 'store/dialog/with-dialog';
 import { InjectedFormProps } from 'redux-form';
-import { CollectionPartialCopyToSelectedCollectionFormData } from 'store/collections/collection-partial-copy-actions';
+import { CollectionPartialCopyToExistingCollectionFormData } from 'store/collections/collection-partial-copy-actions';
 import { PickerIdProp } from "store/tree-picker/picker-id";
 import { CollectionPickerField } from 'views-components/form-fields/collection-form-fields';
 
-type DialogCollectionPartialCopyProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialCopyToSelectedCollectionFormData>;
+type DialogCollectionPartialCopyProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialCopyToExistingCollectionFormData>;
 
-export const DialogCollectionPartialCopyToSelectedCollection = (props: DialogCollectionPartialCopyProps & PickerIdProp) =>
+export const DialogCollectionPartialCopyToExistingCollection = (props: DialogCollectionPartialCopyProps & PickerIdProp) =>
     <FormDialog
-        dialogTitle='Choose collection'
+        dialogTitle='Copy to existing collection'
         formFields={CollectionPartialCopyFields(props.pickerId)}
         submitLabel='Copy files'
         {...props}
     />;
 
-export const CollectionPartialCopyFields = memoize(
+const CollectionPartialCopyFields = memoize(
     (pickerId: string) =>
         () =>
             <>
diff --git a/src/views-components/dialog-copy/dialog-collection-partial-copy.tsx b/src/views-components/dialog-copy/dialog-collection-partial-copy-to-new-collection.tsx
similarity index 68%
copy from src/views-components/dialog-copy/dialog-collection-partial-copy.tsx
copy to src/views-components/dialog-copy/dialog-collection-partial-copy-to-new-collection.tsx
index 3c584e4f..6b5a7759 100644
--- a/src/views-components/dialog-copy/dialog-collection-partial-copy.tsx
+++ b/src/views-components/dialog-copy/dialog-collection-partial-copy-to-new-collection.tsx
@@ -8,20 +8,20 @@ import { FormDialog } from 'components/form-dialog/form-dialog';
 import { CollectionNameField, CollectionDescriptionField, CollectionProjectPickerField } from 'views-components/form-fields/collection-form-fields';
 import { WithDialogProps } from 'store/dialog/with-dialog';
 import { InjectedFormProps } from 'redux-form';
-import { CollectionPartialCopyFormData } from 'store/collections/collection-partial-copy-actions';
+import { CollectionPartialCopyToNewCollectionFormData } from 'store/collections/collection-partial-copy-actions';
 import { PickerIdProp } from "store/tree-picker/picker-id";
 
-type DialogCollectionPartialCopyProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialCopyFormData>;
+type DialogCollectionPartialCopyProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialCopyToNewCollectionFormData>;
 
-export const DialogCollectionPartialCopy = (props: DialogCollectionPartialCopyProps & PickerIdProp) =>
+export const DialogCollectionPartialCopyToNewCollection = (props: DialogCollectionPartialCopyProps & PickerIdProp) =>
     <FormDialog
-        dialogTitle='Create a collection'
+        dialogTitle='Copy to new collection'
         formFields={CollectionPartialCopyFields(props.pickerId)}
-        submitLabel='Create a collection'
+        submitLabel='Create collection'
         {...props}
     />;
 
-export const CollectionPartialCopyFields = memoize(
+const CollectionPartialCopyFields = memoize(
     (pickerId: string) =>
         () =>
             <>
diff --git a/src/views-components/dialog-forms/partial-copy-collection-dialog.ts b/src/views-components/dialog-forms/partial-copy-collection-dialog.ts
deleted file mode 100644
index 3630ffb7..00000000
--- a/src/views-components/dialog-forms/partial-copy-collection-dialog.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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 { CollectionPartialCopyFormData, copyCollectionPartial, COLLECTION_PARTIAL_COPY_FORM_NAME } from 'store/collections/collection-partial-copy-actions';
-import { DialogCollectionPartialCopy } from "views-components/dialog-copy/dialog-collection-partial-copy";
-import { pickerId } from "store/tree-picker/picker-id";
-
-
-export const PartialCopyCollectionDialog = compose(
-    withDialog(COLLECTION_PARTIAL_COPY_FORM_NAME),
-    reduxForm<CollectionPartialCopyFormData>({
-        form: COLLECTION_PARTIAL_COPY_FORM_NAME,
-        onSubmit: (data, dispatch) => {
-            dispatch(copyCollectionPartial(data));
-        }
-    }),
-    pickerId(COLLECTION_PARTIAL_COPY_FORM_NAME),
-)(DialogCollectionPartialCopy);
\ No newline at end of file
diff --git a/src/views-components/dialog-forms/partial-copy-to-collection-dialog.ts b/src/views-components/dialog-forms/partial-copy-to-existing-collection-dialog.ts
similarity index 54%
rename from src/views-components/dialog-forms/partial-copy-to-collection-dialog.ts
rename to src/views-components/dialog-forms/partial-copy-to-existing-collection-dialog.ts
index d7b33929..8fe57f2d 100644
--- a/src/views-components/dialog-forms/partial-copy-to-collection-dialog.ts
+++ b/src/views-components/dialog-forms/partial-copy-to-existing-collection-dialog.ts
@@ -5,17 +5,17 @@
 import { compose } from "redux";
 import { reduxForm } from 'redux-form';
 import { withDialog, } from 'store/dialog/with-dialog';
-import { CollectionPartialCopyToSelectedCollectionFormData, copyCollectionPartialToSelectedCollection, COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION } from 'store/collections/collection-partial-copy-actions';
-import { DialogCollectionPartialCopyToSelectedCollection } from "views-components/dialog-copy/dialog-partial-copy-to-collection";
+import { CollectionPartialCopyToExistingCollectionFormData, copyCollectionPartialToExistingCollection, COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION } from 'store/collections/collection-partial-copy-actions';
+import { DialogCollectionPartialCopyToExistingCollection } from "views-components/dialog-copy/dialog-collection-partial-copy-to-existing-collection";
 import { pickerId } from "store/tree-picker/picker-id";
 
-export const PartialCopyToCollectionDialog = compose(
+export const PartialCopyToExistingCollectionDialog = compose(
     withDialog(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION),
-    reduxForm<CollectionPartialCopyToSelectedCollectionFormData>({
+    reduxForm<CollectionPartialCopyToExistingCollectionFormData>({
         form: COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION,
         onSubmit: (data, dispatch) => {
-            dispatch(copyCollectionPartialToSelectedCollection(data));
+            dispatch(copyCollectionPartialToExistingCollection(data));
         }
     }),
     pickerId(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION),
-)(DialogCollectionPartialCopyToSelectedCollection);
\ No newline at end of file
+)(DialogCollectionPartialCopyToExistingCollection);
diff --git a/src/views-components/dialog-forms/partial-copy-to-new-collection-dialog.ts b/src/views-components/dialog-forms/partial-copy-to-new-collection-dialog.ts
new file mode 100644
index 00000000..40bb1478
--- /dev/null
+++ b/src/views-components/dialog-forms/partial-copy-to-new-collection-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 { CollectionPartialCopyToNewCollectionFormData, copyCollectionPartialToNewCollection, COLLECTION_PARTIAL_COPY_FORM_NAME } from 'store/collections/collection-partial-copy-actions';
+import { DialogCollectionPartialCopyToNewCollection } from "views-components/dialog-copy/dialog-collection-partial-copy-to-new-collection";
+import { pickerId } from "store/tree-picker/picker-id";
+
+export const PartialCopyToNewCollectionDialog = compose(
+    withDialog(COLLECTION_PARTIAL_COPY_FORM_NAME),
+    reduxForm<CollectionPartialCopyToNewCollectionFormData>({
+        form: COLLECTION_PARTIAL_COPY_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            dispatch(copyCollectionPartialToNewCollection(data));
+        }
+    }),
+    pickerId(COLLECTION_PARTIAL_COPY_FORM_NAME),
+)(DialogCollectionPartialCopyToNewCollection);
diff --git a/src/views-components/dialog-forms/partial-move-to-existing-collection-dialog.ts b/src/views-components/dialog-forms/partial-move-to-existing-collection-dialog.ts
new file mode 100644
index 00000000..cb18a663
--- /dev/null
+++ b/src/views-components/dialog-forms/partial-move-to-existing-collection-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_SELECTED_COLLECTION, CollectionPartialMoveToExistingCollectionFormData, moveCollectionPartialToExistingCollection } from "store/collections/collection-partial-move-actions";
+import { DialogCollectionPartialMoveToExistingCollection } from "views-components/dialog-move/dialog-collection-partial-move-to-existing-collection";
+import { pickerId } from "store/tree-picker/picker-id";
+
+export const PartialMoveToExistingCollectionDialog = compose(
+    withDialog(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION),
+    reduxForm<CollectionPartialMoveToExistingCollectionFormData>({
+        form: COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION,
+        onSubmit: (data, dispatch) => {
+            dispatch(moveCollectionPartialToExistingCollection(data));
+        }
+    }),
+    pickerId(COLLECTION_PARTIAL_MOVE_TO_SELECTED_COLLECTION),
+)(DialogCollectionPartialMoveToExistingCollection);
diff --git a/src/views-components/dialog-forms/partial-move-to-new-collection-dialog.ts b/src/views-components/dialog-forms/partial-move-to-new-collection-dialog.ts
new file mode 100644
index 00000000..d7251c8a
--- /dev/null
+++ b/src/views-components/dialog-forms/partial-move-to-new-collection-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_NEW_COLLECTION, CollectionPartialMoveToNewCollectionFormData, moveCollectionPartialToNewCollection } from "store/collections/collection-partial-move-actions";
+import { DialogCollectionPartialMoveToNewCollection } from "views-components/dialog-move/dialog-collection-partial-move-to-new-collection";
+import { pickerId } from "store/tree-picker/picker-id";
+
+export const PartialMoveToNewCollectionDialog = compose(
+    withDialog(COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION),
+    reduxForm<CollectionPartialMoveToNewCollectionFormData>({
+        form: COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION,
+        onSubmit: (data, dispatch) => {
+            dispatch(moveCollectionPartialToNewCollection(data));
+        }
+    }),
+    pickerId(COLLECTION_PARTIAL_MOVE_TO_NEW_COLLECTION),
+)(DialogCollectionPartialMoveToNewCollection);
diff --git a/src/views-components/dialog-copy/dialog-partial-copy-to-collection.tsx b/src/views-components/dialog-move/dialog-collection-partial-move-to-existing-collection.tsx
similarity index 54%
rename from src/views-components/dialog-copy/dialog-partial-copy-to-collection.tsx
rename to src/views-components/dialog-move/dialog-collection-partial-move-to-existing-collection.tsx
index 4e9dde6a..e8081037 100644
--- a/src/views-components/dialog-copy/dialog-partial-copy-to-collection.tsx
+++ b/src/views-components/dialog-move/dialog-collection-partial-move-to-existing-collection.tsx
@@ -7,21 +7,21 @@ import { memoize } from "lodash/fp";
 import { FormDialog } from 'components/form-dialog/form-dialog';
 import { WithDialogProps } from 'store/dialog/with-dialog';
 import { InjectedFormProps } from 'redux-form';
-import { CollectionPartialCopyToSelectedCollectionFormData } from 'store/collections/collection-partial-copy-actions';
+import { CollectionPartialMoveToExistingCollectionFormData } from "store/collections/collection-partial-move-actions";
 import { PickerIdProp } from "store/tree-picker/picker-id";
 import { CollectionPickerField } from 'views-components/form-fields/collection-form-fields';
 
-type DialogCollectionPartialCopyProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialCopyToSelectedCollectionFormData>;
+type DialogCollectionPartialMoveProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialMoveToExistingCollectionFormData>;
 
-export const DialogCollectionPartialCopyToSelectedCollection = (props: DialogCollectionPartialCopyProps & PickerIdProp) =>
+export const DialogCollectionPartialMoveToExistingCollection = (props: DialogCollectionPartialMoveProps & PickerIdProp) =>
     <FormDialog
-        dialogTitle='Choose collection'
-        formFields={CollectionPartialCopyFields(props.pickerId)}
-        submitLabel='Copy files'
+        dialogTitle='Move to existing collection'
+        formFields={CollectionPartialMoveFields(props.pickerId)}
+        submitLabel='Move files'
         {...props}
     />;
 
-export const CollectionPartialCopyFields = memoize(
+const CollectionPartialMoveFields = memoize(
     (pickerId: string) =>
         () =>
             <>
diff --git a/src/views-components/dialog-copy/dialog-collection-partial-copy.tsx b/src/views-components/dialog-move/dialog-collection-partial-move-to-new-collection.tsx
similarity index 59%
rename from src/views-components/dialog-copy/dialog-collection-partial-copy.tsx
rename to src/views-components/dialog-move/dialog-collection-partial-move-to-new-collection.tsx
index 3c584e4f..a33f377c 100644
--- a/src/views-components/dialog-copy/dialog-collection-partial-copy.tsx
+++ b/src/views-components/dialog-move/dialog-collection-partial-move-to-new-collection.tsx
@@ -8,20 +8,20 @@ import { FormDialog } from 'components/form-dialog/form-dialog';
 import { CollectionNameField, CollectionDescriptionField, CollectionProjectPickerField } from 'views-components/form-fields/collection-form-fields';
 import { WithDialogProps } from 'store/dialog/with-dialog';
 import { InjectedFormProps } from 'redux-form';
-import { CollectionPartialCopyFormData } from 'store/collections/collection-partial-copy-actions';
+import { CollectionPartialMoveToNewCollectionFormData } from "store/collections/collection-partial-move-actions";
 import { PickerIdProp } from "store/tree-picker/picker-id";
 
-type DialogCollectionPartialCopyProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialCopyFormData>;
+type DialogCollectionPartialMoveProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialMoveToNewCollectionFormData>;
 
-export const DialogCollectionPartialCopy = (props: DialogCollectionPartialCopyProps & PickerIdProp) =>
+export const DialogCollectionPartialMoveToNewCollection = (props: DialogCollectionPartialMoveProps & PickerIdProp) =>
     <FormDialog
-        dialogTitle='Create a collection'
-        formFields={CollectionPartialCopyFields(props.pickerId)}
-        submitLabel='Create a collection'
+        dialogTitle='Move to new collection'
+        formFields={CollectionPartialMoveFields(props.pickerId)}
+        submitLabel='Create collection'
         {...props}
     />;
 
-export const CollectionPartialCopyFields = memoize(
+const CollectionPartialMoveFields = memoize(
     (pickerId: string) =>
         () =>
             <>
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index d549c529..5b1fff39 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -32,7 +32,10 @@ import { MoveProcessDialog } from 'views-components/dialog-forms/move-process-di
 import { MoveProjectDialog } from 'views-components/dialog-forms/move-project-dialog';
 import { MoveCollectionDialog } from 'views-components/dialog-forms/move-collection-dialog';
 import { FilesUploadCollectionDialog } from 'views-components/dialog-forms/files-upload-collection-dialog';
-import { PartialCopyCollectionDialog } from 'views-components/dialog-forms/partial-copy-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 { 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 { 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';
@@ -88,7 +91,6 @@ import { GroupAttributesDialog } from 'views-components/groups-dialog/attributes
 import { GroupDetailsPanel } from 'views/group-details-panel/group-details-panel';
 import { RemoveGroupMemberDialog } from 'views-components/groups-dialog/member-remove-dialog';
 import { GroupMemberAttributesDialog } from 'views-components/groups-dialog/member-attributes-dialog';
-import { PartialCopyToCollectionDialog } from 'views-components/dialog-forms/partial-copy-to-collection-dialog';
 import { PublicFavoritePanel } from 'views/public-favorites-panel/public-favorites-panel';
 import { LinkAccountPanel } from 'views/link-account-panel/link-account-panel';
 import { FedLogin } from './fed-login';
@@ -264,8 +266,10 @@ export const WorkbenchPanel =
             <MoveProjectDialog />
             <MultipleFilesRemoveDialog />
             <PublicKeyDialog />
-            <PartialCopyCollectionDialog />
-            <PartialCopyToCollectionDialog />
+            <PartialCopyToNewCollectionDialog />
+            <PartialCopyToExistingCollectionDialog />
+            <PartialMoveToNewCollectionDialog />
+            <PartialMoveToExistingCollectionDialog />
             <ProcessInputDialog />
             <RestoreCollectionVersionDialog />
             <RemoveApiClientAuthorizationDialog />

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list