diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bd4747ccb5..a1f2e55bb8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -118,9 +118,9 @@ } }, "@seafile/resumablejs": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@seafile/resumablejs/-/resumablejs-1.1.9.tgz", - "integrity": "sha512-YsAX+gnnf1ytv7asZgJP7T56DALQniKtRVtlz0f11PljLV19I1Av+Oz3QcYaRiKhCCB+EMnVKI9Yc14sYKp6lA==" + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@seafile/resumablejs/-/resumablejs-1.1.12.tgz", + "integrity": "sha512-IK3POb3mdqFOJwQRerzpamQf5/3LdKFFgxe81M6X/ZQwjusINZKJwTZmqawKU1EnW3ghX7d3HW0nmcIrZayfLw==" }, "@seafile/seafile-editor": { "version": "0.2.57", @@ -2216,7 +2216,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -2267,7 +2267,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -4296,7 +4296,7 @@ }, "events": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", "dev": true }, @@ -12394,7 +12394,7 @@ }, "yargs": { "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", "dev": true, "requires": { @@ -12929,7 +12929,7 @@ }, "load-json-file": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "dev": true, "requires": { @@ -13219,7 +13219,7 @@ }, "yargs": { "version": "6.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", "dev": true, "requires": { diff --git a/frontend/package.json b/frontend/package.json index 515d6ae72a..4f0eaa4e7e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@reach/router": "^1.2.0", - "@seafile/resumablejs": "^1.1.9", + "@seafile/resumablejs": "^1.1.12", "@seafile/seafile-editor": "^0.2.57", "MD5": "^1.3.0", "autoprefixer": "7.1.6", diff --git a/frontend/src/components/file-uploader/file-uploader.js b/frontend/src/components/file-uploader/file-uploader.js index 372547bb39..f7a1cff58f 100644 --- a/frontend/src/components/file-uploader/file-uploader.js +++ b/frontend/src/components/file-uploader/file-uploader.js @@ -37,6 +37,7 @@ class FileUploader extends React.Component { constructor(props) { super(props); this.state = { + retryFileList: [], uploadFileList: [], totalProgress: 0, isUploadProgressDialogShow: false, @@ -140,6 +141,12 @@ class FileUploader extends React.Component { } onChunkingComplete = (resumableFile) => { + + let allFilesUploaded = this.state.allFilesUploaded; + if (allFilesUploaded === true) { + this.setState({allFilesUploaded: false}); + } + //get parent_dir relative_path let path = this.props.path === '/' ? '/' : this.props.path + '/'; let fileName = resumableFile.fileName; @@ -210,42 +217,38 @@ class FileUploader extends React.Component { } setUploadFileList = () => { - let uploadFileList = this.resumable.files.map(resumableFile => { - return this.buildCustomFileObj(resumableFile); - }); + let uploadFileList = this.resumable.files; this.setState({ - isUploadProgressDialogShow: true, uploadFileList: uploadFileList, + isUploadProgressDialogShow: true, }); Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', true); } - buildCustomFileObj = (resumableFile) => { - return { - uniqueIdentifier: resumableFile.uniqueIdentifier, - resumableFile: resumableFile, - progress: resumableFile.progress(), - isSaved: resumableFile.progress() === 1 ? true : false, // The 'isSaved' property is not saved in resumableFile. - }; - } - - onFileProgress = (file) => { - let uniqueIdentifier = file.uniqueIdentifier; + onFileProgress = (resumableFile) => { + let uploadBitrate = this.getBitrate(); let uploadFileList = this.state.uploadFileList.map(item => { - if (item.uniqueIdentifier === uniqueIdentifier) { - item.progress = Math.round(file.progress() * 100); + if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) { + if (uploadBitrate) { + let lastSize = (item.size - (item.size * item.progress())) * 8; + let time = Math.ceil(lastSize / uploadBitrate); + item.remainingTime = time; + } } return item; }); - this.setState({uploadFileList: uploadFileList}); + this.setState({ + uploadBitrate: uploadBitrate, + uploadFileList: uploadFileList + }); } getBitrate = () => { let loaded = 0; let uploadBitrate = 0; let now = new Date().getTime(); - + this.resumable.files.forEach(file => { loaded += file.progress() * file.size; }); @@ -255,23 +258,25 @@ class FileUploader extends React.Component { if (timeDiff < this.bitrateInterval) { return this.state.uploadBitrate; } + + // 1. Cancel will produce loaded greater than this.loaded + // 2. reset can make this.loaded to be 0 + if (loaded < this.loaded || this.loaded === 0) { + this.loaded = loaded; // + } + uploadBitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; } this.timestamp = now; this.loaded = loaded; - - uploadBitrate = Utils.formatBitRate(uploadBitrate); + return uploadBitrate; } onProgress = () => { let progress = Math.round(this.resumable.progress() * 100); - let uploadBitrate = this.getBitrate(); - this.setState({ - totalProgress: progress, - uploadBitrate: uploadBitrate - }); + this.setState({totalProgress: progress}); Utils.registerGlobalVariable('uploader', 'totalProgress', progress); } @@ -298,10 +303,10 @@ class FileUploader extends React.Component { // update uploadFileList let uploadFileList = this.state.uploadFileList.map(item => { - if (item.resumableFile.uniqueIdentifier === resumableFile.uniqueIdentifier) { - item.resumableFile.fileName = message.name; - item.resumableFile.relativePath = relative_path + message.name; + if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) { + item.newFileName = relative_path + message.name; item.isSaved = true; + item.remainingTime = 0; } return item; }); @@ -321,8 +326,10 @@ class FileUploader extends React.Component { this.props.onFileUploadSuccess(dirent); // this contance: just one file let uploadFileList = this.state.uploadFileList.map(item => { - if (item.resumableFile.uniqueIdentifier === resumableFile.uniqueIdentifier) { + if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) { + item.newFileName = fileName; item.isSaved = true; + item.remainingTime = 0; } return item; }); @@ -342,10 +349,10 @@ class FileUploader extends React.Component { this.props.onFileUploadSuccess(dirent); // this contance: no repetition file let uploadFileList = this.state.uploadFileList.map(item => { - if (item.resumableFile.uniqueIdentifier === resumableFile.uniqueIdentifier) { - item.resumableFile.fileName = message.name; - item.resumableFile.relativePath = message.name; + if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) { + item.newFileName = message.name; item.isSaved = true; + item.remainingTime = 0; } return item; }); @@ -361,12 +368,18 @@ class FileUploader extends React.Component { } let uploadFileList = this.state.uploadFileList.map(item => { - if (item.resumableFile.uniqueIdentifier === resumableFile.uniqueIdentifier) { - item.resumableFile.error = error; + if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) { + this.state.retryFileList.push(item); + item.error = error; } return item; }); - this.setState({uploadFileList: uploadFileList}); + + this.loaded = 0; // reset loaded data; + this.setState({ + retryFileList: this.state.retryFileList, + uploadFileList: uploadFileList + }); } @@ -384,11 +397,11 @@ class FileUploader extends React.Component { } onFileRetry = () => { - //todos, cancel upload file, uploded again; + // todo, cancel upload file, uploded again; } onBeforeCancel = () => { - //todos, giving a pop message ? + // todo, giving a pop message ? } onCancel = () => { @@ -476,43 +489,96 @@ class FileUploader extends React.Component { } onCloseUploadDialog = () => { + this.loaded = 0; this.resumable.files = []; this.setState({isUploadProgressDialogShow: false, uploadFileList: []}); Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', false); } onUploadCancel = (uploadingItem) => { + let uploadFileList = this.state.uploadFileList.filter(item => { if (item.uniqueIdentifier === uploadingItem.uniqueIdentifier) { - uploadingItem.resumableFile.cancel(); - this.resumable.removeFile(uploadingItem.resumableFile.file); - } else { - return item; + item.cancel(); // execute cancel function will delete the file at the same time + return false; } + return true; }); - let newUploaderFileList = uploadFileList.map(item => { - let progress = Math.round(item.resumableFile.progress() * 100); - item.progress = progress; - return item; - }); - this.setState({uploadFileList: newUploaderFileList}); + + if (!this.resumable.isUploading()) { + this.setState({ + totalProgress: '100', + allFilesUploaded: true, + }); + this.loaded = 0; + } + + this.setState({uploadFileList: uploadFileList}); } onCancelAllUploading = () => { let uploadFileList = this.state.uploadFileList.filter(item => { - let resumableFile = item.resumableFile; - if (Math.round(resumableFile.progress() !== 1)) { - resumableFile.cancel(); - this.resumable.removeFile(resumableFile.file); - } else { - return item; + if (Math.round(item.progress() !== 1)) { + item.cancel(); + return false; } + return true; + }); + + this.loaded = 0; + + this.setState({ + allFilesUploaded: true, + totalProgress: '100', + uploadFileList: uploadFileList }); - this.setState({uploadFileList: uploadFileList}); } - onUploaderRetry = () => { + onUploadRetry = (resumableFile) => { + seafileAPI.getUploadLink(this.props.repoID, this.props.path).then(res => { + this.resumable.opts.target = res.data; + + let retryFileList = this.state.retryFileList.filter(item => { + return item.uniqueIdentifier !== resumableFile.uniqueIdentifier; + }); + let uploadFileList = this.state.uploadFileList.map(item => { + if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) { + item.error = null; + item.retry(); + } + return item; + }); + + this.setState({ + retryFileList: retryFileList, + uploadFileList: uploadFileList + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + onUploadRetryAll = () => { + + seafileAPI.getUploadLink(this.props.repoID, this.props.path).then(res => { + this.resumable.opts.target = res.data; + this.state.retryFileList.forEach(item => { + item.retry(); + item.error = false; + }); + + let uploadFileList = this.state.uploadFileList.slice(0); + this.setState({ + retryFileList: [], + uploadFileList: uploadFileList + }); + + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); } replaceRepetitionFile = () => { @@ -534,11 +600,10 @@ class FileUploader extends React.Component { uploadFile = () => { let resumableFile = this.resumable.files[this.resumable.files.length - 1]; - let fileObject = this.buildCustomFileObj(resumableFile); this.setState({ isUploadRemindDialogShow: false, isUploadProgressDialogShow: true, - uploadFileList: [...this.state.uploadFileList, fileObject] + uploadFileList: [...this.state.uploadFileList, resumableFile] }, () => { this.resumable.upload(); }); @@ -568,6 +633,7 @@ class FileUploader extends React.Component { } {this.state.isUploadProgressDialogShow && } diff --git a/frontend/src/components/file-uploader/upload-list-item.js b/frontend/src/components/file-uploader/upload-list-item.js index 5bdf15e629..fd5dea27be 100644 --- a/frontend/src/components/file-uploader/upload-list-item.js +++ b/frontend/src/components/file-uploader/upload-list-item.js @@ -1,17 +1,55 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { gettext } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; const propTypes = { - item: PropTypes.object.isRequired, + resumableFile: PropTypes.object.isRequired, onUploadCancel: PropTypes.func.isRequired, + onUploadRetry: PropTypes.func.isRequired, }; +const UPLOAD_UPLOADING = 'uploading'; +const UPLOAD_ERROR = 'error'; +const UPLOAD_ISSAVING = 'isSaving'; +const UPLOAD_UPLOADED = 'uploaded'; + class UploadListItem extends React.Component { + constructor(props) { + super(props); + this.state = { + uploadState: UPLOAD_UPLOADING + } + } + + componentWillReceiveProps(nextProps) { + let { resumableFile } = nextProps; + let uploadState = UPLOAD_UPLOADING; + + if (resumableFile.error) { + uploadState = UPLOAD_ERROR; + } else { + if (resumableFile.progress() === 1 && !resumableFile.isSaved) { + uploadState = UPLOAD_ISSAVING; + } + + if (resumableFile.isSaved) { + uploadState = UPLOAD_UPLOADED; + } + } + + this.setState({uploadState: uploadState}); + } + onUploadCancel = (e) => { e.preventDefault(); - this.props.onUploadCancel(this.props.item); + this.props.onUploadCancel(this.props.resumableFile); + } + + onUploadRetry = (e) => { + e.preventDefault(); + this.props.onUploadRetry(this.props.resumableFile) } formatFileSize = (size) => { @@ -31,38 +69,69 @@ class UploadListItem extends React.Component { } render() { - let { item } = this.props; - let progress = Math.round(item.resumableFile.progress() * 100); - let error = item.resumableFile.error; + let { resumableFile } = this.props; + let progress = Math.round(resumableFile.progress() * 100); + let error = resumableFile.error; return ( -
{item.resumableFile.relativePath}
-
+
{resumableFile.newFileName}
+ + + {this.formatFileSize(resumableFile.size)} - {this.formatFileSize(item.resumableFile.size)} - {!item.resumableFile.error && progress !== 100 && -
-
-
+ {this.state.uploadState === UPLOAD_UPLOADING && + + {resumableFile.size >= (100 * 1000 * 1000) && + + {resumableFile.isUploading() && ( +
+
+
+
+ {resumableFile.remainingTime === 0 &&
{gettext('Preparing to upload...')}
} + {resumableFile.remainingTime !== 0 &&
{gettext('Remaining')}{' '}{Utils.formatTime(resumableFile.remainingTime)}
} +
+ )} + {!resumableFile.isUploading() && ( +
+
+
+
+
+ )} +
+ } + {(resumableFile.size < (100 * 1000 * 1000)) && +
+
+
+
+
+ } +
} + {this.state.uploadState === UPLOAD_ERROR && ( +
+ )} - {!item.resumableFile.error && ( - - {(!item.isSaved && progress !== 100) && ( - {gettext('cancel')} - )} - {(!item.isSaved && progress === 100) && ( - {gettext('saving...')} - )} - {item.isSaved && ( - {gettext('uploaded')} - )} - - )} + + {this.state.uploadState === UPLOAD_UPLOADING && ( + {gettext('Cancel')} + )} + {this.state.uploadState === UPLOAD_ERROR && ( + {gettext('Retry')} + )} + {this.state.uploadState === UPLOAD_ISSAVING && ( + {gettext('Saving...')} + )} + {this.state.uploadState === UPLOAD_UPLOADED && ( + {gettext('Uploaded')} + )} + ); diff --git a/frontend/src/components/file-uploader/upload-progress-dialog.js b/frontend/src/components/file-uploader/upload-progress-dialog.js index 18464a8441..40511b4a35 100644 --- a/frontend/src/components/file-uploader/upload-progress-dialog.js +++ b/frontend/src/components/file-uploader/upload-progress-dialog.js @@ -2,14 +2,18 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { gettext } from '../../utils/constants'; import UploadListItem from './upload-list-item'; +import { Utils } from '../../utils/utils'; const propTypes = { uploadBitrate: PropTypes.string.isRequired, totalProgress: PropTypes.number.isRequired, + retryFileList: PropTypes.array.isRequired, uploadFileList: PropTypes.array.isRequired, onCloseUploadDialog: PropTypes.func.isRequired, onCancelAllUploading: PropTypes.func.isRequired, onUploadCancel: PropTypes.func.isRequired, + onUploadRetry: PropTypes.func.isRequired, + onUploadRetryAll: PropTypes.func.isRequired, allFilesUploaded: PropTypes.bool.isRequired, }; @@ -37,8 +41,10 @@ class UploadProgressDialog extends React.Component { } render() { + + let uploadBitrate = Utils.formatBitRate(this.props.uploadBitrate) let uploadedMessage = gettext('File Upload'); - let uploadingMessage = gettext('File Uploading...') + ' ' + this.props.totalProgress + '%' + ' (' + this.props.uploadBitrate + ')'; + let uploadingMessage = gettext('File Uploading...') + ' ' + this.props.totalProgress + '%' + ' (' + uploadBitrate + ')'; let uploadingOptions = (); @@ -49,7 +55,7 @@ class UploadProgressDialog extends React.Component { ); - let { totalProgress, allFilesUploaded } = this.props; + let { totalProgress, allFilesUploaded, retryFileList } = this.props; return (
@@ -65,19 +71,38 @@ class UploadProgressDialog extends React.Component { - - + + + - {(!allFilesUploaded) && - - } + + + + { - this.props.uploadFileList.map((item, index) => { + this.props.uploadFileList.map((resumableFile, index) => { return ( - + ); }) } diff --git a/frontend/src/css/file-uploader.css b/frontend/src/css/file-uploader.css index 29ea99d0d6..3a4ddc57b1 100644 --- a/frontend/src/css/file-uploader.css +++ b/frontend/src/css/file-uploader.css @@ -43,20 +43,41 @@ } .uploader-list-content { - padding: 0.625rem 1rem 1.25rem; + padding: 0rem 1rem 1.25rem; background-color: #fff; overflow: auto; } -.file-upload-item .progress { +.file-upload-item { + height: 44px; +} + +.upload-progress .progress-container { + height: 24px; + padding: 4px 0; +} + +.upload-progress .progress { + height: 5px; width: 80%; } -.file-upload-item .progress .progress-bar { +.upload-progress .progress .progress-bar { color: #e83; } -.file-upload-item .saving { +.upload-progress .progress-text { + margin-top: 2px; + font-size: 12px; + line-height: 12px; + color: #666666; +} + +.upload-operation .saving { color: #ee8204; word-wrap: break-word; +} + +.disabled-link { + color: #999999; } \ No newline at end of file diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 7069f20c5c..ce8ce32c48 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -989,11 +989,38 @@ export const Utils = { return false; }, - registerGlobalVariable(namespace, key, value) { + registerGlobalVariable: function(namespace, key, value) { if (!window[namespace]) { window[namespace] = {}; } window[namespace][key] = value; + }, + + formatTime: function(seconds) { + var ss = parseInt(seconds); + var mm = 0; + var hh = 0; + if (ss > 60) { + mm = parseInt(ss / 60); + ss = parseInt(ss % 60); + } + if (mm > 60) { + hh = parseInt(mm / 60); + mm = parseInt(mm % 60); + } + + var result = ('00' + parseInt(ss)).slice(-2); + if (mm > 0) { + result = ('00' + parseInt(mm)).slice(-2) + ':' + result; + } else { + result = '00:' + result; + } + if (hh > 0) { + result = ('00' + parseInt(hh)).slice(-2) + ':' + result; + } else { + result = '00:' + result; + } + return result; } };
{gettext('name')}{gettext('progress')}{gettext('name')}{gettext('size')}{gettext('progress')} {gettext('state')}
{gettext('Cancel All')}
+ {retryFileList.length > 0 ? + {gettext('Retry All')} + : + {gettext('Retry All')} + } + + {!allFilesUploaded ? + {gettext('Cancel All')} + : + {gettext('Cancel All')} + } +