20225: Add subdirectory selection support to directory input
authorStephen Smith <stephen@curii.com>
Thu, 21 Sep 2023 19:02:10 +0000 (15:02 -0400)
committerStephen Smith <stephen@curii.com>
Fri, 22 Sep 2023 14:37:09 +0000 (10:37 -0400)
Converts getFileOperationLocation into a dispatchable helper so it can access
the store to augment FileOperationLocations with a PDH

Converts DirectoryTreePickerField into a connected class component in order to
use getFileOperationLocation

Change DirectoryInputComponent to include directories in the picker and use
getFileOperationLocation to pass FileOperationLocation instead of Collection
objects so that the subpath within the collection can be embedded in the
Directory object

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/store/tree-picker/tree-picker-actions.ts
src/views-components/form-fields/collection-form-fields.tsx
src/views-components/projects-tree-picker/tree-picker-field.tsx
src/views/run-process-panel/inputs/directory-input.tsx

index 4b3af2a2cdc0273a9913b53ec7d2a7069999cd7e..a0933c64da0ffb715d28df82db9472624346a7e7 100644 (file)
@@ -156,7 +156,13 @@ export const copyCollectionPartialToExistingCollection = (fileSelection: Collect
                 dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
 
                 // Copy files
-                const updatedCollection = await services.collectionService.copyFiles(fileSelection.collection.portableDataHash, fileSelection.selectedPaths, {uuid: formData.destination.uuid}, formData.destination.path || '/', false);
+                const updatedCollection = await services.collectionService.copyFiles(
+                    fileSelection.collection.portableDataHash,
+                    fileSelection.selectedPaths,
+                    {uuid: formData.destination.uuid},
+                    formData.destination.subpath || '/',
+                    false
+                );
                 dispatch(updateResources([updatedCollection]));
 
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION }));
index 92e209811d91b465496f3dbf0ab169c7dbe09fef..56f7302db127873f3350b0c92ffb43a6aef3e279 100644 (file)
@@ -158,7 +158,7 @@ export const moveCollectionPartialToExistingCollection = (fileSelection: Collect
                     fileSelection.collection.portableDataHash,
                     fileSelection.selectedPaths,
                     {uuid: formData.destination.uuid},
-                    formData.destination.path || '/', false
+                    formData.destination.subpath || '/', false
                 );
                 dispatch(updateResources([updatedCollection]));
 
index 72d1cb65d969de803ffc93d162b69dfc250fb56b..18385e31ef14da66aec69302574793e00c96c22e 100644 (file)
@@ -23,6 +23,8 @@ import { mapTreeValues } from "models/tree";
 import { sortFilesTree } from "services/collection-service/collection-service-files-response";
 import { GroupClass, GroupResource } from "models/group";
 import { CollectionResource } from "models/collection";
+import { getResource } from "store/resources/resources";
+import { updateResources } from "store/resources/resources-actions";
 
 export const treePickerActions = unionize({
     LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
@@ -167,6 +169,7 @@ export const loadProject = (params: LoadProjectParamsWithId) =>
         const itemLimit = 200;
 
         const { items, itemsAvailable } = await services.groupsService.contents((loadShared || searchProjects) ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: itemLimit });
+        dispatch<any>(updateResources(items));
 
         if (itemsAvailable > itemLimit) {
             items.push({
@@ -523,29 +526,38 @@ const buildParams = (ownerUuid: string) => {
  *   if the item represents a valid target/destination location
  */
 export type FileOperationLocation = {
+    name: string;
     uuid: string;
-    path: string;
+    pdh?: string;
+    subpath: string;
 }
-export const getFileOperationLocation = (item: ProjectsTreePickerItem): FileOperationLocation | undefined => {
-    if ('kind' in item && item.kind === ResourceKind.COLLECTION) {
-        return {
-            uuid: item.uuid,
-            path: '/'
-        };
-    } else if ('type' in item && item.type === CollectionFileType.DIRECTORY) {
-        const uuid = getCollectionResourceCollectionUuid(item.id);
-        if (uuid) {
+export const getFileOperationLocation = (item: ProjectsTreePickerItem) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<FileOperationLocation | undefined> => {
+        if ('kind' in item && item.kind === ResourceKind.COLLECTION) {
             return {
-                uuid,
-                path: [item.path, item.name].join('/')
+                name: item.name,
+                uuid: item.uuid,
+                pdh: item.portableDataHash,
+                subpath: '/',
             };
-        } else {
-            return undefined;
+        } else if ('type' in item && item.type === CollectionFileType.DIRECTORY) {
+            const uuid = getCollectionResourceCollectionUuid(item.id);
+            if (uuid) {
+                const collection = getResource<CollectionResource>(uuid)(getState().resources);
+                if (collection) {
+                    const itemPath = [item.path, item.name].join('/');
+
+                    return {
+                        name: item.name,
+                        uuid,
+                        pdh: collection.portableDataHash,
+                        subpath: itemPath,
+                    };
+                }
+            }
         }
-    } else {
         return undefined;
-    }
-};
+    };
 
 /**
  * Create an expanded tree picker subtree from array of nested projects/collection
index 23a44965c0eac305a09da1c4545b44b772a883bc..7d5fcf8035ca976270140ea638b0788279b9c439 100644 (file)
@@ -65,7 +65,7 @@ export const DirectoryPickerField = (props: PickerIdProp) =>
     <Field
         name="destination"
         pickerId={props.pickerId}
-        component={DirectoryTreePickerField}
+        component={DirectoryTreePickerField as any}
         validate={validateDirectory} />;
 
 interface StorageClassesProps {
index 17417bf554f7bafcaf92c2fa1afbc03fcd3f6a2d..793eeaa3e60598261a6b9f63bf5cc2cf8e85b265 100644 (file)
@@ -9,7 +9,9 @@ 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 { getFileOperationLocation } from "store/tree-picker/tree-picker-actions";
+import { FileOperationLocation, getFileOperationLocation } from "store/tree-picker/tree-picker-actions";
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
 
 export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
     <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
@@ -44,24 +46,40 @@ export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdPro
         </div>
     </div>;
 
-const handleDirectoryChange = (props: WrappedFieldProps) =>
-    (_: any, { data }: TreeItem<ProjectsTreePickerItem>) => {
-        props.input.onChange(getFileOperationLocation(data) || '');
-    }
+type ProjectsTreePickerActionProps = {
+    getFileOperationLocation: (item: ProjectsTreePickerItem) => Promise<FileOperationLocation | undefined>;
+}
 
-export const DirectoryTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
-    <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
-        <div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
-            <ProjectsTreePicker
-                currentUuid={props.input.value.uuid}
-                pickerId={props.pickerId}
-                toggleItemActive={handleDirectoryChange(props)}
-                options={{ showOnlyOwned: false, showOnlyWritable: true }}
-                includeCollections
-                includeDirectories />
-            {props.meta.dirty && props.meta.error &&
-                <Typography variant='caption' color='error'>
-                    {props.meta.error}
-                </Typography>}
-        </div>
-    </div>;
+const projectsTreePickerMapDispatchToProps = (dispatch: Dispatch): ProjectsTreePickerActionProps => ({
+    getFileOperationLocation: (item: ProjectsTreePickerItem) => dispatch<any>(getFileOperationLocation(item)),
+});
+
+type ProjectsTreePickerCombinedProps = ProjectsTreePickerActionProps & WrappedFieldProps & PickerIdProp;
+
+export const DirectoryTreePickerField = connect(null, projectsTreePickerMapDispatchToProps)(
+    class DirectoryTreePickerFieldComponent extends React.Component<ProjectsTreePickerCombinedProps> {
+
+        handleDirectoryChange = (props: WrappedFieldProps) =>
+            async (_: any, { data }: TreeItem<ProjectsTreePickerItem>) => {
+                const location = await this.props.getFileOperationLocation(data);
+                props.input.onChange(location || '');
+            }
+
+        render() {
+            return <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
+                <div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
+                    <ProjectsTreePicker
+                        currentUuid={this.props.input.value.uuid}
+                        pickerId={this.props.pickerId}
+                        toggleItemActive={this.handleDirectoryChange(this.props)}
+                        options={{ showOnlyOwned: false, showOnlyWritable: true }}
+                        includeCollections
+                        includeDirectories />
+                    {this.props.meta.dirty && this.props.meta.error &&
+                        <Typography variant='caption' color='error'>
+                            {this.props.meta.error}
+                        </Typography>}
+                </div>
+            </div>;
+        }
+    });
index 5348cc2b76ca93a4e24fc1d9474702bc327867e2..bd9dc67eb810d0872bd4cdf5f5bcf2eb8ca152f3 100644 (file)
@@ -15,12 +15,11 @@ import {
 } from 'models/workflow';
 import { GenericInputProps, GenericInput } from './generic-input';
 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
-import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { FileOperationLocation, getFileOperationLocation, initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
 import { TreeItem } from 'components/tree/tree';
 import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
-import { CollectionResource } from 'models/collection';
-import { ResourceKind } from 'models/resource';
 import { ERROR_MESSAGE } from 'validators/require';
+import { Dispatch } from 'redux';
 
 export interface DirectoryInputProps {
     input: DirectoryCommandInputParameter;
@@ -43,9 +42,9 @@ export const DirectoryInput = ({ input, options }: DirectoryInputProps) =>
 
 const format = (value?: Directory) => value ? value.basename : '';
 
-const parse = (directory: CollectionResource): Directory => ({
+const parse = (directory: FileOperationLocation): Directory => ({
     class: CWLType.DIRECTORY,
-    location: `keep:${directory.portableDataHash}`,
+    location: `keep:${directory.pdh}${directory.subpath}`,
     basename: directory.name,
 });
 
@@ -59,11 +58,21 @@ const getValidation = memoize(
 
 interface DirectoryInputComponentState {
     open: boolean;
-    directory?: CollectionResource;
+    directory?: FileOperationLocation;
 }
 
-const DirectoryInputComponent = connect()(
-    class FileInputComponent extends React.Component<GenericInputProps & DispatchProp & {
+interface DirectoryInputActionProps {
+    initProjectsTreePicker: (pickerId: string) => void;
+    getFileOperationLocation: (item: ProjectsTreePickerItem) => Promise<FileOperationLocation | undefined>;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): DirectoryInputActionProps => ({
+    initProjectsTreePicker: (pickerId: string) => dispatch<any>(initProjectsTreePicker(pickerId)),
+    getFileOperationLocation: (item: ProjectsTreePickerItem) => dispatch<any>(getFileOperationLocation(item)),
+});
+
+const DirectoryInputComponent = connect(null, mapDispatchToProps)(
+    class FileInputComponent extends React.Component<GenericInputProps & DirectoryInputActionProps & DispatchProp & {
         options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
     }, DirectoryInputComponentState> {
         state: DirectoryInputComponentState = {
@@ -71,8 +80,7 @@ const DirectoryInputComponent = connect()(
         };
 
         componentDidMount() {
-            this.props.dispatch<any>(
-                initProjectsTreePicker(this.props.commandInput.id));
+            this.props.initProjectsTreePicker(this.props.commandInput.id);
         }
 
         render() {
@@ -95,12 +103,9 @@ const DirectoryInputComponent = connect()(
             this.props.input.onChange(this.state.directory);
         }
 
-        setDirectory = (_: {}, { data }: TreeItem<ProjectsTreePickerItem>) => {
-            if ('kind' in data && data.kind === ResourceKind.COLLECTION) {
-                this.setState({ directory: data });
-            } else {
-                this.setState({ directory: undefined });
-            }
+        setDirectory = async (_: {}, { data: item }: TreeItem<ProjectsTreePickerItem>) => {
+            const location = await this.props.getFileOperationLocation(item);
+            this.setState({ directory: location });
         }
 
         renderInput() {
@@ -143,6 +148,7 @@ const DirectoryInputComponent = connect()(
                             <ProjectsTreePicker
                                 pickerId={this.props.commandInput.id}
                                 includeCollections
+                                includeDirectories
                                 options={this.props.options}
                                 toggleItemActive={this.setDirectory} />
                         </div>