From fb1ecf2421f8aac07d733d3bb56bb39312274f8c Mon Sep 17 00:00:00 2001 From: Michal Klobukowski Date: Tue, 28 Aug 2018 12:47:16 +0200 Subject: [PATCH] Extract file-uploader, resotore file upload to collection creator Feature #14119 Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski --- .../file-upload-dialog/file-upload-dialog.tsx | 4 +- src/components/file-upload/file-upload.tsx | 39 +++---- .../collections/collection-create-actions.ts | 12 +- ...ctions.ts => collection-upload-actions.ts} | 106 ++++++++---------- src/store/collections/collections-reducer.ts | 14 --- .../file-uploader/file-uploader-actions.ts | 25 +++++ .../file-uploader-reducer.ts} | 10 +- src/store/store.ts | 20 ++-- .../collection-panel-files.ts | 5 +- .../dialog-collection-create.tsx | 26 ++--- .../file-uploader/file-uploader.tsx | 35 ++++++ .../upload-collection-files-dialog.ts | 11 +- 12 files changed, 162 insertions(+), 145 deletions(-) rename src/store/collections/{uploader/collection-uploader-actions.ts => collection-upload-actions.ts} (57%) delete mode 100644 src/store/collections/collections-reducer.ts create mode 100644 src/store/file-uploader/file-uploader-actions.ts rename src/store/{collections/uploader/collection-uploader-reducer.ts => file-uploader/file-uploader-reducer.ts} (69%) create mode 100644 src/views-components/file-uploader/file-uploader.tsx diff --git a/src/components/file-upload-dialog/file-upload-dialog.tsx b/src/components/file-upload-dialog/file-upload-dialog.tsx index 7810c491..7215b6d1 100644 --- a/src/components/file-upload-dialog/file-upload-dialog.tsx +++ b/src/components/file-upload-dialog/file-upload-dialog.tsx @@ -4,10 +4,10 @@ import * as React from 'react'; import { FileUpload } from "~/components/file-upload/file-upload"; -import { UploadFile } from '~/store/collections/uploader/collection-uploader-actions'; import { Dialog, DialogTitle, DialogContent, DialogActions } from '@material-ui/core/'; import { Button, CircularProgress } from '@material-ui/core'; -import { WithDialogProps } from '../../store/dialog/with-dialog'; +import { WithDialogProps } from '~/store/dialog/with-dialog'; +import { UploadFile } from '~/store/file-uploader/file-uploader-actions'; export interface FilesUploadDialogProps { files: UploadFile[]; diff --git a/src/components/file-upload/file-upload.tsx b/src/components/file-upload/file-upload.tsx index 74efe009..e7f402cd 100644 --- a/src/components/file-upload/file-upload.tsx +++ b/src/components/file-upload/file-upload.tsx @@ -14,7 +14,7 @@ import { withStyles } from '@material-ui/core'; import Dropzone from 'react-dropzone'; import { CloudUploadIcon } from "../icon/icon"; import { formatFileSize, formatProgress, formatUploadSpeed } from "~/common/formatters"; -import { UploadFile } from "~/store/collections/uploader/collection-uploader-actions"; +import { UploadFile } from '~/store/file-uploader/file-uploader-actions'; type CssRules = "root" | "dropzone" | "container" | "uploadIcon"; @@ -36,7 +36,7 @@ const styles: StyleRulesCallback = theme => ({ } }); -interface FileUploadProps { +export interface FileUploadProps { files: UploadFile[]; disabled: boolean; onDrop: (files: File[]) => void; @@ -44,21 +44,17 @@ interface FileUploadProps { export const FileUpload = withStyles(styles)( ({ classes, files, disabled, onDrop }: FileUploadProps & WithStyles) => - - - Upload data - onDrop(files)} disabled={disabled}> {files.length === 0 && - - - - Drag and drop data or click to browse + + + + Drag and drop data or click to browse - - } + + } {files.length > 0 && - +
File name @@ -68,17 +64,16 @@ export const FileUpload = withStyles(styles)( - {files.map(f => - - {f.file.name} - {formatFileSize(f.file.size)} - {formatUploadSpeed(f.prevLoaded, f.loaded, f.prevTime, f.currentTime)} - {formatProgress(f.loaded, f.total)} - - )} + {files.map(f => + + {f.file.name} + {formatFileSize(f.file.size)} + {formatUploadSpeed(f.prevLoaded, f.loaded, f.prevTime, f.currentTime)} + {formatProgress(f.loaded, f.total)} + + )}
}
-
); diff --git a/src/store/collections/collection-create-actions.ts b/src/store/collections/collection-create-actions.ts index 1981af0d..5a1246a7 100644 --- a/src/store/collections/collection-create-actions.ts +++ b/src/store/collections/collection-create-actions.ts @@ -5,17 +5,16 @@ import { Dispatch } from "redux"; import { reset, startSubmit, stopSubmit, initialize } from 'redux-form'; import { RootState } from '~/store/store'; -import { uploadCollectionFiles } from '~/store/collections/uploader/collection-uploader-actions'; import { dialogActions } from "~/store/dialog/dialog-actions"; -import { CollectionResource } from '~/models/collection'; import { ServiceRepository } from '~/services/services'; import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service"; +import { uploadCollectionFiles } from './collection-upload-actions'; +import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions'; export interface CollectionCreateFormDialogData { ownerUuid: string; name: string; description: string; - files: File[]; } export const COLLECTION_CREATE_FORM_NAME = "collectionCreateFormName"; @@ -23,14 +22,15 @@ export const COLLECTION_CREATE_FORM_NAME = "collectionCreateFormName"; export const openCollectionCreateDialog = (ownerUuid: string) => (dispatch: Dispatch) => { dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { ownerUuid })); + dispatch(fileUploaderActions.CLEAR_UPLOAD()); dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_CREATE_FORM_NAME, data: { ownerUuid } })); }; -export const createCollection = (collection: Partial) => +export const createCollection = (data: CollectionCreateFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(startSubmit(COLLECTION_CREATE_FORM_NAME)); try { - const newCollection = await services.collectionService.create(collection); + const newCollection = await services.collectionService.create(data); await dispatch(uploadCollectionFiles(newCollection.uuid)); dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME })); dispatch(reset(COLLECTION_CREATE_FORM_NAME)); @@ -42,4 +42,4 @@ export const createCollection = (collection: Partial) => } return ; } - }; \ No newline at end of file + }; diff --git a/src/store/collections/uploader/collection-uploader-actions.ts b/src/store/collections/collection-upload-actions.ts similarity index 57% rename from src/store/collections/uploader/collection-uploader-actions.ts rename to src/store/collections/collection-upload-actions.ts index 58dcdc4c..c04ef1a2 100644 --- a/src/store/collections/uploader/collection-uploader-actions.ts +++ b/src/store/collections/collection-upload-actions.ts @@ -1,63 +1,45 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import { unionize, ofType, UnionOf } from "~/common/unionize"; -import { Dispatch } from 'redux'; -import { RootState } from '~/store/store'; -import { ServiceRepository } from '~/services/services'; -import { dialogActions } from '~/store/dialog/dialog-actions'; -import { loadCollectionFiles } from '../../collection-panel/collection-panel-files/collection-panel-files-actions'; -import { snackbarActions } from "~/store/snackbar/snackbar-actions"; - -export interface UploadFile { - id: number; - file: File; - prevLoaded: number; - loaded: number; - total: number; - startTime: number; - prevTime: number; - currentTime: number; -} - -export const collectionUploaderActions = unionize({ - SET_UPLOAD_FILES: ofType(), - START_UPLOAD: ofType(), - SET_UPLOAD_PROGRESS: ofType<{ fileId: number, loaded: number, total: number, currentTime: number }>(), - CLEAR_UPLOAD: ofType() -}); - -export type CollectionUploaderAction = UnionOf; - -export const uploadCollectionFiles = (collectionUuid: string) => - async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - dispatch(collectionUploaderActions.START_UPLOAD()); - const files = getState().collections.uploader.map(file => file.file); - await services.collectionService.uploadFiles(collectionUuid, files, handleUploadProgress(dispatch)); - dispatch(collectionUploaderActions.CLEAR_UPLOAD()); - }; - - -export const uploadCurrentCollectionFiles = () => - async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const currentCollection = getState().collectionPanel.item; - if (currentCollection) { - await dispatch(uploadCollectionFiles(currentCollection.uuid)); - dispatch(loadCollectionFiles(currentCollection.uuid)); - dispatch(closeUploadCollectionFilesDialog()); - dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Data has been uploaded.', hideDuration: 2000 })); - } - }; - -export const UPLOAD_COLLECTION_FILES_DIALOG = 'uploadCollectionFilesDialog'; -export const openUploadCollectionFilesDialog = () => (dispatch: Dispatch) => { - dispatch(collectionUploaderActions.CLEAR_UPLOAD()); - dispatch(dialogActions.OPEN_DIALOG({ id: UPLOAD_COLLECTION_FILES_DIALOG, data: {} })); -}; - -export const closeUploadCollectionFilesDialog = () => dialogActions.CLOSE_DIALOG({ id: UPLOAD_COLLECTION_FILES_DIALOG }); - -const handleUploadProgress = (dispatch: Dispatch) => (fileId: number, loaded: number, total: number, currentTime: number) => { - dispatch(collectionUploaderActions.SET_UPLOAD_PROGRESS({ fileId, loaded, total, currentTime })); +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Dispatch } from 'redux'; +import { RootState } from '~/store/store'; +import { ServiceRepository } from '~/services/services'; +import { dialogActions } from '~/store/dialog/dialog-actions'; +import { loadCollectionFiles } from '../collection-panel/collection-panel-files/collection-panel-files-actions'; +import { snackbarActions } from '~/store/snackbar/snackbar-actions'; +import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions'; +import { reset } from 'redux-form'; + +export const uploadCollectionFiles = (collectionUuid: string) => + async (dispatch: Dispatch, 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(fileUploaderActions.CLEAR_UPLOAD()); + }; + +export const UPLOAD_COLLECTION_FILES_DIALOG = 'uploadCollectionFilesDialog'; + +export const openUploadCollectionFilesDialog = () => (dispatch: Dispatch) => { + dispatch(reset(UPLOAD_COLLECTION_FILES_DIALOG)); + dispatch(fileUploaderActions.CLEAR_UPLOAD()); + dispatch(dialogActions.OPEN_DIALOG({ id: UPLOAD_COLLECTION_FILES_DIALOG, data: {} })); +}; + +export const uploadCurrentCollectionFiles = () => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const currentCollection = getState().collectionPanel.item; + if (currentCollection) { + await dispatch(uploadCollectionFiles(currentCollection.uuid)); + dispatch(loadCollectionFiles(currentCollection.uuid)); + dispatch(closeUploadCollectionFilesDialog()); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Data has been uploaded.', hideDuration: 2000 })); + } + }; + +export const closeUploadCollectionFilesDialog = () => dialogActions.CLOSE_DIALOG({ id: UPLOAD_COLLECTION_FILES_DIALOG }); + +const handleUploadProgress = (dispatch: Dispatch) => (fileId: number, loaded: number, total: number, currentTime: number) => { + dispatch(fileUploaderActions.SET_UPLOAD_PROGRESS({ fileId, loaded, total, currentTime })); }; \ No newline at end of file diff --git a/src/store/collections/collections-reducer.ts b/src/store/collections/collections-reducer.ts deleted file mode 100644 index c7601505..00000000 --- a/src/store/collections/collections-reducer.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import { combineReducers } from 'redux'; -import { collectionUploaderReducer, CollectionUploaderState } from "./uploader/collection-uploader-reducer"; - -export type CollectionsState = { - uploader: CollectionUploaderState -}; - -export const collectionsReducer = combineReducers({ - uploader: collectionUploaderReducer -}); diff --git a/src/store/file-uploader/file-uploader-actions.ts b/src/store/file-uploader/file-uploader-actions.ts new file mode 100644 index 00000000..906263fe --- /dev/null +++ b/src/store/file-uploader/file-uploader-actions.ts @@ -0,0 +1,25 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { unionize, ofType, UnionOf } from "~/common/unionize"; + +export interface UploadFile { + id: number; + file: File; + prevLoaded: number; + loaded: number; + total: number; + startTime: number; + prevTime: number; + currentTime: number; +} + +export const fileUploaderActions = unionize({ + CLEAR_UPLOAD: ofType(), + SET_UPLOAD_FILES: ofType(), + SET_UPLOAD_PROGRESS: ofType<{ fileId: number, loaded: number, total: number, currentTime: number }>(), + START_UPLOAD: ofType(), +}); + +export type FileUploaderAction = UnionOf; diff --git a/src/store/collections/uploader/collection-uploader-reducer.ts b/src/store/file-uploader/file-uploader-reducer.ts similarity index 69% rename from src/store/collections/uploader/collection-uploader-reducer.ts rename to src/store/file-uploader/file-uploader-reducer.ts index 79a8a623..625306f0 100644 --- a/src/store/collections/uploader/collection-uploader-reducer.ts +++ b/src/store/file-uploader/file-uploader-reducer.ts @@ -2,14 +2,14 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { CollectionUploaderAction, collectionUploaderActions, UploadFile } from "./collection-uploader-actions"; +import { UploadFile, fileUploaderActions, FileUploaderAction } from "./file-uploader-actions"; -export type CollectionUploaderState = UploadFile[]; +export type UploaderState = UploadFile[]; -const initialState: CollectionUploaderState = []; +const initialState: UploaderState = []; -export const collectionUploaderReducer = (state: CollectionUploaderState = initialState, action: CollectionUploaderAction) => { - return collectionUploaderActions.match(action, { +export const fileUploaderReducer = (state: UploaderState = initialState, action: FileUploaderAction) => { + return fileUploaderActions.match(action, { SET_UPLOAD_FILES: files => files.map((f, idx) => ({ id: idx, file: f, diff --git a/src/store/store.ts b/src/store/store.ts index abfe187c..4fe0f97a 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -22,12 +22,12 @@ import { ProjectPanelMiddlewareService } from "./project-panel/project-panel-mid import { FavoritePanelMiddlewareService } from "./favorite-panel/favorite-panel-middleware-service"; import { collectionPanelReducer } from './collection-panel/collection-panel-reducer'; import { dialogReducer } from './dialog/dialog-reducer'; -import { collectionsReducer } from './collections/collections-reducer'; import { ServiceRepository } from "~/services/services"; import { treePickerReducer } from './tree-picker/tree-picker-reducer'; import { resourcesReducer } from '~/store/resources/resources-reducer'; import { propertiesReducer } from './properties/properties-reducer'; import { RootState } from './store'; +import { fileUploaderReducer } from './file-uploader/file-uploader-reducer'; const composeEnhancers = (process.env.NODE_ENV === 'development' && @@ -60,18 +60,18 @@ export function configureStore(history: History, services: ServiceRepository): R const createRootReducer = (services: ServiceRepository) => combineReducers({ auth: authReducer(services), - collections: collectionsReducer, - router: routerReducer, - dataExplorer: dataExplorerReducer, collectionPanel: collectionPanelReducer, - detailsPanel: detailsPanelReducer, + collectionPanelFiles: collectionPanelFilesReducer, contextMenu: contextMenuReducer, - form: formReducer, + dataExplorer: dataExplorerReducer, + detailsPanel: detailsPanelReducer, + dialog: dialogReducer, favorites: favoritesReducer, + form: formReducer, + properties: propertiesReducer, + resources: resourcesReducer, + router: routerReducer, snackbar: snackbarReducer, - collectionPanelFiles: collectionPanelFilesReducer, - dialog: dialogReducer, treePicker: treePickerReducer, - resources: resourcesReducer, - properties: propertiesReducer, + fileUploader: fileUploaderReducer, }); diff --git a/src/views-components/collection-panel-files/collection-panel-files.ts b/src/views-components/collection-panel-files/collection-panel-files.ts index 10469957..ccb18c8f 100644 --- a/src/views-components/collection-panel-files/collection-panel-files.ts +++ b/src/views-components/collection-panel-files/collection-panel-files.ts @@ -10,12 +10,11 @@ import { CollectionPanelFilesState, CollectionPanelDirectory, CollectionPanelFil import { FileTreeData } from "~/components/file-tree/file-tree-data"; import { Dispatch } from "redux"; import { collectionPanelFilesAction } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions"; -import { contextMenuActions } from "~/store/context-menu/context-menu-actions"; import { ContextMenuKind } from "../context-menu/context-menu"; import { Tree, getNodeChildrenIds, getNode } from "~/models/tree"; import { CollectionFileType } from "~/models/collection-file"; -import { openUploadCollectionFilesDialog } from '~/store/collections/uploader/collection-uploader-actions'; -import { openContextMenu } from '../../store/context-menu/context-menu-actions'; +import { openContextMenu } from '~/store/context-menu/context-menu-actions'; +import { openUploadCollectionFilesDialog } from '~/store/collections/collection-upload-actions'; const memoizedMapStateToProps = () => { let prevState: CollectionPanelFilesState; diff --git a/src/views-components/dialog-create/dialog-collection-create.tsx b/src/views-components/dialog-create/dialog-collection-create.tsx index 2fb007b8..38a8114d 100644 --- a/src/views-components/dialog-create/dialog-collection-create.tsx +++ b/src/views-components/dialog-create/dialog-collection-create.tsx @@ -3,22 +3,14 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { InjectedFormProps } from 'redux-form'; +import { InjectedFormProps, Field } from 'redux-form'; import { WithDialogProps } from '~/store/dialog/with-dialog'; import { CollectionCreateFormDialogData } from '~/store/collections/collection-create-actions'; -import { collectionUploaderActions, UploadFile } from "~/store/collections/uploader/collection-uploader-actions"; import { FormDialog } from '~/components/form-dialog/form-dialog'; import { CollectionNameField, CollectionDescriptionField } from '~/views-components/form-fields/collection-form-fields'; -import { FileUpload } from '~/components/file-upload/file-upload'; +import { require } from '~/validators/require'; +import { FileUploaderField } from '../file-uploader/file-uploader'; -// interface DialogCollectionDataProps { -// open: boolean; -// handleSubmit: any; -// submitting: boolean; -// invalid: boolean; -// pristine: boolean; -// files: UploadFile[]; -// } type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps; @@ -33,8 +25,10 @@ export const DialogCollectionCreate = (props: DialogCollectionProps) => const CollectionAddFields = () => - {/* this.props.dispatch(collectionUploaderActions.SET_UPLOAD_FILES(files))} /> */} -; \ No newline at end of file + +; + diff --git a/src/views-components/file-uploader/file-uploader.tsx b/src/views-components/file-uploader/file-uploader.tsx new file mode 100644 index 00000000..d9111f47 --- /dev/null +++ b/src/views-components/file-uploader/file-uploader.tsx @@ -0,0 +1,35 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { FileUpload } from '~/components/file-upload/file-upload'; +import { connect } from 'react-redux'; +import { RootState } from '~/store/store'; +import { FileUploadProps } from '../../components/file-upload/file-upload'; +import { Dispatch } from 'redux'; +import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions'; +import { WrappedFieldProps } from 'redux-form'; +import { Typography } from '@material-ui/core'; + +export type FileUploaderProps = Pick; + +const mapStateToProps = (state: RootState, { disabled }: FileUploaderProps): Pick => ({ + disabled, + files: state.fileUploader, +}); + +const mapDispatchToProps = (dispatch: Dispatch, { onDrop }: FileUploaderProps): Pick => ({ + onDrop: files => { + dispatch(fileUploaderActions.SET_UPLOAD_FILES(files)); + onDrop(files); + }, +}); + +export const FileUploader = connect(mapStateToProps, mapDispatchToProps)(FileUpload); + +export const FileUploaderField = (props: WrappedFieldProps & { label?: string }) => +
+ {props.label} + +
; diff --git a/src/views-components/upload-collection-files-dialog/upload-collection-files-dialog.ts b/src/views-components/upload-collection-files-dialog/upload-collection-files-dialog.ts index 1f3a50eb..52a2e1db 100644 --- a/src/views-components/upload-collection-files-dialog/upload-collection-files-dialog.ts +++ b/src/views-components/upload-collection-files-dialog/upload-collection-files-dialog.ts @@ -6,12 +6,13 @@ import { connect } from "react-redux"; import { Dispatch, compose } from "redux"; import { withDialog } from '~/store/dialog/with-dialog'; import { FilesUploadDialog } from '~/components/file-upload-dialog/file-upload-dialog'; -import { RootState } from '../../store/store'; -import { uploadCurrentCollectionFiles, UPLOAD_COLLECTION_FILES_DIALOG, collectionUploaderActions } from '~/store/collections/uploader/collection-uploader-actions'; +import { RootState } from '~/store/store'; +import { UPLOAD_COLLECTION_FILES_DIALOG, uploadCurrentCollectionFiles } from '~/store/collections/collection-upload-actions'; +import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions'; const mapStateToProps = (state: RootState) => ({ - files: state.collections.uploader, - uploading: state.collections.uploader.some(file => file.loaded < file.total) + files: state.fileUploader, + uploading: state.fileUploader.some(file => file.loaded < file.total) }); const mapDispatchToProps = (dispatch: Dispatch) => ({ @@ -19,7 +20,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ dispatch(uploadCurrentCollectionFiles()); }, onChange: (files: File[]) => { - dispatch(collectionUploaderActions.SET_UPLOAD_FILES(files)); + dispatch(fileUploaderActions.SET_UPLOAD_FILES(files)); } }); -- 2.30.2