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 &&
- - {gettext('Upload Files')}
- - {gettext('Upload Folder')}
+ - {gettext('File Upload')}
+ - {gettext('Folder Upload')}
}
{
@@ -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 }}'
}
};