UNPKG

box-ui-elements-mlh

Version:
1,280 lines (1,126 loc) 41.2 kB
/** * @flow * @file Content Uploader component * @author Box */ import 'regenerator-runtime/runtime'; import React, { Component } from 'react'; import classNames from 'classnames'; import getProp from 'lodash/get'; import noop from 'lodash/noop'; import uniqueid from 'lodash/uniqueId'; import cloneDeep from 'lodash/cloneDeep'; import { getTypedFileId, getTypedFolderId } from '../../utils/file'; import Browser from '../../utils/Browser'; import makeResponsive from '../common/makeResponsive'; import Internationalize from '../common/Internationalize'; import FolderUpload from '../../api/uploads/FolderUpload'; import API from '../../api'; import { getDataTransferItemId, getFileId, getFileFromDataTransferItem, getPackageFileFromDataTransferItem, getFile, getFileAPIOptions, getDataTransferItemAPIOptions, isDataTransferItemAFolder, isDataTransferItemAPackage, isMultiputSupported, } from '../../utils/uploads'; import DroppableContent from './DroppableContent'; import UploadsManager from './UploadsManager'; import Footer from './Footer'; import { DEFAULT_ROOT, CLIENT_NAME_CONTENT_UPLOADER, DEFAULT_HOSTNAME_UPLOAD, DEFAULT_HOSTNAME_API, VIEW_ERROR, VIEW_UPLOAD_EMPTY, VIEW_UPLOAD_IN_PROGRESS, VIEW_UPLOAD_SUCCESS, STATUS_PENDING, STATUS_IN_PROGRESS, STATUS_STAGED, STATUS_COMPLETE, STATUS_ERROR, ERROR_CODE_UPLOAD_FILE_LIMIT, } from '../../constants'; import type { UploadItem, UploadDataTransferItemWithAPIOptions, UploadFileWithAPIOptions, UploadFile, UploadItemAPIOptions, UploadStatus, } from '../../common/types/upload'; import type { StringMap, Token, View, BoxItem } from '../../common/types/core'; import '../common/fonts.scss'; import '../common/base.scss'; type Props = { apiHost: string, chunked: boolean, className: string, clientName: string, dataTransferItems: Array<DataTransferItem | UploadDataTransferItemWithAPIOptions>, fileLimit: number, files?: Array<UploadFileWithAPIOptions | File>, isDraggingItemsToUploadsManager?: boolean, isFolderUploadEnabled: boolean, isLarge: boolean, isResumableUploadsEnabled: boolean, isSmall: boolean, isTouch: boolean, isUploadFallbackLogicEnabled: boolean, language?: string, measureRef: Function, messages?: StringMap, onBeforeUpload: (file: Array<UploadFileWithAPIOptions | File>) => void, onCancel: Function, onClickCancel: UploadItem => void, onClickResume: UploadItem => void, onClickRetry: UploadItem => void, onClose: Function, onComplete: Function, onError: Function, onMinimize?: Function, onProgress: Function, onResume: Function, onUpload: Function, overwrite: boolean, requestInterceptor?: Function, responseInterceptor?: Function, rootFolderId: string, sharedLink?: string, sharedLinkPassword?: string, token?: Token, uploadHost: string, useUploadsManager?: boolean, }; type State = { errorCode?: string, isUploadsManagerExpanded: boolean, itemIds: Object, items: UploadItem[], view: View, }; const CHUNKED_UPLOAD_MIN_SIZE_BYTES = 104857600; // 100MB const FILE_LIMIT_DEFAULT = 100; // Upload at most 100 files at once by default const HIDE_UPLOAD_MANAGER_DELAY_MS_DEFAULT = 8000; const EXPAND_UPLOADS_MANAGER_ITEMS_NUM_THRESHOLD = 5; const UPLOAD_CONCURRENCY = 6; class ContentUploader extends Component<Props, State> { id: string; state: State; props: Props; rootElement: HTMLElement; appElement: HTMLElement; resetItemsTimeout: TimeoutID; isAutoExpanded: boolean = false; static defaultProps = { rootFolderId: DEFAULT_ROOT, apiHost: DEFAULT_HOSTNAME_API, chunked: true, className: '', clientName: CLIENT_NAME_CONTENT_UPLOADER, fileLimit: FILE_LIMIT_DEFAULT, uploadHost: DEFAULT_HOSTNAME_UPLOAD, onBeforeUpload: noop, onClickCancel: noop, onClickResume: noop, onClickRetry: noop, onClose: noop, onComplete: noop, onError: noop, onResume: noop, onUpload: noop, onProgress: noop, overwrite: true, useUploadsManager: false, files: [], onMinimize: noop, onCancel: noop, isFolderUploadEnabled: false, isResumableUploadsEnabled: false, isUploadFallbackLogicEnabled: false, dataTransferItems: [], isDraggingItemsToUploadsManager: false, }; /** * [constructor] * * @return {ContentUploader} */ constructor(props: Props) { super(props); const { rootFolderId, token, useUploadsManager } = props; this.state = { view: (rootFolderId && token) || useUploadsManager ? VIEW_UPLOAD_EMPTY : VIEW_ERROR, items: [], errorCode: '', itemIds: {}, isUploadsManagerExpanded: false, }; this.id = uniqueid('bcu_'); } /** * Fetches the root folder on load * * @private * @inheritdoc * @return {void} */ componentDidMount() { this.rootElement = ((document.getElementById(this.id): any): HTMLElement); this.appElement = this.rootElement; } /** * Cancels pending uploads * * @private * @inheritdoc * @return {void} */ componentWillUnmount() { this.cancel(); } /** * Adds new items to the queue when files prop gets updated in window view * * @return {void} */ componentDidUpdate(): void { const { files, dataTransferItems, useUploadsManager } = this.props; const hasFiles = Array.isArray(files) && files.length > 0; const hasItems = Array.isArray(dataTransferItems) && dataTransferItems.length > 0; const hasUploads = hasFiles || hasItems; if (!useUploadsManager || !hasUploads) { return; } // TODO: this gets called unnecessarily (upon each render regardless of the queue not changing) this.addFilesWithOptionsToUploadQueueAndStartUpload(files, dataTransferItems); } /** * Create and return new instance of API creator * * @param {UploadItemAPIOptions} [uploadAPIOptions] * @return {API} */ createAPIFactory(uploadAPIOptions?: UploadItemAPIOptions): API { const { rootFolderId } = this.props; const folderId = getProp(uploadAPIOptions, 'folderId') || rootFolderId; const fileId = getProp(uploadAPIOptions, 'fileId'); const itemFolderId = getTypedFolderId(folderId); const itemFileId = fileId ? getTypedFileId(fileId) : null; return new API({ ...this.getBaseAPIOptions(), id: itemFileId || itemFolderId, ...uploadAPIOptions, }); } /** * Return base API options from props * * @private * @returns {Object} */ getBaseAPIOptions = (): Object => { const { token, sharedLink, sharedLinkPassword, apiHost, uploadHost, clientName, requestInterceptor, responseInterceptor, } = this.props; return { token, sharedLink, sharedLinkPassword, apiHost, uploadHost, clientName, requestInterceptor, responseInterceptor, }; }; /** * Given an array of files, return the files that are new to the Content Uploader * * @param {Array<UploadFileWithAPIOptions | File>} files */ getNewFiles = (files: Array<UploadFileWithAPIOptions | File>): Array<UploadFileWithAPIOptions | File> => { const { rootFolderId } = this.props; const { itemIds } = this.state; return Array.from(files).filter(file => !itemIds[getFileId(file, rootFolderId)]); }; /** * Given an array of files, return the files that are new to the Content Uploader * * @param {Array<UploadFileWithAPIOptions | File>} files */ getNewDataTransferItems = ( items: Array<DataTransferItem | UploadDataTransferItemWithAPIOptions>, ): Array<DataTransferItem | UploadDataTransferItemWithAPIOptions> => { const { rootFolderId } = this.props; const { itemIds } = this.state; return Array.from(items).filter(item => !itemIds[getDataTransferItemId(item, rootFolderId)]); }; /** * Converts File API to upload items and adds to upload queue. * * @private * @param {Array<UploadFileWithAPIOptions | UploadFile>} files - Files to be added to upload queue * @param {Function} itemUpdateCallback - function to be invoked after items status are updated * @param {boolean} [isRelativePathIgnored] - if true webkitRelativePath property is ignored * @return {void} */ addFilesToUploadQueue = ( files?: Array<UploadFileWithAPIOptions | UploadFile>, itemUpdateCallback: Function, isRelativePathIgnored?: boolean = false, ) => { const { onBeforeUpload, rootFolderId } = this.props; if (!files || files.length === 0) { return; } const newFiles = this.getNewFiles(files); if (newFiles.length === 0) { return; } const newItemIds = {}; newFiles.forEach(file => { newItemIds[getFileId(file, rootFolderId)] = true; }); clearTimeout(this.resetItemsTimeout); const firstFile = getFile(newFiles[0]); this.setState( state => ({ itemIds: { ...state.itemIds, ...newItemIds, }, }), () => { onBeforeUpload(newFiles); if (firstFile.webkitRelativePath && !isRelativePathIgnored) { // webkitRelativePath should be ignored when the upload destination folder is known this.addFilesWithRelativePathToQueue(newFiles, itemUpdateCallback); } else { this.addFilesWithoutRelativePathToQueue(newFiles, itemUpdateCallback); } }, ); }; /** * Add dropped items to the upload queue * * @private * @param {DataTransfer} droppedItems * @param {Function} itemUpdateCallback * @returns {Promise<any>} */ addDroppedItemsToUploadQueue = (droppedItems: DataTransfer, itemUpdateCallback: Function): void => { if (droppedItems.items) { this.addDataTransferItemsToUploadQueue(droppedItems.items, itemUpdateCallback); } else { Array.from(droppedItems.files).forEach(file => { this.addFilesToUploadQueue([file], itemUpdateCallback); }); } }; /** * Add dataTransferItems to the upload queue * * @private * @param {DataTransferItemList} dataTransferItems * @param {Function} itemUpdateCallback * @returns {Promise<any>} */ addDataTransferItemsToUploadQueue = ( dataTransferItems: DataTransferItemList | Array<DataTransferItem | UploadDataTransferItemWithAPIOptions>, itemUpdateCallback: Function, ): void => { const { isFolderUploadEnabled } = this.props; if (!dataTransferItems || dataTransferItems.length === 0) { return; } const folderItems = []; const fileItems = []; const packageItems = []; Array.from(dataTransferItems).forEach(item => { const isDirectory = isDataTransferItemAFolder(item); if (Browser.isSafari() && isDataTransferItemAPackage(item)) { packageItems.push(item); } else if (isDirectory && isFolderUploadEnabled) { folderItems.push(item); } else if (!isDirectory) { fileItems.push(item); } }); this.addFileDataTransferItemsToUploadQueue(fileItems, itemUpdateCallback); this.addPackageDataTransferItemsToUploadQueue(packageItems, itemUpdateCallback); this.addFolderDataTransferItemsToUploadQueue(folderItems, itemUpdateCallback); }; /** * Add dataTransferItem of file type to the upload queue * * @private * @param {Array<DataTransferItem | UploadDataTransferItemWithAPIOptions>} dataTransferItems * @param {Function} itemUpdateCallback * @returns {void} */ addFileDataTransferItemsToUploadQueue = ( dataTransferItems: Array<DataTransferItem | UploadDataTransferItemWithAPIOptions>, itemUpdateCallback: Function, ): void => { dataTransferItems.forEach(async item => { const file = await getFileFromDataTransferItem(item); if (!file) { return; } this.addFilesToUploadQueue([file], itemUpdateCallback); }); }; /** * Add dataTransferItem of package type to the upload queue * * @private * @param {Array<DataTransferItem | UploadDataTransferItemWithAPIOptions>} dataTransferItems * @param {Function} itemUpdateCallback * @returns {void} */ addPackageDataTransferItemsToUploadQueue = ( dataTransferItems: Array<DataTransferItem | UploadDataTransferItemWithAPIOptions>, itemUpdateCallback: Function, ): void => { dataTransferItems.forEach(item => { const file = getPackageFileFromDataTransferItem(item); if (!file) { return; } this.addFilesToUploadQueue([file], itemUpdateCallback); }); }; /** * Add dataTransferItem of folder type to the upload queue * * @private * @param {Array<DataTransferItem | UploadDataTransferItemWithAPIOptions>} dataTransferItems * @param {Function} itemUpdateCallback * @returns {Promise<any>} */ addFolderDataTransferItemsToUploadQueue = async ( dataTransferItems: Array<DataTransferItem | UploadDataTransferItemWithAPIOptions>, itemUpdateCallback: Function, ): Promise<any> => { const { rootFolderId } = this.props; const { itemIds } = this.state; if (dataTransferItems.length === 0) { return; } const newItems = this.getNewDataTransferItems(dataTransferItems); newItems.forEach(item => { itemIds[getDataTransferItemId(item, rootFolderId)] = true; }); if (newItems.length === 0) { return; } const fileAPIOptions: Object = getDataTransferItemAPIOptions(newItems[0]); const { folderId = rootFolderId } = fileAPIOptions; newItems.forEach(async item => { const folderUpload = this.getFolderUploadAPI(folderId); await folderUpload.buildFolderTreeFromDataTransferItem(item); this.addFolderToUploadQueue(folderUpload, itemUpdateCallback, fileAPIOptions); }); }; /** * Converts File API to upload items and adds to upload queue for files with webkitRelativePath. * * @private * @param {Array<UploadFileWithAPIOptions | File>} files - Files to be added to upload queue * @param {Function} itemUpdateCallback - function to be invoked after items status are updated * @return {void} */ addFilesWithRelativePathToQueue(files: Array<UploadFileWithAPIOptions | File>, itemUpdateCallback: Function) { if (files.length === 0) { return; } const { rootFolderId } = this.props; const fileAPIOptions: Object = getFileAPIOptions(files[0]); const { folderId = rootFolderId } = fileAPIOptions; const folderUpload = this.getFolderUploadAPI(folderId); // Only 1 folder tree can be built with files having webkitRelativePath properties folderUpload.buildFolderTreeFromWebkitRelativePath(files); this.addFolderToUploadQueue(folderUpload, itemUpdateCallback, fileAPIOptions); } /** * Get folder upload API instance * * @private * @param {string} folderId * @return {FolderUpload} */ getFolderUploadAPI = (folderId: string): FolderUpload => { const uploadBaseAPIOptions = this.getBaseAPIOptions(); return new FolderUpload(this.addFilesToUploadQueue, folderId, this.addToQueue, uploadBaseAPIOptions); }; /** * Add folder to upload queue * * @private * @param {FolderUpload} folderUpload * @param {Function} itemUpdateCallback * @param {Object} apiOptions * @return {void} */ addFolderToUploadQueue = (folderUpload: FolderUpload, itemUpdateCallback: Function, apiOptions: Object): void => { this.addToQueue( [ // $FlowFixMe no file property { api: folderUpload, extension: '', isFolder: true, name: folderUpload.folder.name, options: apiOptions, progress: 0, size: 1, status: STATUS_PENDING, }, ], itemUpdateCallback, ); }; /** * Converts File API to upload items and adds to upload queue for files with webkitRelativePath missing or ignored. * * @private * @param {Array<UploadFileWithAPIOptions | File>} files - Files to be added to upload queue * @param {Function} itemUpdateCallback - function to be invoked after items status are updated * @return {void} */ addFilesWithoutRelativePathToQueue = ( files: Array<UploadFileWithAPIOptions | File>, itemUpdateCallback: Function, ) => { const { itemIds } = this.state; const { rootFolderId } = this.props; // Convert files from the file API to upload items const newItems = files.map(file => { const uploadFile = getFile(file); const uploadAPIOptions = getFileAPIOptions(file); const { name, size } = uploadFile; // Extract extension or use empty string if file has no extension let extension = name.substr(name.lastIndexOf('.') + 1); if (extension.length === name.length) { extension = ''; } const api = this.getUploadAPI(uploadFile, uploadAPIOptions); const uploadItem: Object = { api, extension, file: uploadFile, name, progress: 0, size, status: STATUS_PENDING, }; if (uploadAPIOptions) { uploadItem.options = uploadAPIOptions; } itemIds[getFileId(uploadItem, rootFolderId)] = true; return uploadItem; }); if (newItems.length === 0) { return; } this.setState({ itemIds, }); this.addToQueue(newItems, itemUpdateCallback); }; /** * Add new items to the upload queue * * @private * @param {Array<UploadFileWithAPIOptions | File>} newItems - Files to be added to upload queue * @param {Function} itemUpdateCallback - function to be invoked after items status are updated * @return {void} */ addToQueue = (newItems: UploadItem[], itemUpdateCallback: Function) => { const { fileLimit, useUploadsManager } = this.props; const { items, isUploadsManagerExpanded } = this.state; let updatedItems = []; const prevItemsNum = items.length; const totalNumOfItems = prevItemsNum + newItems.length; // Don't add more than fileLimit # of items if (totalNumOfItems > fileLimit) { updatedItems = items.concat(newItems.slice(0, fileLimit - items.length)); this.setState({ errorCode: ERROR_CODE_UPLOAD_FILE_LIMIT, }); } else { updatedItems = items.concat(newItems); this.setState({ errorCode: '' }); // If the number of items being uploaded passes the threshold, expand the upload manager if ( prevItemsNum < EXPAND_UPLOADS_MANAGER_ITEMS_NUM_THRESHOLD && totalNumOfItems >= EXPAND_UPLOADS_MANAGER_ITEMS_NUM_THRESHOLD && useUploadsManager && !isUploadsManagerExpanded ) { this.isAutoExpanded = true; this.expandUploadsManager(); } } this.updateViewAndCollection(updatedItems, () => { if (itemUpdateCallback) { itemUpdateCallback(); } const { view } = this.state; // Automatically start upload if other files are being uploaded if (view === VIEW_UPLOAD_IN_PROGRESS) { this.upload(); } }); }; /** * Returns a new API instance for the given file. * * @private * @param {File} file - File to get a new API instance for * @param {UploadItemAPIOptions} [uploadAPIOptions] * @return {UploadAPI} - Instance of Upload API */ getUploadAPI(file: File, uploadAPIOptions?: UploadItemAPIOptions) { const { chunked, isResumableUploadsEnabled, isUploadFallbackLogicEnabled } = this.props; const { size } = file; const factory = this.createAPIFactory(uploadAPIOptions); if (chunked && size > CHUNKED_UPLOAD_MIN_SIZE_BYTES) { if (isMultiputSupported()) { const chunkedUploadAPI = factory.getChunkedUploadAPI(); if (isResumableUploadsEnabled) { chunkedUploadAPI.isResumableUploadsEnabled = true; } if (isUploadFallbackLogicEnabled) { chunkedUploadAPI.isUploadFallbackLogicEnabled = true; } return chunkedUploadAPI; } /* eslint-disable no-console */ console.warn( 'Chunked uploading is enabled, but not supported by your browser. You may need to enable HTTPS.', ); /* eslint-enable no-console */ } const plainUploadAPI = factory.getPlainUploadAPI(); if (isUploadFallbackLogicEnabled) { plainUploadAPI.isUploadFallbackLogicEnabled = true; } return plainUploadAPI; } /** * Removes an item from the upload queue. Cancels upload if in progress. * * @param {UploadItem} item - Item to remove * @return {void} */ removeFileFromUploadQueue = (item: UploadItem) => { const { onCancel, useUploadsManager } = this.props; const { items } = this.state; // Clear any error errorCode in footer this.setState({ errorCode: '' }); const { api } = item; api.cancel(); items.splice(items.indexOf(item), 1); onCancel([item]); this.updateViewAndCollection(items, () => { // Minimize uploads manager if there are no more items if (useUploadsManager && !items.length) { this.minimizeUploadsManager(); } const { view } = this.state; if (view === VIEW_UPLOAD_IN_PROGRESS) { this.upload(); } }); }; /** * Aborts uploads in progress and clears upload list. * * @private * @return {void} */ cancel = () => { const { items } = this.state; items.forEach(uploadItem => { const { api, status } = uploadItem; if (status === STATUS_IN_PROGRESS) { api.cancel(); } }); // Reset upload collection this.updateViewAndCollection([]); }; /** * Uploads all items in the upload collection. * * @private * @return {void} */ upload = () => { const { items } = this.state; items.forEach(uploadItem => { if (uploadItem.status === STATUS_PENDING) { this.uploadFile(uploadItem); } }); }; /** * Helper to upload a single file. * * @param {UploadItem} item - Upload item object * @return {void} */ uploadFile(item: UploadItem) { const { overwrite, rootFolderId } = this.props; const { api, file, options } = item; const { items } = this.state; const numItemsUploading = items.filter(item_t => item_t.status === STATUS_IN_PROGRESS).length; if (numItemsUploading >= UPLOAD_CONCURRENCY) { return; } const uploadOptions: Object = { file, folderId: options && options.folderId ? options.folderId : rootFolderId, errorCallback: error => this.handleUploadError(item, error), progressCallback: event => this.handleUploadProgress(item, event), successCallback: entries => this.handleUploadSuccess(item, entries), overwrite, fileId: options && options.fileId ? options.fileId : null, }; item.status = STATUS_IN_PROGRESS; items[items.indexOf(item)] = item; api.upload(uploadOptions); this.updateViewAndCollection(items); } /** * Helper to resume uploading a single file. * * @param {UploadItem} item - Upload item object * @return {void} */ resumeFile(item: UploadItem) { const { overwrite, rootFolderId, onResume } = this.props; const { api, file, options } = item; const { items } = this.state; const numItemsUploading = items.filter(item_t => item_t.status === STATUS_IN_PROGRESS).length; if (numItemsUploading >= UPLOAD_CONCURRENCY) { return; } const resumeOptions: Object = { file, folderId: options && options.folderId ? options.folderId : rootFolderId, errorCallback: error => this.handleUploadError(item, error), progressCallback: event => this.handleUploadProgress(item, event), successCallback: entries => this.handleUploadSuccess(item, entries), overwrite, sessionId: api && api.sessionId ? api.sessionId : null, fileId: options && options.fileId ? options.fileId : null, }; item.status = STATUS_IN_PROGRESS; delete item.error; items[items.indexOf(item)] = item; onResume(item); api.resume(resumeOptions); this.updateViewAndCollection(items); } /** * Helper to reset a file. Cancels any current upload and resets progress. * * @param {UploadItem} item - Upload item to reset * @return {void} */ resetFile(item: UploadItem) { const { api, file, options } = item; if (api && typeof api.cancel === 'function') { api.cancel(); } // Reset API, progress & status item.api = this.getUploadAPI(file, options); item.progress = 0; item.status = STATUS_PENDING; delete item.error; const { items } = this.state; items[items.indexOf(item)] = item; this.updateViewAndCollection(items); } /** * Handles a successful upload. * * @private * @param {UploadItem} item - Upload item corresponding to success event * @param {BoxItem[]} entries - Successfully uploaded Box File objects * @return {void} */ handleUploadSuccess = (item: UploadItem, entries?: BoxItem[]) => { const { onUpload, useUploadsManager } = this.props; item.progress = 100; if (!item.error) { item.status = STATUS_COMPLETE; } // Cache Box File object of successfully uploaded item if (entries && entries.length === 1) { const [boxFile] = entries; item.boxFile = boxFile; } const { items } = this.state; items[items.indexOf(item)] = item; // Broadcast that a file has been uploaded if (useUploadsManager) { onUpload(item); this.checkClearUploadItems(); } else { onUpload(item.boxFile); } this.updateViewAndCollection(items, () => { const { view } = this.state; if (view === VIEW_UPLOAD_IN_PROGRESS) { this.upload(); } }); }; resetUploadManagerExpandState = () => { this.isAutoExpanded = false; this.setState({ isUploadsManagerExpanded: false, }); }; /** * Updates view and internal upload collection with provided items. * * @private * @param {UploadItem[]} item - Items to update collection with * @param {Function} callback * @return {void} */ updateViewAndCollection(items: UploadItem[], callback?: Function) { const { onComplete, useUploadsManager, isResumableUploadsEnabled }: Props = this.props; const someUploadIsInProgress = items.some(uploadItem => uploadItem.status !== STATUS_COMPLETE); const someUploadHasFailed = items.some(uploadItem => uploadItem.status === STATUS_ERROR); const allItemsArePending = !items.some(uploadItem => uploadItem.status !== STATUS_PENDING); const noFileIsPendingOrInProgress = items.every( uploadItem => uploadItem.status !== STATUS_PENDING && uploadItem.status !== STATUS_IN_PROGRESS, ); const areAllItemsFinished = items.every( uploadItem => uploadItem.status === STATUS_COMPLETE || uploadItem.status === STATUS_ERROR, ); const uploadItemsStatus = isResumableUploadsEnabled ? areAllItemsFinished : noFileIsPendingOrInProgress; let view = ''; if ((items && items.length === 0) || allItemsArePending) { view = VIEW_UPLOAD_EMPTY; } else if (someUploadHasFailed && useUploadsManager) { view = VIEW_ERROR; } else if (someUploadIsInProgress) { view = VIEW_UPLOAD_IN_PROGRESS; } else { view = VIEW_UPLOAD_SUCCESS; if (!useUploadsManager) { onComplete(cloneDeep(items.map(item => item.boxFile))); // Reset item collection after successful upload items = []; } } if (uploadItemsStatus && useUploadsManager) { if (this.isAutoExpanded) { this.resetUploadManagerExpandState(); } // Else manually expanded so don't close onComplete(items); } const state: Object = { items, view, }; if (items.length === 0) { state.itemIds = {}; state.errorCode = ''; } this.setState(state, callback); } /** * Handles an upload error. * * @private * @param {UploadItem} item - Upload item corresponding to error * @param {Error} error - Upload error * @return {void} */ handleUploadError = (item: UploadItem, error: Error) => { const { onError, useUploadsManager } = this.props; const { file } = item; const { items } = this.state; item.status = STATUS_ERROR; item.error = error; const newItems = [...items]; const index = newItems.findIndex(singleItem => singleItem === item); if (index !== -1) { newItems[index] = item; } // Broadcast that there was an error uploading a file const errorData = useUploadsManager ? { item, error, } : { file, error, }; onError(errorData); this.updateViewAndCollection(newItems, () => { if (useUploadsManager) { this.isAutoExpanded = true; this.expandUploadsManager(); } const { view } = this.state; if (view === VIEW_UPLOAD_IN_PROGRESS) { this.upload(); } }); }; /** * Handles an upload progress event. * * @private * @param {UploadItem} item - Upload item corresponding to progress event * @param {ProgressEvent} event - Progress event * @return {void} */ handleUploadProgress = (item: UploadItem, event: any) => { if (!event.total || item.status === STATUS_COMPLETE || item.status === STATUS_STAGED) { return; } item.progress = Math.min(Math.round((event.loaded / event.total) * 100), 100); item.status = item.progress === 100 ? STATUS_STAGED : STATUS_IN_PROGRESS; const { onProgress } = this.props; onProgress(item); const { items } = this.state; items[items.indexOf(item)] = item; this.updateViewAndCollection(items); }; /** * Updates item based on its status. * * @private * @param {UploadItem} item - The upload item to update * @return {void} */ onClick = (item: UploadItem) => { const { chunked, isResumableUploadsEnabled, onClickCancel, onClickResume, onClickRetry } = this.props; const { status, file } = item; const isChunkedUpload = chunked && !item.isFolder && file.size > CHUNKED_UPLOAD_MIN_SIZE_BYTES && isMultiputSupported(); const isResumable = isResumableUploadsEnabled && isChunkedUpload && item.api.sessionId; switch (status) { case STATUS_IN_PROGRESS: case STATUS_STAGED: case STATUS_COMPLETE: case STATUS_PENDING: this.removeFileFromUploadQueue(item); onClickCancel(item); break; case STATUS_ERROR: if (isResumable) { item.bytesUploadedOnLastResume = item.api.totalUploadedBytes; this.resumeFile(item); onClickResume(item); } else { this.resetFile(item); this.uploadFile(item); onClickRetry(item); } break; default: break; } }; /** * Click action button for all uploads in the Uploads Manager with the given status. * * @private * @param {UploadStatus} - the status that items should have for their action button to be clicked * @return {void} */ clickAllWithStatus = (status?: UploadStatus) => { const { items } = this.state; items.forEach(item => { if (!status || item.status === status) { this.onClick(item); } }); }; /** * Expands the upload manager * * @return {void} */ expandUploadsManager = (): void => { const { useUploadsManager } = this.props; if (!useUploadsManager) { return; } clearTimeout(this.resetItemsTimeout); this.setState({ isUploadsManagerExpanded: true }); }; /** * Minimizes the upload manager * * @return {void} */ minimizeUploadsManager = (): void => { const { useUploadsManager, onMinimize } = this.props; if (!useUploadsManager || !onMinimize) { return; } clearTimeout(this.resetItemsTimeout); onMinimize(); this.resetUploadManagerExpandState(); this.checkClearUploadItems(); }; /** * Checks if the upload items should be cleared after a timeout * * @return {void} */ checkClearUploadItems = () => { this.resetItemsTimeout = setTimeout( this.resetUploadsManagerItemsWhenUploadsComplete, HIDE_UPLOAD_MANAGER_DELAY_MS_DEFAULT, ); }; /** * Toggles the upload manager * * @return {void} */ toggleUploadsManager = (): void => { const { isUploadsManagerExpanded } = this.state; if (isUploadsManagerExpanded) { this.minimizeUploadsManager(); } else { this.expandUploadsManager(); } }; /** * Empties the items queue * * @return {void} */ resetUploadsManagerItemsWhenUploadsComplete = (): void => { const { view, items, isUploadsManagerExpanded } = this.state; const { useUploadsManager, onCancel } = this.props; // Do not reset items when upload manger is expanded or there're uploads in progress if ((isUploadsManagerExpanded && useUploadsManager && !!items.length) || view === VIEW_UPLOAD_IN_PROGRESS) { return; } onCancel(items); this.setState({ items: [], itemIds: {}, }); }; /** * Adds file to the upload queue and starts upload immediately * * @param {Array<UploadFileWithAPIOptions | File>} files - Files to be added to upload queue * @return {void} */ addFilesWithOptionsToUploadQueueAndStartUpload = ( files?: Array<UploadFileWithAPIOptions | File>, dataTransferItems: Array<DataTransferItem | UploadDataTransferItemWithAPIOptions>, ): void => { this.addFilesToUploadQueue(files, this.upload); this.addDataTransferItemsToUploadQueue(dataTransferItems, this.upload); }; /** * Renders the content uploader * * @inheritdoc * @return {Component} */ render() { const { language, messages, onClose, className, measureRef, isTouch, fileLimit, useUploadsManager, isResumableUploadsEnabled, isFolderUploadEnabled, isDraggingItemsToUploadsManager = false, }: Props = this.props; const { view, items, errorCode, isUploadsManagerExpanded }: State = this.state; const isEmpty = items.length === 0; const isVisible = !isEmpty || !!isDraggingItemsToUploadsManager; const hasFiles = items.length !== 0; const isLoading = items.some(item => item.status === STATUS_IN_PROGRESS); const isDone = items.every(item => item.status === STATUS_COMPLETE || item.status === STATUS_STAGED); const styleClassName = classNames('bcu', className, { 'be-app-element': !useUploadsManager, be: !useUploadsManager, }); return ( <Internationalize language={language} messages={messages}> {useUploadsManager ? ( <div ref={measureRef} className={styleClassName} id={this.id}> <UploadsManager isDragging={isDraggingItemsToUploadsManager} isExpanded={isUploadsManagerExpanded} isResumableUploadsEnabled={isResumableUploadsEnabled} isVisible={isVisible} items={items} onItemActionClick={this.onClick} onRemoveActionClick={this.removeFileFromUploadQueue} onUploadsManagerActionClick={this.clickAllWithStatus} toggleUploadsManager={this.toggleUploadsManager} view={view} /> </div> ) : ( <div ref={measureRef} className={styleClassName} id={this.id}> <DroppableContent addDataTransferItemsToUploadQueue={this.addDroppedItemsToUploadQueue} addFiles={this.addFilesToUploadQueue} allowedTypes={['Files']} isFolderUploadEnabled={isFolderUploadEnabled} isTouch={isTouch} items={items} onClick={this.onClick} view={view} /> <Footer errorCode={errorCode} fileLimit={fileLimit} hasFiles={hasFiles} isLoading={isLoading} onCancel={this.cancel} onClose={onClose} onUpload={this.upload} isDone={isDone} /> </div> )} </Internationalize> ); } } export type ContentUploaderProps = Props; export default makeResponsive(ContentUploader); export { ContentUploader as ContentUploaderComponent, CHUNKED_UPLOAD_MIN_SIZE_BYTES };