Merge branch '18834-uploading-a-file-into-a-subdirectory-of-a-collection-does-not...
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Wed, 6 Apr 2022 20:15:08 +0000 (22:15 +0200)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Wed, 6 Apr 2022 20:15:08 +0000 (22:15 +0200)
closes #18834

Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

cypress/integration/collection.spec.js
src/components/collection-panel-files/collection-panel-files.tsx
src/services/collection-service/collection-service.test.ts
src/services/collection-service/collection-service.ts
src/store/collections/collection-upload-actions.ts
src/views-components/collection-panel-files/collection-panel-files.ts
src/views-components/dialog-forms/files-upload-collection-dialog.ts

index b451fd66e865c5e2478f1453cf10bcdc7c9965fc..74acd056ffdd21e45f8f8cafb543e7447542982e 100644 (file)
@@ -947,7 +947,7 @@ describe('Collection panel tests', function () {
             cy.createCollection(adminUser.token, {
                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
                 owner_uuid: activeUser.user.uuid,
-                manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+                manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
             }).as('testCollection1');
         });
 
@@ -959,17 +959,26 @@ describe('Collection panel tests', function () {
                     cy.get('[data-cy=upload-button]').click();
                     cy.get('[data-cy=collection-files-panel]')
                         .contains('5mb_a.bin').should('not.exist');
-                    cy.get('[data-cy=collection-file-count]').should('contain', '1');
+                    cy.get('[data-cy=collection-file-count]').should('contain', '2');
                     cy.fixture('files/5mb.bin', 'base64').then(content => {
                         cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
                         cy.get('[data-cy=form-submit-btn]').click();
                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
+                        cy.get('[data-cy=collection-files-panel]')
+                            .contains('5mb_a.bin').should('exist');
+                        cy.get('[data-cy=collection-file-count]').should('contain', '3');
+
+                        cy.get('[data-cy=collection-files-panel]').contains('subdir').click();
+                        cy.get('[data-cy=upload-button]').click();
+                        cy.fixture('files/5mb.bin', 'base64').then(content => {
+                            cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+                            cy.get('[data-cy=form-submit-btn]').click();
+                            cy.get('[data-cy=form-submit-btn]').should('not.exist');
+                            cy.get('[data-cy=collection-files-right-panel]')
+                                 .contains('5mb_b.bin').should('exist');
+                            
+                        });
                     });
-                    // Confirm that the file browser has been updated.
-                    cy.get('[data-cy=collection-files-panel]')
-                        .contains('5mb_a.bin').should('exist');
-                    // Confirm that the collection panel has been updated.
-                    cy.get('[data-cy=collection-file-count]').should('contain', '2');
                 });
         });
 
index 1f7e8603c5ec25bb9afa8a1a68b8565b7c7a909d..05b493636da7aad97474cc3d61b3fafdfe97e0f3 100644 (file)
@@ -28,7 +28,7 @@ export interface CollectionPanelFilesProps {
     isWritable: boolean;
     isLoading: boolean;
     tooManyFiles: boolean;
-    onUploadDataClick: () => void;
+    onUploadDataClick: (targetLocation?: string) => void;
     onSearchChange: (searchValue: string) => void;
     onItemMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>, isWritable: boolean) => void;
     onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>, isWritable: boolean) => void;
@@ -446,7 +446,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                 </Tooltip>
             </div>
             <div className={classes.wrapper}>
-                <div className={classNames(classes.leftPanel, path.length > 1 ? classes.leftPanelVisible : classes.leftPanelHidden)}>
+                <div className={classNames(classes.leftPanel, path.length > 1 ? classes.leftPanelVisible : classes.leftPanelHidden)}  data-cy="collection-files-left-panel">
                     <Tooltip title="Go back" className={path.length > 1 ? classes.backButton : classes.backButtonHidden}>
                         <IconButton onClick={() => setPath([...path.slice(0, path.length -1)])}>
                             <BackIcon />
@@ -498,7 +498,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
 
                     </div>
                 </div>
-                <div className={classes.rightPanel}>
+                <div className={classes.rightPanel} data-cy="collection-files-right-panel">
                     <div className={classes.searchWrapper}>
                         <SearchInput selfClearProp={rightKey} label="Search" value={rightSearch} onSearch={setRightSearch} />
                     </div>
@@ -508,7 +508,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                             className={classes.uploadButton}
                             data-cy='upload-button'
                             onClick={() => {
-                                onUploadDataClick();
+                                onUploadDataClick(rightKey === leftKey ? undefined : rightKey);
                             }}
                             variant='contained'
                             color='primary'
index 610683694f626c7bd9157fabec99240f9f922835..817fdd5453bac324948d1d8d8170c8bec2d7168f 100644 (file)
@@ -22,6 +22,7 @@ describe('collection-service', () => {
         axiosMock = new MockAdapter(serverApi);
         webdavClient = {
             delete: jest.fn(),
+            upload: jest.fn(),
         } as any;
         authService = {} as AuthService;
         actions = {
@@ -82,6 +83,47 @@ describe('collection-service', () => {
         });
     });
 
+    describe('uploadFiles', () => {
+        it('should skip if no files to upload files', async () => {
+            // given
+            const files: File[] = [];
+            const collectionUUID = '';
+
+            // when
+            await collectionService.uploadFiles(collectionUUID, files);
+
+            // then
+            expect(webdavClient.upload).not.toHaveBeenCalled();
+        });
+
+        it('should upload files', async () => {
+            // given
+            const files: File[] = [{name: 'test-file1'} as File];
+            const collectionUUID = 'zzzzz-4zz18-0123456789abcde';
+
+            // when
+            await collectionService.uploadFiles(collectionUUID, files);
+
+            // then
+            expect(webdavClient.upload).toHaveBeenCalledTimes(1);
+            expect(webdavClient.upload.mock.calls[0][0]).toEqual("c=zzzzz-4zz18-0123456789abcde/test-file1");
+        });
+
+        it.only('should upload files with custom uplaod target', async () => {
+            // given
+            const files: File[] = [{name: 'test-file1'} as File];
+            const collectionUUID = 'zzzzz-4zz18-0123456789abcde';
+            const customTarget = 'zzzzz-4zz18-0123456789adddd/test-path/'
+
+            // when
+            await collectionService.uploadFiles(collectionUUID, files, undefined, customTarget);
+
+            // then
+            expect(webdavClient.upload).toHaveBeenCalledTimes(1);
+            expect(webdavClient.upload.mock.calls[0][0]).toEqual("c=zzzzz-4zz18-0123456789adddd/test-path/test-file1");
+        });
+    });
+
     describe('deleteFiles', () => {
         it('should remove no files', async () => {
             // given
index 857828c255ed2d5df521a39b078cb5ae973bc129..92e4dfbae3e6d43f28f73b03dfce992da3f3fcfc 100644 (file)
@@ -75,11 +75,11 @@ export class CollectionService extends TrashableResourceService<CollectionResour
         await this.update(collectionUuid, { preserveVersion: true });
     }
 
-    async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress) {
+    async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress, targetLocation: string = '') {
         if (collectionUuid === "" || files.length === 0) { return; }
         // files have to be uploaded sequentially
         for (let idx = 0; idx < files.length; idx++) {
-            await this.uploadFile(collectionUuid, files[idx], idx, onProgress);
+            await this.uploadFile(collectionUuid, files[idx], idx, onProgress, targetLocation);
         }
         await this.update(collectionUuid, { preserveVersion: true });
     }
@@ -107,8 +107,8 @@ export class CollectionService extends TrashableResourceService<CollectionResour
         };
     }
 
-    private async uploadFile(collectionUuid: string, file: File, fileId: number, onProgress: UploadProgress = () => { return; }) {
-        const fileURL = `c=${collectionUuid}/${file.name}`;
+    private async uploadFile(collectionUuid: string, file: File, fileId: number, onProgress: UploadProgress = () => { return; }, targetLocation: string = '') {
+        const fileURL = `c=${targetLocation !== '' ? targetLocation : collectionUuid}/${file.name}`.replace('//', '/');
         const requestConfig = {
             headers: {
                 'Content-Type': 'text/octet-stream'
index 0ca681b98ed2fcc9d8550165b477231f58fa0287..135538b074dbee437cebf55b5b9daf4ab4d1ecc8 100644 (file)
@@ -16,31 +16,31 @@ import { createTree } from 'models/tree';
 import { loadCollectionPanel } from '../collection-panel/collection-panel-action';
 import * as WorkbenchActions from 'store/workbench/workbench-actions';
 
-export const uploadCollectionFiles = (collectionUuid: string) =>
+export const uploadCollectionFiles = (collectionUuid: string, targetLocation?: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(fileUploaderActions.START_UPLOAD());
         const files = getState().fileUploader.map(file => file.file);
-        await services.collectionService.uploadFiles(collectionUuid, files, handleUploadProgress(dispatch));
+        await services.collectionService.uploadFiles(collectionUuid, files, handleUploadProgress(dispatch), targetLocation);
         dispatch(WorkbenchActions.loadCollection(collectionUuid));
         dispatch(fileUploaderActions.CLEAR_UPLOAD());
     };
 
 export const COLLECTION_UPLOAD_FILES_DIALOG = 'uploadCollectionFilesDialog';
 
-export const openUploadCollectionFilesDialog = () => (dispatch: Dispatch) => {
+export const openUploadCollectionFilesDialog = (targetLocation?: string) => (dispatch: Dispatch) => {
     dispatch(reset(COLLECTION_UPLOAD_FILES_DIALOG));
     dispatch(fileUploaderActions.CLEAR_UPLOAD());
-    dispatch<any>(dialogActions.OPEN_DIALOG({ id: COLLECTION_UPLOAD_FILES_DIALOG, data: {} }));
+    dispatch<any>(dialogActions.OPEN_DIALOG({ id: COLLECTION_UPLOAD_FILES_DIALOG, data: { targetLocation } }));
 };
 
-export const submitCollectionFiles = () =>
+export const submitCollectionFiles = (targetLocation?: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const currentCollection = getState().collectionPanel.item;
         if (currentCollection) {
             try {
                 dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
                 dispatch(startSubmit(COLLECTION_UPLOAD_FILES_DIALOG));
-                await dispatch<any>(uploadCollectionFiles(currentCollection.uuid))
+                await dispatch<any>(uploadCollectionFiles(currentCollection.uuid, targetLocation))
                     .then(() => dispatch<any>(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() })));
                 dispatch<any>(loadCollectionFiles(currentCollection.uuid));
                 dispatch<any>(loadCollectionPanel(currentCollection.uuid));
index 6e57ab47142fa5744165b8f9e6b906e1b3b382b4..216ec66967be4c3ab7085dfff7970fd18c309549 100644 (file)
@@ -45,8 +45,8 @@ const memoizedMapStateToProps = () => {
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onSearchChange' | 'onFileClick' | 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
-    onUploadDataClick: () => {
-        dispatch<any>(openUploadCollectionFilesDialog());
+    onUploadDataClick: (targetLocation?: string) => {
+        dispatch<any>(openUploadCollectionFilesDialog(targetLocation));
     },
     onCollapseToggle: (id) => {
         dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_COLLAPSE({ id }));
index 2299777517e2f5e521740045318652dcb9b25a82..81dbb0e7432cb9e1fa8b2aad3d677989963137ea 100644 (file)
@@ -13,8 +13,9 @@ export const FilesUploadCollectionDialog = compose(
     withDialog(COLLECTION_UPLOAD_FILES_DIALOG),
     reduxForm<CollectionCreateFormDialogData>({
         form: COLLECTION_UPLOAD_FILES_DIALOG,
-        onSubmit: (data, dispatch) => {
-            dispatch(submitCollectionFiles());
+        onSubmit: (data, dispatch, dialog: any) => {
+            const targetLocation = (dialog.data || {}).targetLocation;
+            dispatch(submitCollectionFiles(targetLocation));
         }
     })
 )(DialogCollectionFilesUpload);