20031: Add split files into separate collection move/copy actions
authorStephen Smith <stephen@curii.com>
Wed, 19 Apr 2023 00:44:19 +0000 (20:44 -0400)
committerStephen Smith <stephen@curii.com>
Wed, 19 Apr 2023 00:44:19 +0000 (20:44 -0400)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

src/store/collections/collection-partial-copy-actions.ts
src/store/collections/collection-partial-move-actions.ts
src/views-components/context-menu/action-sets/collection-files-action-set.ts
src/views-components/dialog-copy/dialog-collection-partial-copy-to-separate-collections.tsx [new file with mode: 0644]
src/views-components/dialog-forms/partial-copy-to-separate-collections-dialog.ts [new file with mode: 0644]
src/views-components/dialog-forms/partial-move-to-separate-collections-dialog.ts [new file with mode: 0644]
src/views-components/dialog-move/dialog-collection-partial-move-to-separate-collections.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx

index efdfd96156a57724be3a5fe0f3b6e675ef2219ab..75ff4592b3db6fb54d1a84b7bbe8c4d07edf5149 100644 (file)
@@ -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));
+            }
+        }
+    };
index c5760ed85dec2867243436af28ec1c06813c7669..59f6100c8eac81dce22adff4766c403155b4bf43 100644 (file)
@@ -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));
+            }
+        }
+    };
index 3e6e1a201a9b48b528b911ae427b9a5511ef33cf..c1c541d33a9a3b2016c0ccba0bc3177e9aa5a6b6 100644 (file)
@@ -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 (file)
index 0000000..32f706a
--- /dev/null
@@ -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 (file)
index 0000000..e2687cb
--- /dev/null
@@ -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 (file)
index 0000000..8346709
--- /dev/null
@@ -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 (file)
index 0000000..1b71662
--- /dev/null
@@ -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 }} />
+            </>);
index 5b1fff39b9bcefedf2a9fb354f54affa67d79a31..ce9307465e19ebdcec22c46cbbe9236312649603 100644 (file)
@@ -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 />