20031: Add includeDirectories flag to ProjectsTreePicker and add collection/directory...
authorStephen Smith <stephen@curii.com>
Mon, 17 Apr 2023 19:39:53 +0000 (15:39 -0400)
committerStephen Smith <stephen@curii.com>
Mon, 17 Apr 2023 19:39:53 +0000 (15:39 -0400)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

17 files changed:
src/models/collection-file.ts
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/dialog-copy/dialog-collection-partial-copy-to-existing-collection.tsx
src/views-components/dialog-move/dialog-collection-partial-move-to-existing-collection.tsx
src/views-components/form-fields/collection-form-fields.tsx
src/views-components/projects-tree-picker/favorites-tree-picker.tsx
src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx
src/views-components/projects-tree-picker/home-tree-picker.tsx
src/views-components/projects-tree-picker/projects-tree-picker.tsx
src/views-components/projects-tree-picker/public-favorites-tree-picker.tsx
src/views-components/projects-tree-picker/search-projects-picker.tsx
src/views-components/projects-tree-picker/shared-tree-picker.tsx
src/views-components/projects-tree-picker/tree-picker-field.tsx
src/views/run-process-panel/inputs/file-array-input.tsx
src/views/run-process-panel/inputs/file-input.tsx

index 91008d1fdaf15d33f824ae40c2a694dbb38f5efd..3688557a6154c8e8a3fc9dca87d486d004a6b754 100644 (file)
@@ -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
+);
index 359b6b83808f82fb1d4c06ce062a72f5808e1050..2a3fd5de8b9e9ff1c5fba2f1716f1facd80c03ca 100644 (file)
@@ -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 }));
index de64d99a09fe3f39a0db72e5007b971ba2f61d9f..6d3c45f3bfa451fc2f259700d609b8859f762d7d 100644 (file)
@@ -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 }));
index 460a23e3d778c0d3670e5d360ef06bdb00a0f408..b8003aa1c29ed045c81eb621ba62d8826bd271fc 100644 (file)
@@ -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
             }),
index 1af9dfeecf84faddc0b5c3496987b3a8b6bc695d..f6d4db21b20c820b7007d26514d57e5884ee409e 100644 (file)
@@ -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 }}/>
             </>);
index e808103761b6d3bda29f770362e6ce1a9197807e..f95bd24fb31ddbc7214b2d57df95e2ca3e71309b 100644 (file)
@@ -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 }}/>
             </>);
index 7e18111a672f2397b51fd241493d22677aeb6f15..0faa59b5223cf92355f7faa2ea5f3fb3a74682c0 100644 (file)
@@ -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} />);
index 6ab2b42d177032d766c3f5cf396a48e514e89d6c..7e63152be9bd831127e0af1d040b2cb49851b6cf 100644 (file)
@@ -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);
index 11b51caa884e5b52df00f396f87f4be16d21fa3b..1ed2a5511def60b3cd00cdce63fbfce9c5070ba9 100644 (file)
@@ -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 }));
index 3133c5dbb951ed9632630dc5a1546aee89c62bfb..3f71a58e40a89b94306903862baa22a9230588df 100644 (file)
@@ -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);
index 9ac0b64fc0e580fcdd4dc64e5db82d5319a3510c..1f03682906754c18a411398c20dc90cba6d8a599 100644 (file)
@@ -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,
index 91551c9abf5cfce0f2604b485f958d0a184633e7..ca03f72836ce6d4e88a0ea74a0459bd397f74792 100644 (file)
@@ -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);
index 7bad8ef7e99bc0689ea780b14bbb4c37fbea4ca3..2888050b088cb4ed7bfdf5ce361b25cf67690faf 100644 (file)
@@ -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);
index c15df6ba0c29e9831b8f215c4bf154b1e6896521..1914cd9d3ea16e616f453cb83bf8c965c9cb1739 100644 (file)
@@ -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);
index 2afa606e363cba8a4adaaf2b118c581af2981719..1414d18fd740e62510e6a46d57adb4e67c5875d8 100644 (file)
@@ -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>;
index a2f884e3e636b9c404369fc5b02508887cdbf6de..1e1a42998f50d11e3cc0151a241f90f443b069e7 100644 (file)
@@ -254,6 +254,7 @@ const FileArrayInputComponent = connect(mapStateToProps)(
                                 <ProjectsTreePicker
                                     pickerId={this.props.commandInput.id}
                                     includeCollections
+                                    includeDirectories
                                     includeFiles
                                     showSelection
                                     options={this.props.options}
index b0206e1452e8d2845815665640af50b6a8470c5a..5f48f83784bf1d9196baf36835bf20027f8cbfde 100644 (file)
@@ -142,6 +142,7 @@ const FileInputComponent = connect()(
                             <ProjectsTreePicker
                                 pickerId={this.props.commandInput.id}
                                 includeCollections
+                                includeDirectories
                                 includeFiles
                                 options={this.props.options}
                                 toggleItemActive={this.setFile} />