Merge branch '18169-cancel-button-not-working' into main
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 26 Nov 2021 08:05:56 +0000 (09:05 +0100)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 26 Nov 2021 08:06:36 +0000 (09:06 +0100)
closes #18169

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

14 files changed:
.licenseignore
cypress/fixtures/files/5mb.bin [new file with mode: 0644]
cypress/integration/collection.spec.js
cypress/support/commands.js
src/common/webdav.ts
src/components/collection-panel-files/collection-panel-files.tsx
src/components/file-upload/file-upload.tsx
src/components/form-dialog/form-dialog.tsx
src/services/collection-service/collection-service.ts
src/store/collections/collection-upload-actions.ts
src/store/file-uploader/file-uploader-actions.ts
src/store/file-uploader/file-uploader-reducer.ts
src/views-components/dialog-upload/dialog-collection-files-upload.tsx
src/views-components/file-uploader/file-uploader.tsx

index 853135fc885f09ad99bfec03cfc12ec0190560f6..9b943a1f3f4a78f73af2e25e0acffec5f4824354 100644 (file)
@@ -14,3 +14,4 @@ public/*
 .npmrc
 src/lib/cwl-svg/*
 tools/arvados_config.yml
+cypress/fixtures/files/5mb.bin
diff --git a/cypress/fixtures/files/5mb.bin b/cypress/fixtures/files/5mb.bin
new file mode 100644 (file)
index 0000000..d52f252
Binary files /dev/null and b/cypress/fixtures/files/5mb.bin differ
index 3e06d7e5a0b2bb03b3de36e2cecfe1241b4dbdd0..eb06a06c92549b7a06e684054610d6d0ca8d1ffa 100644 (file)
@@ -793,4 +793,83 @@ describe('Collection panel tests', function () {
                     .contains(adminUser.user.uuid);
             });
     });
+
+    describe('file upload', () => {
+        beforeEach(() => {
+            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"
+            })
+                .as('testCollection1');
+        });
+
+        it('allows to cancel running upload', () => {
+            cy.getAll('@testCollection1')
+                .then(function([testCollection1]) {
+                    cy.loginAs(activeUser);
+
+                    cy.goToPath(`/collections/${testCollection1.uuid}`);
+
+                    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_a.bin');
+                        cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+
+                        cy.get('[data-cy=form-submit-btn]').click();
+
+                        cy.get('button').contains('Cancel').click();
+
+                        cy.get('[data-cy=form-submit-btn]').should('not.exist');
+                    });
+                });
+        });
+
+        it('allows to cancel single file from the running upload', () => {
+            cy.getAll('@testCollection1')
+                .then(function([testCollection1]) {
+                    cy.loginAs(activeUser);
+
+                    cy.goToPath(`/collections/${testCollection1.uuid}`);
+
+                    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_a.bin');
+                        cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+
+                        cy.get('[data-cy=form-submit-btn]').click();
+
+                        cy.get('button[aria-label=Remove]').eq(1).click();
+
+                        cy.get('[data-cy=form-submit-btn]').should('not.exist');
+
+                        cy.get('[data-cy=collection-files-panel]').contains('5mb_a.bin').should('exist');
+                    });
+                });
+        });
+
+        it('allows to cancel all files from the running upload', () => {
+            cy.getAll('@testCollection1')
+                .then(function([testCollection1]) {
+                    cy.loginAs(activeUser);
+
+                    cy.goToPath(`/collections/${testCollection1.uuid}`);
+
+                    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_a.bin');
+                        cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+
+                        cy.get('[data-cy=form-submit-btn]').click();
+
+                        cy.get('button[aria-label=Remove]').click({ multiple: true });
+
+                        cy.get('[data-cy=form-submit-btn]').should('not.exist');
+                    });
+                });
+        });
+    });
 })
index 069ed96dcf3631afec10e55478f6f635f96b6d4b..07290e550aa3b6beceba61559a477216ef220435 100644 (file)
@@ -280,4 +280,42 @@ Cypress.Commands.add('createProject', ({
             cy.addToFavorites(user.token, user.user.uuid, project.uuid);
         }
     });
-});
\ No newline at end of file
+});
+
+Cypress.Commands.add(
+    'upload',
+    {
+        prevSubject: 'element',
+    },
+    (subject, file, fileName) => {
+        cy.window().then(window => {
+            const blob = b64toBlob(file, '', 512);
+            const testFile = new window.File([blob], fileName);
+
+            cy.wrap(subject).trigger('drop', {
+                dataTransfer: { files: [testFile] },
+            });
+        })
+    }
+)
+
+function b64toBlob(b64Data, contentType = '', sliceSize = 512) {
+    const byteCharacters = atob(b64Data)
+    const byteArrays = []
+
+    for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
+        const slice = byteCharacters.slice(offset, offset + sliceSize);
+
+        const byteNumbers = new Array(slice.length);
+        for (let i = 0; i < slice.length; i++) {
+            byteNumbers[i] = slice.charCodeAt(i);
+        }
+
+        const byteArray = new Uint8Array(byteNumbers);
+
+        byteArrays.push(byteArray);
+    }
+
+    const blob = new Blob(byteArrays, { type: contentType });
+    return blob
+}
\ No newline at end of file
index 758a5e18e1e9ad44015b9a802d0e532e70ba3d76..93ec21cb724f26d2ede4cf9be4b211defaf85b9c 100644 (file)
@@ -84,6 +84,15 @@ export class WebDAV {
                 .keys(headers)
                 .forEach(key => r.setRequestHeader(key, headers[key]));
 
+            if (!(window as any).cancelTokens) {
+                Object.assign(window, { cancelTokens: {} });
+            }
+
+            (window as any).cancelTokens[config.url] = () => { 
+                resolve(r);
+                r.abort();
+            }
+
             if (config.onUploadProgress) {
                 r.upload.addEventListener('progress', config.onUploadProgress);
             }
index 97cbc8ce6bc924a2def5e0b88f1261a2e8e8e331..a7001a61ac614e83f5dd6c8eb2e570a9902f55a6 100644 (file)
@@ -517,7 +517,12 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                         <Button
                             className={classes.uploadButton}
                             data-cy='upload-button'
-                            onClick={onUploadDataClick}
+                            onClick={() => {
+                                if (!collectionAutofetchEnabled) {
+                                    setCollectionAutofetchEnabled(true);
+                                }
+                                onUploadDataClick();
+                            }}
                             variant='contained'
                             color='primary'
                             size='small'>
index 617529cdc07da56bdf9579c9b63476c276dbfaff..54d5b5db2b9513a34579d1d5465a89587c3fb8eb 100644 (file)
@@ -123,6 +123,17 @@ export const FileUpload = withStyles(styles)(
             if (!disabled) {
                 onDelete(file);
             }
+
+            let interval = setInterval(() => {
+                const key = Object.keys((window as any).cancelTokens).find(key => key.indexOf(file.file.name) > -1);
+
+                if (key) {
+                    clearInterval(interval);
+                    (window as any).cancelTokens[key]();
+                    delete (window as any).cancelTokens[key];
+                }
+            }, 100);
+
         }
         render() {
             const { classes, onDrop, disabled, files } = this.props;
@@ -140,6 +151,7 @@ export const FileUpload = withStyles(styles)(
                             inputs[0].focus();
                         }
                     }}
+                    data-cy="drag-and-drop"
                     disabled={disabled}
                     inputProps={{
                         onFocus: () => {
index 19145cea34ae12b62813742e8c20acfb419b5f13..0fc799dee92b62eb0ab58a6ce100403dc7477193 100644 (file)
@@ -42,7 +42,9 @@ interface DialogProjectDataProps {
     dialogTitle: string;
     formFields: React.ComponentType<InjectedFormProps<any> & WithDialogProps<any>>;
     submitLabel?: string;
+    cancelCallback?: Function;
     enableWhenPristine?: boolean;
+    doNotDisableCancel?: boolean;
 }
 
 type DialogProjectProps = DialogProjectDataProps & WithDialogProps<{}> & InjectedFormProps<any> & WithStyles<CssRules>;
@@ -65,10 +67,18 @@ export const FormDialog = withStyles(styles)((props: DialogProjectProps) =>
             <DialogActions className={props.classes.dialogActions}>
                 <Button
                     data-cy='form-cancel-btn'
-                    onClick={props.closeDialog}
+                    onClick={() => {
+                        props.closeDialog();
+
+                        if (props.cancelCallback) {
+                            props.cancelCallback();
+                            props.reset();
+                            props.initialize({});
+                        }
+                    }}
                     className={props.classes.button}
                     color="primary"
-                    disabled={props.submitting}>
+                    disabled={props.doNotDisableCancel ? false : props.submitting}>
                     {props.cancelLabel || 'Cancel'}
                 </Button>
                 <Button
index 0c3cda3bcb8abcc2ae779cf83156cdd54d7e3373..48e797c500e8b7e3dade9fc15727773176805773 100644 (file)
@@ -108,7 +108,7 @@ export class CollectionService extends TrashableResourceService<CollectionResour
             },
             onUploadProgress: (e: ProgressEvent) => {
                 onProgress(fileId, e.loaded, e.total, Date.now());
-            }
+            },
         };
         return this.webdavClient.upload(fileURL, [file], requestConfig);
     }
index 8f85ea18a6c4b081238801d5394788f950c81c9e..0ca681b98ed2fcc9d8550165b477231f58fa0287 100644 (file)
@@ -14,12 +14,14 @@ import { progressIndicatorActions } from "store/progress-indicator/progress-indi
 import { collectionPanelFilesAction } from 'store/collection-panel/collection-panel-files/collection-panel-files-actions';
 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) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    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));
+        dispatch(WorkbenchActions.loadCollection(collectionUuid));
         dispatch(fileUploaderActions.CLEAR_UPLOAD());
     };
 
index 8436c485f9ba098efbe0fa880c44c8df7f0c4cb9..a397bbd825f23d7f936fa63470b2a091f27a2642 100644 (file)
@@ -24,6 +24,7 @@ export const fileUploaderActions = unionize({
     SET_UPLOAD_PROGRESS: ofType<{ fileId: number, loaded: number, total: number, currentTime: number }>(),
     START_UPLOAD: ofType(),
     DELETE_UPLOAD_FILE: ofType<UploadFile>(),
+    CANCEL_FILES_UPLOAD: ofType(),
 });
 
 export type FileUploaderAction = UnionOf<typeof fileUploaderActions>;
index c1f9c6810958467629ecdddb0d74fb00035f35fd..4218fbee61a9df1689fa3b69a36d40d328e2d2a2 100644 (file)
@@ -43,6 +43,21 @@ export const fileUploaderReducer = (state: UploaderState = initialState, action:
 
             return updatedState;
         },
+        CANCEL_FILES_UPLOAD: () => {
+            state.forEach((file) => {
+                let interval = setInterval(() => {
+                    const key = Object.keys((window as any).cancelTokens).find(key => key.indexOf(file.file.name) > -1);
+    
+                    if (key) {
+                        clearInterval(interval);
+                        (window as any).cancelTokens[key]();
+                        delete (window as any).cancelTokens[key];
+                    }
+                }, 100);
+            });
+
+            return [];
+        },
         START_UPLOAD: () => {
             const startTime = Date.now();
             return state.map(f => ({ ...f, startTime, prevTime: startTime }));
index 2f662bfae06ffa20982f8f23a409ef6bf8dc9ac5..f65bdabfeb935ef79a93609a5beed7f3988a66d6 100644 (file)
@@ -10,16 +10,30 @@ import { FormDialog } from 'components/form-dialog/form-dialog';
 import { require } from 'validators/require';
 import { FileUploaderField } from 'views-components/file-uploader/file-uploader';
 import { WarningCollection } from 'components/warning-collection/warning-collection';
+import { fileUploaderActions } from 'store/file-uploader/file-uploader-actions';
+import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
 
 type DialogCollectionFilesUploadProps = WithDialogProps<{}> & InjectedFormProps<CollectionCreateFormDialogData>;
 
-export const DialogCollectionFilesUpload = (props: DialogCollectionFilesUploadProps) =>
-    <FormDialog
+export const DialogCollectionFilesUpload = (props: DialogCollectionFilesUploadProps) => {
+
+    return <FormDialog
         dialogTitle='Upload data'
         formFields={UploadCollectionFilesFields}
         submitLabel='Upload data'
+        doNotDisableCancel
+        cancelCallback={() => {
+            const { submitting, dispatch } = (props as any);
+
+            if (submitting) {
+                dispatch(progressIndicatorActions.STOP_WORKING('uploadCollectionFilesDialog'));
+                dispatch(fileUploaderActions.CANCEL_FILES_UPLOAD());
+                dispatch(fileUploaderActions.CLEAR_UPLOAD());
+            }
+        }}
         {...props}
     />;
+}
 
 const UploadCollectionFilesFields = () => <>
     <Field
index 82e400f78a241a114f72999f3e5fae735a7c0fc9..cde286c450c4764be139b288c65ab3ab55f9d4e2 100644 (file)
@@ -30,7 +30,7 @@ const mapDispatchToProps = (dispatch: Dispatch, { onDrop }: FileUploaderProps):
             onDrop(files);
         }
     },
-    onDelete: file => dispatch(fileUploaderActions.DELETE_UPLOAD_FILE(file)),
+    onDelete: file => dispatch(fileUploaderActions.DELETE_UPLOAD_FILE(file))
 });
 
 export const FileUploader = connect(mapStateToProps, mapDispatchToProps)(FileUpload);
@@ -38,5 +38,5 @@ export const FileUploader = connect(mapStateToProps, mapDispatchToProps)(FileUpl
 export const FileUploaderField = (props: WrappedFieldProps & { label?: string }) =>
     <div>
         <Typography variant='caption'>{props.label}</Typography>
-        <FileUploader disabled={props.meta.submitting} onDrop={props.input.onChange} />
+        <FileUploader disabled={false} onDrop={props.input.onChange} />
     </div>;