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