From 360bd739ce64aae6ec3eac0c3a40e62b22dabd16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E9=A1=BA=E5=BC=BA?= Date: Wed, 14 Nov 2018 10:55:11 +0800 Subject: [PATCH] Implement file upload (#2506) --- frontend/package-lock.json | 28 +- frontend/package.json | 2 + .../components/dialog/upload-remind-dialog.js | 42 ++ .../components/file-uploader/file-uploader.js | 379 ++++++++++++++++++ .../file-uploader/upload-list-item.js | 58 +++ .../file-uploader/upload-progress-dialog.js | 71 ++++ frontend/src/css/file-uploader.css | 46 +++ frontend/src/css/react-resumable.css | 13 + .../src/pages/repo-wiki-mode/main-panel.js | 69 +++- frontend/src/utils/constants.js | 3 + frontend/src/utils/utils.js | 5 + media/css/seahub_react.css | 2 + seahub/base/context_processors.py | 2 + seahub/templates/base_for_react.html | 4 +- 14 files changed, 701 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/dialog/upload-remind-dialog.js create mode 100644 frontend/src/components/file-uploader/file-uploader.js create mode 100644 frontend/src/components/file-uploader/upload-list-item.js create mode 100644 frontend/src/components/file-uploader/upload-progress-dialog.js create mode 100644 frontend/src/css/file-uploader.css create mode 100644 frontend/src/css/react-resumable.css diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f1a4382930..c526f34553 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -102,6 +102,11 @@ "warning": "^3.0.0" } }, + "@seafile/resumablejs": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@seafile/resumablejs/-/resumablejs-1.1.6.tgz", + "integrity": "sha512-RTEJHxaEBHMC9bX2v04pfnI7YvFZhRxzilRpkhdJrNMUekVYFYW/C7N7g2DtoXXOrMoWin+okrVKpbp4xUICOA==" + }, "@seafile/seafile-editor": { "version": "0.1.35", "resolved": "https://registry.npmjs.org/@seafile/seafile-editor/-/seafile-editor-0.1.35.tgz", @@ -174,7 +179,7 @@ "dependencies": { "reactstrap": { "version": "5.0.0", - "resolved": "http://registry.npmjs.org/reactstrap/-/reactstrap-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-5.0.0.tgz", "integrity": "sha512-y0eju/LAK7gbEaTFfq2iW92MF7/5Qh0tc1LgYr2mg92IX8NodGc03a+I+cp7bJ0VXHAiLy0bFL9UP89oSm4cBg==", "requires": { "classnames": "^2.2.3", @@ -253,6 +258,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.3.tgz", "integrity": "sha512-sfGmOtSMSbQ/AKG8V9xD1gmjquC9awIIZ/Kj309pHb2n3bcRAcGMQv5nJ6gCXZVsneGE4+ve8DXKRCsrg3TFzg==" }, + "MD5": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/MD5/-/MD5-1.3.0.tgz", + "integrity": "sha1-PMJm8Oiau2tDpQ85pFnfW/3gskA=", + "requires": { + "charenc": ">= 0.0.1", + "crypt": ">= 0.0.1" + } + }, "abab": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", @@ -617,7 +631,7 @@ }, "axios": { "version": "0.18.0", - "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", "requires": { "follow-redirects": "^1.3.0", @@ -2338,6 +2352,11 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, "chokidar": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", @@ -2792,6 +2811,11 @@ "which": "^1.2.9" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9ec076b593..e0525e2f7d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,9 @@ "private": true, "dependencies": { "@reach/router": "^1.2.0", + "@seafile/resumablejs": "^1.1.6", "@seafile/seafile-editor": "^0.1.35", + "MD5": "^1.3.0", "autoprefixer": "7.1.6", "classnames": "^2.2.6", "css-loader": "0.28.7", diff --git a/frontend/src/components/dialog/upload-remind-dialog.js b/frontend/src/components/dialog/upload-remind-dialog.js new file mode 100644 index 0000000000..3c1d1d621d --- /dev/null +++ b/frontend/src/components/dialog/upload-remind-dialog.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../utils/constants'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; + +const propTypes = { + currentResumableFile: PropTypes.object.isRequired, + replaceRepetitionFile: PropTypes.func.isRequired, + uploadFile: PropTypes.func.isRequired, + cancelFileUpload: PropTypes.func.isRequired, +}; + +class UploadRemindDialog extends React.Component { + + toggle = () => { + this.props.cancelFileUpload(); + } + + render() { + + let title = gettext('Replace file {filename}?'); + title = title.replace('{filename}', '' + this.props.currentResumableFile.fileName + ''); + return ( + +
+ +

{gettext('A file with the same name already exists in this folder.')}

+

{gettext('Replacing it will overwrite its content.')}

+
+ + + + + +
+ ); + } +} + +UploadRemindDialog.propTypes = propTypes; + +export default UploadRemindDialog; diff --git a/frontend/src/components/file-uploader/file-uploader.js b/frontend/src/components/file-uploader/file-uploader.js new file mode 100644 index 0000000000..0b55c93df7 --- /dev/null +++ b/frontend/src/components/file-uploader/file-uploader.js @@ -0,0 +1,379 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Resumablejs from '@seafile/resumablejs'; +import MD5 from 'MD5'; +import { repoID, enableResumableFileUpload } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import UploadProgressDialog from './upload-progress-dialog'; +import UploadRemindDialog from '../dialog/upload-remind-dialog'; +import '../../css/file-uploader.css'; + +const propTypes = { + filetypes: PropTypes.array, + chunkSize: PropTypes.number, + withCredentials: PropTypes.bool, + maxFiles: PropTypes.number, + maxFileSize: PropTypes.number, + testMethod: PropTypes.string, + testChunks: PropTypes.number, + simultaneousUploads: PropTypes.number, + fileParameterName: PropTypes.string, + maxFilesErrorCallback: PropTypes.func, + maxFileSizeErrorCallback: PropTypes.func, + minFileSizeErrorCallback: PropTypes.func, + fileTypeErrorCallback: PropTypes.func, + dragAndDrop: PropTypes.bool.isRequired, + filePath: PropTypes.string.isRequired, + onFileSuccess: PropTypes.func.isRequired, +}; + +class FileUploader extends React.Component { + + constructor(props) { + super(props); + this.state = { + uploadFileList: [], + totalProgress: 0, + isUploadProgressDialogShow: false, + isUploadRemindDialogShow: false, + currentResumableFile: null, + }; + } + + componentDidMount() { + this.resumable = new Resumablejs({ + target: '', + query: this.setQuery || {}, + fileType: this.props.filetypes, + maxFiles: this.props.maxFiles, + maxFileSize: this.props.maxFileSize, + testMethod: this.props.testMethod || 'post', + testChunks: this.props.testChunks || false, + headers: this.setHeaders || {}, + withCredentials: this.props.withCredentials || false, + chunkSize: this.props.chunkSize, + simultaneousUploads: this.props.simultaneousUploads || 1, + fileParameterName: this.props.fileParameterName, + generateUniqueIdentifier: this.generateUniqueIdentifier, + forceChunkSize: true, + }); + + this.resumable.assignBrowse(this.uploadInput, true); + + //Enable or Disable DragAnd Drop + if (this.props.dragAndDrop === true) { + this.resumable.enableDropOnDocument(); + } + + this.bindCallbackHandler(); + this.bindEventHandler(); + } + + bindCallbackHandler = () => { + let {maxFilesErrorCallback, minFileSizeErrorCallback, maxFileSizeErrorCallback, fileTypeErrorCallback } = this.props; + + if (maxFilesErrorCallback) { + this.resumable.opts.maxFilesErrorCallback = this.props.maxFilesErrorCallback; + } + + if (minFileSizeErrorCallback) { + this.resumable.opts.minFileSizeErrorCallback = this.props.minFileSizeErrorCallback; + } + + if (maxFileSizeErrorCallback) { + this.resumable.opts.maxFileSizeErrorCallback = this.props.maxFileSizeErrorCallback; + } + + if (fileTypeErrorCallback) { + this.resumable.opts.fileTypeErrorCallback = this.props.fileTypeErrorCallback; + } + + } + + bindEventHandler = () => { + this.resumable.on('chunkingComplete', this.onChunkingComplete); + this.resumable.on('fileAdded', this.onFileAdded); + this.resumable.on('filesAddedComplete', this.filesAddedComplete); + this.resumable.on('fileProgress', this.onFileProgress); + this.resumable.on('fileSuccess', this.onFileSuccess); + this.resumable.on('progress', this.onProgress); + this.resumable.on('complete', this.onComplete); + this.resumable.on('pause', this.onPause); + this.resumable.on('fileRetry', this.onFileRetry); + this.resumable.on('fileError', this.onFileError); + this.resumable.on('error', this.onError); + this.resumable.on('beforeCancel', this.onBeforeCancel); + this.resumable.on('cancel', this.onCancel); + this.resumable.on('dragstart', this.onDragStart); + } + + onChunkingComplete = (file) => { + if (file.relativePath !== file.fileName) { + return; // is upload a folder; + } + if (enableResumableFileUpload) { + seafileAPI.getFileUploadedBytes(repoID, this.props.filePath, file.fileName).then(res => { + let uploadedBytes = res.data.uploadedBytes; + let offset = Math.floor(uploadedBytes / (1024 * 1024)); + file.markChunksCompleted(offset); + }); + } + } + + onFileAdded = (resumableFile, files) => { + //get parent_dir、relative_path; + let filePath = this.props.filePath === '/' ? '/' : this.props.filePath + '/'; + let fileName = resumableFile.fileName; + let relativePath = resumableFile.relativePath; + let isFile = fileName === relativePath; + + //update formdata; + resumableFile.formData = {}; + if (isFile) { + resumableFile.formData = { + parent_dir: filePath, + }; + } else { + let relative_path = relativePath.slice(0, relativePath.lastIndexOf('/') + 1); + resumableFile.formData = { + parent_dir: filePath, + relative_path: relative_path + }; + } + + //check repetition + //uploading is file and only upload one file + if (isFile && files.length === 1) { + let hasRepetition = false; + let direntList = this.props.direntList; + for (let i = 0; i < direntList.length; i++) { + if (direntList[i].type === 'file' && direntList[i].name === resumableFile.fileName) { + hasRepetition = true; + break; + } + } + if (hasRepetition) { + this.setState({ + isUploadRemindDialogShow: true, + currentResumableFile: resumableFile, + }); + } else { + this.setUploadFileList(this.resumable.files); + resumableFile.upload(); + } + } else { + this.setUploadFileList(this.resumable.files); + resumableFile.upload(); + } + } + + filesAddedComplete = (resumable, files) => { + // single file uploading can check repetition, because custom dialog conn't prevent program execution; + } + + setUploadFileList = (files) => { + let uploadFileList = files.map(resumableFile => { + return this.buildCustomFileObj(resumableFile); + }); + this.setState({ + isUploadRemindDialogShow: false, + isUploadProgressDialogShow: true, + uploadFileList: uploadFileList + }); + } + + buildCustomFileObj = (resumableFile) => { + return { + uniqueIdentifier: resumableFile.uniqueIdentifier, + resumableFile: resumableFile, + progress: resumableFile.progress(), + }; + } + + onFileProgress = (file) => { + let uniqueIdentifier = file.uniqueIdentifier; + let uploadFileList = this.state.uploadFileList.map(item => { + if (item.uniqueIdentifier === uniqueIdentifier) { + item.progress = Math.round(file.progress() * 100); + } + return item; + }); + + this.setState({uploadFileList: uploadFileList}); + } + + onFileSuccess = (file) => { + // todos, update uploadList or updateList; + } + + onFileError = (file) => { + + } + + onProgress = () => { + let progress = Math.round(this.resumable.progress() * 100); + this.setState({totalProgress: progress}); + } + + onComplete = () => { + + } + + onPause = () => [ + + ] + + onError = () => { + + } + + onFileRetry = () => { + //todos, cancel upload file, uploded again; + } + + onBeforeCancel = () => { + //todos, giving a pop message ? + } + + onCancel = () => { + + } + + setHeaders = (resumableFile, resumable) => { + let offset = resumable.offset; + let chunkSize = resumable.getOpt('chunkSize'); + let fileSize = resumableFile.size; + let startByte = offset !== 0 ? offset * chunkSize : 0; + let endByte = Math.min(fileSize, (offset + 1) * chunkSize) - 1; + + if (fileSize - resumable.endByte < chunkSize && !resumable.getOpt('forceChunkSize')) { + endByte = fileSize; + } + + let headers = { + 'Accept': 'application/json; text/javascript, */*; q=0.01', + 'Content-Disposition': 'attachment; filename="' + encodeURI(resumableFile.fileName) + '"', + 'Content-Range': 'bytes ' + startByte + '-' + endByte + '/' + fileSize, + }; + + return headers; + } + + setQuery = (resumableFile) => { + let formData = resumableFile.formData; + return formData; + } + + generateUniqueIdentifier = (file) => { + let relativePath = file.webkitRelativePath||file.relativePath||file.fileName||file.name; + return MD5(relativePath + new Date()) + relativePath; + } + + onClick = (e) => { + e.nativeEvent.stopImmediatePropagation(); + e.stopPropagation(); + } + + onFileUpload = () => { + this.uploadInput.removeAttribute('webkitdirectory'); + this.uploadInput.click(); + seafileAPI.getUploadLink(repoID, this.props.filePath).then(res => { + this.resumable.opts.target = res.data; + }); + } + + onFolderUpload = () => { + this.uploadInput.setAttribute('webkitdirectory', 'webkitdirectory'); + this.uploadInput.click(); + seafileAPI.getUploadLink(repoID, this.props.filePath).then(res => { + this.resumable.opts.target = res.data; + }); + } + + onDragStart = () => { + this.uploadInput.setAttribute('webkitdirectory', 'webkitdirectory'); + seafileAPI.getUploadLink(repoID, this.props.filePath).then(res => { + this.resumable.opts.target = res.data; + }); + } + + onMinimizeUploadDialog = () => { + this.setState({isUploadProgressDialogShow: false}); + } + + onCloseUploadDialog = () => { + this.setState({isUploadProgressDialogShow: false, uploadFileList: []}); + } + + onUploadCancel = (resumableFile) => { + let uploadFileList = this.state.uploadFileList.filter(item => { + return item.uniqueIdentifier !== resumableFile.uniqueIdentifier; + }); + let newUploaderFileList = uploadFileList.map(item => { + let progress = Math.round(item.resumableFile.progress() * 100); + item.progress = progress; + return item; + }); + this.setState({uploadFileList: newUploaderFileList}); + } + + onUploaderRetry = () => { + + } + + replaceRepetitionFile = () => { + let resumableFile = this.resumable.files[this.resumable.files.length - 1]; + resumableFile.formData['replace'] = 1; + + // this.setState({isUploadRemindDialogShow: false}); + + this.setUploadFileList(this.resumable.files); + + this.resumable.upload(); + } + + uploadFile = () => { + // this.setState({isUploadRemindDialogShow: false}); + + this.setUploadFileList(this.resumable.files); + this.resumable.upload(); + } + + cancelFileUpload = () => { + this.resumable.files.pop(); //delete latest file; + this.setState({isUploadRemindDialogShow: false}); + } + + render() { + return ( +
+
+ this.uploadInput = node} onClick={this.onClick}/> +
+ { + this.state.isUploadProgressDialogShow && + + } + { + this.state.isUploadRemindDialogShow && + + } +
+ ); + } +} + +FileUploader.propTypes = propTypes; + +export default FileUploader; diff --git a/frontend/src/components/file-uploader/upload-list-item.js b/frontend/src/components/file-uploader/upload-list-item.js new file mode 100644 index 0000000000..22baeac3e2 --- /dev/null +++ b/frontend/src/components/file-uploader/upload-list-item.js @@ -0,0 +1,58 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../utils/constants'; + +const propTypes = { + item: PropTypes.object.isRequired, + onUploadCancel: PropTypes.func.isRequired, +}; + +class UploadListItem extends React.Component { + + onUploadCancel = () => { + let item = this.props.item; + item.resumableFile.cancel(); + this.props.onUploadCancel(item); + } + + formatFileSize = (size) => { + if (typeof size !== 'number') { + return ''; + } + if (size >= 1000 * 1000 * 1000) { + return (size / (1000 * 1000 * 1000)).toFixed(1) + ' G'; + } + if (size >= 1000 * 1000) { + return (size / (1000 * 1000)).toFixed(1) + ' M'; + } + if (size >= 1000) { + return (size / 1000).toFixed(1) + ' K'; + } + return size.toFixed(1) + ' B'; + } + + render() { + let { item } = this.props; + let progress = Math.round(item.resumableFile.progress() * 100); + return ( + + {item.resumableFile.relativePath} + + { + progress === 100 ? this.formatFileSize(item.resumableFile.size) : progress + '%' + } + + + { progress !== 100 ? + {gettext(('cancel'))} : + {gettext('uploaded')} + } + + + ); + } +} + +UploadListItem.propTypes = propTypes; + +export default UploadListItem; diff --git a/frontend/src/components/file-uploader/upload-progress-dialog.js b/frontend/src/components/file-uploader/upload-progress-dialog.js new file mode 100644 index 0000000000..99236b969e --- /dev/null +++ b/frontend/src/components/file-uploader/upload-progress-dialog.js @@ -0,0 +1,71 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../utils/constants'; +import UploadListItem from './upload-list-item'; + +const propTypes = { + totalProgress: PropTypes.number.isRequired, + uploadFileList: PropTypes.array.isRequired, + onMinimizeUploadDialog: PropTypes.func.isRequired, + onCloseUploadDialog: PropTypes.func.isRequired, + onUploadCancel: PropTypes.func.isRequired, +}; + +class UploadProgressDialog extends React.Component { + + onMinimizeUpload = (e) => { + e.nativeEvent.stopImmediatePropagation(); + this.props.onMinimizeUploadDialog(); + } + + onCloseUpload = (e) => { + e.nativeEvent.stopImmediatePropagation(); + this.props.onCloseUploadDialog(); + } + + render() { + let uploadedMessage = gettext('File Upload'); + let uploadingMessage = gettext('File is upload...') + this.props.totalProgress + '%'; + + let uploadingOptions = (); + + let uploadedOptions = ( + + + + + ); + + let totalProgress = this.props.totalProgress; + + return ( +
+
+
+ {totalProgress === 100 ? uploadedMessage : uploadingMessage} +
+
+ {totalProgress === 100 ? uploadedOptions : uploadingOptions} +
+
+
+ + + { + this.props.uploadFileList.map((item, index) => { + return ( + + ); + }) + } + +
+
+
+ ); + } +} + +UploadProgressDialog.propTypes = propTypes; + +export default UploadProgressDialog; diff --git a/frontend/src/css/file-uploader.css b/frontend/src/css/file-uploader.css new file mode 100644 index 0000000000..1193324c93 --- /dev/null +++ b/frontend/src/css/file-uploader.css @@ -0,0 +1,46 @@ +.file-uploader-container { + display: flex; + flex: 1; +} + +.file-uploader { + position: fixed; + bottom: 99999px; +} + +.uploader-list-view { + display: flex; + flex-direction: column; + position: fixed; + right: 1px; + bottom: 1px; + width: 35rem; + min-height: 15rem; + max-height: 20rem; + border: 1px solid #ddd; + border-radius: 3px; + box-shadow: 0 0 6px #ddd; +} + +.uploader-list-header { + background-color: #f0f0f0; + padding: 0.375rem 0.625rem; + font-size: 1rem; + line-height: 1.5; + color: #322; + display: flex; + justify-content: space-between; + min-height: 2.25rem; +} + +.uploader-list-header .uploader-options span{ + display: inline-block; + margin-left: 0.25rem; + font-size: 18px; + color: #b8b8b8; + cursor: pointer; +} + +.uploader-list-content { + background-color: #fff; +} \ No newline at end of file diff --git a/frontend/src/css/react-resumable.css b/frontend/src/css/react-resumable.css new file mode 100644 index 0000000000..40e61780a2 --- /dev/null +++ b/frontend/src/css/react-resumable.css @@ -0,0 +1,13 @@ +.sf-resumable-input-container { + display: flex; + flex: 1; +} + +.sf-resumable-input-container .resumable-input { + display: none; +} + +.sf-resumable-input-container .input-placeholder { + display: flex; + flex: 1; +} diff --git a/frontend/src/pages/repo-wiki-mode/main-panel.js b/frontend/src/pages/repo-wiki-mode/main-panel.js index 7ec3fb93a7..4c2c0f9ce7 100644 --- a/frontend/src/pages/repo-wiki-mode/main-panel.js +++ b/frontend/src/pages/repo-wiki-mode/main-panel.js @@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { gettext, repoID, serviceUrl, slug, siteRoot } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; import Repo from '../../models/repo'; import Dirent from '../../models/dirent'; import CommonToolbar from '../../components/toolbar/common-toolbar'; @@ -11,6 +12,7 @@ import DirentListView from '../../components/dirent-list-view/dirent-list-view'; import DirentDetail from '../../components/dirent-detail/dirent-details'; import CreateFolder from '../../components/dialog/create-folder-dialog'; import CreateFile from '../../components/dialog/create-file-dialog'; +import FileUploader from '../../components/file-uploader/file-uploader'; const propTypes = { content: PropTypes.string, @@ -208,6 +210,20 @@ class MainPanel extends Component { this.updateViewList(this.props.filePath); } + uploadFile = (e) => { + e.nativeEvent.stopImmediatePropagation(); + this.uploader.onFileUpload(); + } + + uploadFolder = (e) => { + e.nativeEvent.stopImmediatePropagation(); + this.uploader.onFolderUpload(); + } + + onFileSuccess = (file) => { + + } + render() { let filePathList = this.props.filePath.split('/'); let nodePath = ''; @@ -249,17 +265,21 @@ class MainPanel extends Component { { !this.props.isViewFileState && - - - + { + Utils.isSupportUploadFolder() ? + : + + } + + } - { + { this.state.uploadMenuShow && } { @@ -303,20 +323,29 @@ class MainPanel extends Component { onLinkClick={this.props.onLinkClick} isFileLoading={this.props.isFileLoading} /> : - + + + this.uploader = uploader} + dragAndDrop={true} + filePath={this.props.filePath} + onFileSuccess={this.onFileSuccess} + direntList={this.state.direntList} + /> + } diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index c68e98770b..03289db495 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -19,6 +19,9 @@ export const canGenerateUploadLink = window.app.pageOptions.canGenerateUploadLin export const fileAuditEnabled = window.app.pageOptions.fileAuditEnabled ? true : false; export const enableFileComment = window.app.pageOptions.enableFileComment ? true : false; export const folderPermEnabled = window.app.pageOptions.folderPermEnabled === 'True'; +export const enableUploadFolder = window.app.pageOptions.enableUploadFolder === 'True'; +export const enableResumableFileUpload = window.app.pageOptions.enableResumableFileUpload === 'True'; + // wiki export const slug = window.wiki ? window.wiki.config.slug : ''; export const repoID = window.wiki ? window.wiki.config.repoId : ''; diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 40ed836ab5..d5d8c4e9fe 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -166,5 +166,10 @@ export const Utils = { getFileName: function(filePath) { let lastIndex = filePath.lastIndexOf('/'); return filePath.slice(lastIndex+1); + }, + + isSupportUploadFolder: function() { + return navigator.userAgent.indexOf('Firefox')!=-1 || + navigator.userAgent.indexOf('Chrome') > -1; } }; diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index e2fe85ed11..a6b3b814a8 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -77,6 +77,7 @@ .sf2-icon-confirm:before {content:"\e01e"} .sf2-icon-cancel:before {content:"\e01f"} .sf2-icon-tag:before {content:"\e037"} +.sf2-icon-minus:before {content:"\e01c"} /* common class and element style*/ a { color:#eb8205; } @@ -773,6 +774,7 @@ a.op-icon:focus { } .table-container table { width: 100%; + table-layout: fixed; } .table-container table th { text-align: left; diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index 9889c7028f..166b7339e3 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -117,6 +117,8 @@ def base(request): 'show_logout_icon': SHOW_LOGOUT_ICON, 'is_pro': True if is_pro_version() else False, 'enable_repo_wiki_mode': dj_settings.ENABLE_REPO_WIKI_MODE, + 'enable_upload_folder': dj_settings.ENABLE_UPLOAD_FOLDER, + 'enable_resumable_fileupload': dj_settings.ENABLE_RESUMABLE_FILEUPLOAD, } if request.user.is_staff: diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index a8f89d53e8..262ca1d0e5 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -39,7 +39,9 @@ canGenerateUploadLink: '{{ user.permissions.can_generate_upload_link }}', fileAuditEnabled: '{{ file_audit_enabled }}', enableFileComment: '{{ enable_file_comment }}', - folderPermEnabled: '{{ folder_perm_enabled }}' + folderPermEnabled: '{{ folder_perm_enabled }}', + enableUploadFolder: '{{ enable_upload_folder }}', + enableResumableFileUpload: '{{ enable_resumable_fileupload }}' } };