1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-07-14 15:35:35 +00:00

[shared upload link] rewrote it with react (#4611)

* [shared upload link] rewrote it with react

* modification
This commit is contained in:
llj 2020-10-20 16:24:25 +08:00 committed by GitHub
parent 5d41064329
commit 25cee90eda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1066 additions and 15 deletions

View File

@ -210,6 +210,11 @@ module.exports = {
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/pages/search",
],
uploadLink: [
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/pages/upload-link",
]
},
output: {

View File

@ -90,6 +90,7 @@ module.exports = {
orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"],
sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"],
search: [require.resolve('./polyfills'), paths.appSrc + "/pages/search"],
uploadLink: [require.resolve('./polyfills'), paths.appSrc + "/pages/upload-link"],
},
output: {

View File

@ -0,0 +1,44 @@
body {
overflow: hidden;
}
#wrapper {
height: 100%;
}
.top-header {
background: #f4f4f7;
border-bottom: 1px solid #e8e8e8;
padding: .5rem 1rem;
flex-shrink: 0;
}
#upload-link-panel {
width: 928px;
max-width: calc(100% - 20px);
border: 1px solid #ddd;
margin: 2em auto;
}
.shared-by .avatar {
width: 20px;
height: 20px;
}
#upload-link-panel .warning-icon {
color: #f25041;
font-size: 48px;
}
#upload-link-panel .tip-list-item {
list-style: decimal inside none;
}
#upload-link-drop-zone {
background: rgba(240, 159, 63, 0.1);
border: 2px dashed #f09f3f;
border-radius: 4px;
padding: 28px 0;
}
#upload-link-drop-zone .upload-icon {
color: rgba(240, 159, 63, 0.8);
font-size: 60px;
line-height: 1;
}
.mh-2 {
min-height: 2rem;
}

View File

@ -0,0 +1,635 @@
// This file is copied from frontend/src/components/file-uploader/file-uploader.js,
// and modified according to the requirements of this page.
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import Resumablejs from '@seafile/resumablejs';
import MD5 from 'MD5';
import { resumableUploadFileBlockSize, maxUploadFileSize, maxNumberOfFilesForFileupload } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import { gettext } from '../../utils/constants';
import UploadProgressDialog from './upload-progress-dialog';
import toaster from '../../components/toast';
import '../../css/file-uploader.css';
const propTypes = {
dragAndDrop: PropTypes.bool.isRequired,
token: PropTypes.string.isRequired,
repoID: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
filetypes: PropTypes.array,
chunkSize: PropTypes.number,
withCredentials: PropTypes.bool,
testMethod: PropTypes.string,
testChunks: PropTypes.number,
simultaneousUploads: PropTypes.number,
fileParameterName: PropTypes.string,
minFileSizeErrorCallback: PropTypes.func,
fileTypeErrorCallback: PropTypes.func,
onFileUploadSuccess: PropTypes.func.isRequired,
};
class FileUploader extends React.Component {
constructor(props) {
super(props);
this.state = {
retryFileList: [],
uploadFileList: [],
forbidUploadFileList: [],
totalProgress: 0,
isUploadProgressDialogShow: false,
currentResumableFile: null,
uploadBitrate: 0,
allFilesUploaded: false,
};
this.uploadInput = React.createRef();
this.notifiedFolders = [];
this.timestamp = null;
this.loaded = 0;
this.bitrateInterval = 500; // Interval in milliseconds to calculate the bitrate
this.isUploadLinkLoaded = false;
window.onbeforeunload = this.onbeforeunload;
}
componentDidMount() {
this.resumable = new Resumablejs({
target: '',
query: this.setQuery || {},
fileType: this.props.filetypes,
maxFiles: maxNumberOfFilesForFileupload || undefined,
maxFileSize: maxUploadFileSize * 1000 * 1000 || undefined,
testMethod: this.props.testMethod || 'post',
testChunks: this.props.testChunks || false,
headers: this.setHeaders || {},
withCredentials: this.props.withCredentials || false,
chunkSize: parseInt(resumableUploadFileBlockSize) * 1024 * 1024 || 1 * 1024 * 1024,
simultaneousUploads: this.props.simultaneousUploads || 1,
fileParameterName: this.props.fileParameterName,
generateUniqueIdentifier: this.generateUniqueIdentifier,
forceChunkSize: true,
maxChunkRetries: 3,
minFileSize: 0,
});
this.resumable.assignBrowse(this.uploadInput.current, true);
if (this.props.dragAndDrop) {
this.resumable.assignDrop(document.getElementById('upload-link-drop-zone'));
}
this.bindCallbackHandler();
this.bindEventHandler();
}
componentWillUnmount = () => {
window.onbeforeunload = null;
if (this.props.dragAndDrop === true) {
this.resumable.disableDropOnDocument();
}
}
onbeforeunload = () => {
if (window.uploader &&
window.uploader.isUploadProgressDialogShow &&
window.uploader.totalProgress !== 100) {
return '';
}
}
bindCallbackHandler = () => {
let { minFileSizeErrorCallback, fileTypeErrorCallback } = this.props;
if (this.maxFilesErrorCallback) {
this.resumable.opts.maxFilesErrorCallback = this.maxFilesErrorCallback;
}
if (minFileSizeErrorCallback) {
this.resumable.opts.minFileSizeErrorCallback = this.props.minFileSizeErrorCallback;
}
if (this.maxFileSizeErrorCallback) {
this.resumable.opts.maxFileSizeErrorCallback = this.maxFileSizeErrorCallback;
}
if (fileTypeErrorCallback) {
this.resumable.opts.fileTypeErrorCallback = this.props.fileTypeErrorCallback;
}
}
bindEventHandler = () => {
this.resumable.on('chunkingComplete', this.onChunkingComplete.bind(this));
this.resumable.on('fileAdded', this.onFileAdded.bind(this));
this.resumable.on('fileProgress', this.onFileProgress.bind(this));
this.resumable.on('fileSuccess', this.onFileUploadSuccess.bind(this));
this.resumable.on('progress', this.onProgress.bind(this));
this.resumable.on('complete', this.onComplete.bind(this));
this.resumable.on('fileError', this.onFileError.bind(this));
this.resumable.on('error', this.onError.bind(this));
this.resumable.on('dragstart', this.onDragStart.bind(this));
}
maxFilesErrorCallback = (files, errorCount) => {
let maxFiles = maxNumberOfFilesForFileupload;
let message = gettext('Please upload no more than {maxFiles} files at a time.');
message = message.replace('{maxFiles}', maxFiles);
toaster.danger(message);
}
maxFileSizeErrorCallback = (file) => {
let { forbidUploadFileList } = this.state;
forbidUploadFileList.push(file);
this.setState({forbidUploadFileList: forbidUploadFileList});
}
onChunkingComplete = (resumableFile) => {
let allFilesUploaded = this.state.allFilesUploaded;
if (allFilesUploaded === true) {
this.setState({allFilesUploaded: false});
}
let path = this.props.path;
let fileName = resumableFile.fileName;
let relativePath = resumableFile.relativePath;
let isFile = fileName === relativePath;
resumableFile.formData = {};
if (isFile) { // upload file
resumableFile.formData = {
parent_dir: path,
};
} else { // upload folder
let relative_path = relativePath.slice(0, relativePath.lastIndexOf('/') + 1);
resumableFile.formData = {
parent_dir: path,
relative_path: relative_path
};
}
}
onFileAdded = (resumableFile, files) => {
let isFile = resumableFile.fileName === resumableFile.relativePath;
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);
seafileAPI.sharedUploadLinkGetFileUploadUrl(this.props.token).then(res => {
this.resumable.opts.target = res.data.upload_link + '?ret-json=1';
this.resumableUpload(resumableFile);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
} else {
this.setUploadFileList(this.resumable.files);
if (!this.isUploadLinkLoaded) {
this.isUploadLinkLoaded = true;
seafileAPI.sharedUploadLinkGetFileUploadUrl(this.props.token).then(res => {
this.resumable.opts.target = res.data.upload_link + '?ret-json=1';
this.resumable.upload();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
}
}
resumableUpload = (resumableFile) => {
let { repoID, path } = this.props;
seafileAPI.getFileUploadedBytes(repoID, path, resumableFile.fileName).then(res => {
let uploadedBytes = res.data.uploadedBytes;
let blockSize = parseInt(resumableUploadFileBlockSize) * 1024 * 1024 || 1024 * 1024;
let offset = Math.floor(uploadedBytes / blockSize);
resumableFile.markChunksCompleted(offset);
this.resumable.upload();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
filesAddedComplete = (resumable, files) => {
let { forbidUploadFileList } = this.state;
if (forbidUploadFileList.length > 0 && files.length === 0) {
this.setState({
isUploadProgressDialogShow: true,
totalProgress: 100
});
}
}
setUploadFileList = () => {
let uploadFileList = this.resumable.files;
this.setState({
uploadFileList: uploadFileList,
isUploadProgressDialogShow: true,
});
Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', true);
}
onFileProgress = (resumableFile) => {
let uploadBitrate = this.getBitrate();
let uploadFileList = this.state.uploadFileList.map(item => {
if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) {
if (uploadBitrate) {
let lastSize = (item.size - (item.size * item.progress())) * 8;
let time = Math.floor(lastSize / uploadBitrate);
item.remainingTime = time;
}
}
return item;
});
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;
});
if (this.timestamp) {
let timeDiff = (now - this.timestamp);
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;
return uploadBitrate;
}
onProgress = () => {
let progress = Math.round(this.resumable.progress() * 100);
this.setState({totalProgress: progress});
Utils.registerGlobalVariable('uploader', 'totalProgress', progress);
}
onFileUploadSuccess = (resumableFile, message) => {
let formData = resumableFile.formData;
let currentTime = new Date().getTime()/1000;
message = formData.replace ? message : JSON.parse(message)[0];
if (formData.relative_path) { // upload folder
let relative_path = formData.relative_path;
let dir_name = relative_path.slice(0, relative_path.indexOf('/'));
let dirent = {
id: message.id,
name: dir_name,
type: 'dir',
mtime: currentTime,
};
// update folders cache
let isExist = this.notifiedFolders.some(item => {return item.name === dirent.name;});
if (!isExist) {
this.notifiedFolders.push(dirent);
this.props.onFileUploadSuccess(dirent);
}
// update uploadFileList
let uploadFileList = this.state.uploadFileList.map(item => {
if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) {
item.newFileName = relative_path + message.name;
item.isSaved = true;
}
return item;
});
this.setState({uploadFileList: uploadFileList});
return;
}
if (formData.replace) { // upload file -- replace exist file
let fileName = resumableFile.fileName;
let dirent = {
id: message,
name: fileName,
type: 'file',
mtime: currentTime
};
this.props.onFileUploadSuccess(dirent); // this contance: just one file
let uploadFileList = this.state.uploadFileList.map(item => {
if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) {
item.newFileName = fileName;
item.isSaved = true;
}
return item;
});
this.setState({uploadFileList: uploadFileList});
return;
}
// upload file -- add files
let dirent = {
id: message.id,
type: 'file',
name: message.name,
size: message.size,
mtime: currentTime,
};
this.props.onFileUploadSuccess(dirent);
let uploadFileList = this.state.uploadFileList.map(item => {
if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) {
item.newFileName = message.name;
item.isSaved = true;
}
return item;
});
this.setState({uploadFileList: uploadFileList});
}
onFileError = (resumableFile, message) => {
let error = '';
if (!message) {
error = gettext('Network error');
} else {
// eg: '{"error": "Internal error" \n }'
let errorMessage = message.replace(/\n/g, '');
errorMessage = JSON.parse(errorMessage);
error = errorMessage.error;
if (error === 'File locked by others.') {
error = gettext('File is locked by others.');
}
if (error === 'Internal error.') {
error = gettext('Internal Server Error');
}
}
let uploadFileList = this.state.uploadFileList.map(item => {
if (item.uniqueIdentifier === resumableFile.uniqueIdentifier) {
this.state.retryFileList.push(item);
item.error = error;
}
return item;
});
this.loaded = 0; // reset loaded data;
this.setState({
retryFileList: this.state.retryFileList,
uploadFileList: uploadFileList
});
}
onComplete = () => {
this.notifiedFolders = [];
// reset upload link loaded
this.isUploadLinkLoaded = false;
this.setState({allFilesUploaded: true});
}
onError = (message) => {
// reset upload link loaded
this.isUploadLinkLoaded = false;
// After the error, the user can switch windows
Utils.registerGlobalVariable('uploader', 'totalProgress', 100);
}
setHeaders = (resumableFile, resumable) => {
let offset = resumable.offset;
let chunkSize = resumable.getOpt('chunkSize');
let fileSize = resumableFile.size === 0 ? 1 : 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.current.removeAttribute('webkitdirectory');
this.uploadInput.current.click();
}
onFolderUpload = () => {
this.uploadInput.current.setAttribute('webkitdirectory', 'webkitdirectory');
this.uploadInput.current.click();
}
onDragStart = () => {
this.uploadInput.current.setAttribute('webkitdirectory', 'webkitdirectory');
}
onCloseUploadDialog = () => {
this.loaded = 0;
this.resumable.files = [];
// reset upload link loaded
this.isUploadLinkLoaded = false;
this.setState({isUploadProgressDialogShow: false, uploadFileList: [], forbidUploadFileList: []});
Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', false);
}
onUploadCancel = (uploadingItem) => {
let uploadFileList = this.state.uploadFileList.filter(item => {
if (item.uniqueIdentifier === uploadingItem.uniqueIdentifier) {
item.cancel(); // execute cancel function will delete the file at the same time
return false;
}
return true;
});
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 => {
if (Math.round(item.progress() !== 1)) {
item.cancel();
return false;
}
return true;
});
this.loaded = 0;
this.setState({
allFilesUploaded: true,
totalProgress: '100',
uploadFileList: uploadFileList
});
// reset upload link loaded
this.isUploadLinkLoaded = false;
}
onUploadRetry = (resumableFile) => {
seafileAPI.sharedUploadLinkGetFileUploadUrl(this.props.token).then(res => {
this.resumable.opts.target = res.data.upload_link + '?ret-json=1';
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;
this.retryUploadFile(item);
}
return item;
});
this.setState({
retryFileList: retryFileList,
uploadFileList: uploadFileList
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
retryUploadFile = (resumableFile) => {
let { repoID, path } = this.props;
let fileName = resumableFile.fileName;
let isFile = resumableFile.fileName === resumableFile.relativePath;
if (!isFile) {
let relative_path = resumableFile.formData.relative_path;
let prefix = path === '/' ? (path + relative_path) : (path + '/' + relative_path);
fileName = prefix + fileName;
}
resumableFile.bootstrap();
var firedRetry = false;
resumableFile.resumableObj.on('chunkingComplete', () => {
if(!firedRetry) {
seafileAPI.getFileUploadedBytes(repoID, path, fileName).then(res => {
let uploadedBytes = res.data.uploadedBytes;
let blockSize = parseInt(resumableUploadFileBlockSize) * 1024 * 1024 || 1024 * 1024;
let offset = Math.floor(uploadedBytes / blockSize);
resumableFile.markChunksCompleted(offset);
resumableFile.resumableObj.upload();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
firedRetry = true;
});
}
replaceRepetitionFile = () => {
let { repoID, path } = this.props;
seafileAPI.getUpdateLink(repoID, path).then(res => {
this.resumable.opts.target = res.data;
let resumableFile = this.resumable.files[this.resumable.files.length - 1];
resumableFile.formData['replace'] = 1;
resumableFile.formData['target_file'] = resumableFile.formData.parent_dir + resumableFile.fileName;
this.setUploadFileList(this.resumable.files);
this.resumable.upload();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
cancelFileUpload = () => {
this.resumable.files.pop(); //delete latest file
}
render() {
return (
<Fragment>
<div className="file-uploader-container">
<div className="file-uploader">
<input className="upload-input" type="file" ref={this.uploadInput} onClick={this.onClick} />
</div>
</div>
<UploadProgressDialog
retryFileList={this.state.retryFileList}
uploadFileList={this.state.uploadFileList}
forbidUploadFileList={this.state.forbidUploadFileList}
totalProgress={this.state.totalProgress}
uploadBitrate={this.state.uploadBitrate}
allFilesUploaded={this.state.allFilesUploaded}
onCloseUploadDialog={this.onCloseUploadDialog}
onCancelAllUploading={this.onCancelAllUploading}
onUploadCancel={this.onUploadCancel}
onUploadRetry={this.onUploadRetry}
onFileUpload={this.onFileUpload}
onFolderUpload={this.onFolderUpload}
/>
</Fragment>
);
}
}
FileUploader.propTypes = propTypes;
export default FileUploader;

View File

@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext, maxUploadFileSize } from '../../utils/constants';
const propTypes = {
file: PropTypes.object,
};
class ForbidUploadListItem extends React.Component {
render() {
let { file } = this.props;
let msg = gettext('Please upload files less than {placeholder}M').replace('{placeholder}', maxUploadFileSize);
return (
<tr className="file-upload-item">
<td className="upload-name">
<div className="ellipsis">{file.name}</div>
</td>
<td colSpan={3} className="error">{msg}</td>
</tr>
);
}
}
ForbidUploadListItem.propTypes = propTypes;
export default ForbidUploadListItem;

View File

@ -0,0 +1,72 @@
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import { Utils } from '../../utils/utils';
import { gettext } from '../../utils/constants';
import Logo from '../../components/logo';
import Account from '../../components/common/account';
import FileUploader from './file-uploader';
import '../../css/upload-link.css';
const loggedUser = window.app.pageOptions.username;
const {
dirName,
sharedBy,
noQuota,
maxUploadFileSize,
token,
repoID,
path
} = window.uploadLink;
class SharedUploadLink extends React.Component {
render() {
return (
<div className="h-100 d-flex flex-column">
<div className="top-header d-flex justify-content-between">
<Logo />
{loggedUser && <Account />}
</div>
<div className="o-auto">
<div className="py-4 px-6 mx-auto rounded" id="upload-link-panel">
<h3 className="h5" dangerouslySetInnerHTML={{__html: gettext('Upload files to {folder_name_placeholder}')
.replace('{folder_name_placeholder}', `<span class="op-target">${Utils.HTMLescape(dirName)}</span>`)}}></h3>
<p className="small shared-by" dangerouslySetInnerHTML={{__html: `${gettext('shared by:')} ${sharedBy.avatar} ${sharedBy.name}`}}></p>
{noQuota ? (
<div className="py-6 text-center">
<span className="sf3-font sf3-font-tips warning-icon"></span>
<p>{gettext('The owner of this library has run out of space.')}</p>
</div>
) : (
<Fragment>
<ol className="small text-gray">
<li className="tip-list-item">{gettext('Folder upload is limited to Chrome, Firefox 50+, and Microsoft Edge.')}</li>
{maxUploadFileSize && <li className="tip-list-item">{gettext('File size should be smaller than {max_size_placeholder}').replace('{max_size_placeholder}', maxUploadFileSize)}</li>}
</ol>
<div id="upload-link-drop-zone" className="text-center mt-2 mb-4">
<span className="sf3-font sf3-font-upload upload-icon"></span>
<p className="small text-gray mb-0">{gettext('Drag and drop files or folders here.')}</p>
</div>
<FileUploader
ref={uploader => this.uploader = uploader}
dragAndDrop={true}
token={token}
repoID={repoID}
path={path}
onFileUploadSuccess={() => {}}
/>
</Fragment>
)}
</div>
</div>
</div>
);
}
}
ReactDOM.render(
<SharedUploadLink />,
document.getElementById('wrapper')
);

View File

@ -0,0 +1,144 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
const propTypes = {
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.remainingTime === 0 && !resumableFile.isSaved) {
uploadState = UPLOAD_ISSAVING;
}
if (resumableFile.isSaved) {
uploadState = UPLOAD_UPLOADED;
}
}
this.setState({uploadState: uploadState});
}
onUploadCancel = (e) => {
e.preventDefault();
this.props.onUploadCancel(this.props.resumableFile);
}
onUploadRetry = (e) => {
e.preventDefault();
this.props.onUploadRetry(this.props.resumableFile);
}
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 { resumableFile } = this.props;
let progress = Math.round(resumableFile.progress() * 100);
let error = resumableFile.error;
return (
<tr className="file-upload-item">
<td className="upload-name">
<div className="ellipsis">{resumableFile.newFileName}</div>
</td>
<td>
<span className="file-size">{this.formatFileSize(resumableFile.size)}</span>
</td>
<td className="upload-progress">
{(this.state.uploadState === UPLOAD_UPLOADING || this.state.uploadState === UPLOAD_ISSAVING) &&
<Fragment>
{resumableFile.size >= (100 * 1000 * 1000) &&
<Fragment>
{resumableFile.isUploading() && (
<div className="progress-container">
<div className="progress">
<div className="progress-bar" role="progressbar" style={{width: `${progress}%`}} aria-valuenow={progress} aria-valuemin="0" aria-valuemax="100"></div>
</div>
{(resumableFile.remainingTime === -1) && <div className="progress-text">{gettext('Preparing to upload...')}</div>}
{(resumableFile.remainingTime > 0) && <div className="progress-text">{gettext('Remaining')}{' '}{Utils.formatTime(resumableFile.remainingTime)}</div>}
{(resumableFile.remainingTime === 0) && <div className="progress-text">{gettext('Indexing...')}</div>}
</div>
)}
{!resumableFile.isUploading() && (
<div className="progress-container d-flex align-items-center">
<div className="progress">
<div className="progress-bar" role="progressbar" style={{width: `${progress}%`}} aria-valuenow={progress} aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
)}
</Fragment>
}
{(resumableFile.size < (100 * 1000 * 1000)) &&
<div className="progress-container d-flex align-items-center">
<div className="progress">
<div className="progress-bar" role="progressbar" style={{width: `${progress}%`}} aria-valuenow={progress} aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
}
</Fragment>
}
{this.state.uploadState === UPLOAD_ERROR && (
<div className="message err-message ml-0" dangerouslySetInnerHTML={{__html: error}}></div>
)}
</td>
<td className="upload-operation">
<Fragment>
{this.state.uploadState === UPLOAD_UPLOADING && (
<a href="#" onClick={this.onUploadCancel}>{gettext('Cancel')}</a>
)}
{this.state.uploadState === UPLOAD_ERROR && (
<a href="#" onClick={this.onUploadRetry}>{gettext('Retry')}</a>
)}
{this.state.uploadState === UPLOAD_ISSAVING && (
<span className="saving">{gettext('Saving...')}</span>
)}
{this.state.uploadState === UPLOAD_UPLOADED && (
<span className="uploaded">{gettext('Uploaded')}</span>
)}
</Fragment>
</td>
</tr>
);
}
}
UploadListItem.propTypes = propTypes;
export default UploadListItem;

View File

@ -0,0 +1,87 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { gettext } from '../../utils/constants';
import UploadListItem from './upload-list-item';
import ForbidUploadListItem from './forbid-upload-list-item';
const propTypes = {
uploadFileList: PropTypes.array.isRequired,
forbidUploadFileList: PropTypes.array.isRequired,
onCancelAllUploading: PropTypes.func.isRequired,
onUploadCancel: PropTypes.func.isRequired,
onUploadRetry: PropTypes.func.isRequired,
onFileUpload: PropTypes.func.isRequired,
onFolderUpload: PropTypes.func.isRequired,
allFilesUploaded: PropTypes.bool.isRequired
};
class UploadProgressDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
dropdownOpen: false
};
}
toggleDropdown = () => {
this.setState({
dropdownOpen: !this.state.dropdownOpen
});
}
render() {
let { allFilesUploaded } = this.props;
return (
<Fragment>
<div className="text-center">
<ButtonDropdown isOpen={this.state.dropdownOpen} toggle={this.toggleDropdown}>
<DropdownToggle color="primary" caret>{gettext('Upload')}</DropdownToggle>
<DropdownMenu>
<DropdownItem onClick={this.props.onFileUpload}>{gettext('Upload Files')}</DropdownItem>
<DropdownItem onClick={this.props.onFolderUpload}>{gettext('Upload Folder')}</DropdownItem>
</DropdownMenu>
</ButtonDropdown>
<Button color="primary" outline={true} className="ml-4"
onClick={this.props.onCancelAllUploading}
disabled={allFilesUploaded}>
{gettext('Cancel All')}
</Button>
</div>
<div className="mt-4 mh-2">
<table className="table-thead-hidden">
<thead>
<tr>
<th width="35%">{gettext('name')}</th>
<th width="15%">{gettext('size')}</th>
<th width="35%">{gettext('progress')}</th>
<th width="15%">{gettext('state')}</th>
</tr>
</thead>
<tbody>
{this.props.forbidUploadFileList.map((file, index) => {
return (<ForbidUploadListItem key={index} file={file} />);
})}
{this.props.uploadFileList.map((resumableFile, index) => {
return (
<UploadListItem
key={index}
resumableFile={resumableFile}
onUploadCancel={this.props.onUploadCancel}
onUploadRetry={this.props.onUploadRetry}
/>
);
})
}
</tbody>
</table>
</div>
</Fragment>
);
}
}
UploadProgressDialog.propTypes = propTypes;
export default UploadProgressDialog;

View File

@ -2020,13 +2020,6 @@ a.sf-popover-item {
vertical-align:middle;
line-height:16px;
}
#upload-link-panel {
width:600px;
padding:10px 20px 20px;
}
#upload-link-panel .avatar {
vertical-align: middle;
}
/* user profile */
#user-profile {
width: 290px;

View File

@ -1,10 +1,10 @@
@font-face {font-family: "sf3-font";
src: url('iconfont.eot?t=1585125765434'); /* IE9 */
src: url('iconfont.eot?t=1585125765434#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'),
url('iconfont.woff?t=1585125765434') format('woff'),
url('iconfont.ttf?t=1585125765434') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('iconfont.svg?t=1585125765434#sf3-font') format('svg'); /* iOS 4.1- */
src: url('iconfont.eot?t=1593420657805'); /* IE9 */
src: url('iconfont.eot?t=1593420657805#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'),
url('iconfont.woff?t=1593420657805') format('woff'),
url('iconfont.ttf?t=1593420657805') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('iconfont.svg?t=1593420657805#sf3-font') format('svg'); /* iOS 4.1- */
}
.sf3-font {
@ -15,6 +15,14 @@
-moz-osx-font-smoothing: grayscale;
}
.sf3-font-tips:before {
content: "\e6d4";
}
.sf3-font-upload:before {
content: "\e6d3";
}
.sf3-font-logout:before {
content: "\e6bb";
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,12 @@ Created by iconfont
/>
<missing-glyph />
<glyph glyph-name="tips" unicode="&#59092;" d="M512-96C249.6-96 32 118.4 32 384S249.6 864 512 864s480-214.4 480-480-214.4-480-480-480zM480 659.2c-19.2 0-35.2-16-35.2-35.2v-275.2c0-19.2 16-35.2 35.2-35.2h67.2c19.2 0 35.2 16 35.2 35.2V624c0 19.2-16 35.2-35.2 35.2H480z m0-412.8c-19.2 0-35.2-16-35.2-35.2v-67.2c0-19.2 16-35.2 35.2-35.2h67.2c19.2 0 35.2 16 35.2 35.2v67.2c0 19.2-16 35.2-35.2 35.2H480z" horiz-adv-x="1024" />
<glyph glyph-name="upload" unicode="&#59091;" d="M249.6 19.2C108.8 32 0 140.8 0 256c0 147.2 102.4 256 240 272C256 672 371.2 768 512 768s256-96 272-240c137.6-16 240-124.8 240-272 0-115.2-108.8-224-249.6-236.8h-160c-12.8 0-22.4 9.6-22.4 22.4v160c0 12.8 9.6 22.4 22.4 22.4h86.4c22.4 0 32 16 16 35.2l-179.2 227.2c-16 19.2-38.4 19.2-54.4 0L304 256c-12.8-16-6.4-32 16-32h89.6c12.8 0 22.4-9.6 22.4-22.4v-160c0-12.8-9.6-22.4-22.4-22.4h-160z" horiz-adv-x="1024" />
<glyph glyph-name="logout" unicode="&#59067;" d="M572.8 249.6H310.4c-12.8 0-22.4 6.4-22.4 16v233.6c0 12.8 6.4 22.4 22.4 22.4H576V729.6c0 9.6 3.2 16 12.8 19.2 9.6 3.2 16 3.2 22.4-6.4l12.8-12.8L768 585.6l185.6-185.6c9.6-9.6 9.6-22.4 0-32l-265.6-265.6-76.8-76.8c-6.4-6.4-12.8-9.6-22.4-6.4-9.6 3.2-12.8 9.6-12.8 19.2v211.2h-3.2z m-160 496v-76.8H214.4c-41.6 0-73.6-28.8-73.6-70.4v-425.6c0-38.4 28.8-64 54.4-70.4h217.6v-76.8H204.8c-67.2 0-124.8 51.2-137.6 118.4 0 6.4-3.2 16-3.2 25.6V601.6C67.2 662.4 96 704 147.2 732.8c19.2 9.6 38.4 12.8 60.8 12.8h204.8z" horiz-adv-x="1024" />

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,27 @@
{% extends "base_for_react.html" %}
{% load seahub_tags i18n avatar_tags %}
{% load render_bundle from webpack_loader %}
{% block extra_style %}
{% render_bundle 'uploadLink' 'css' %}
{% endblock %}
{% block extra_script %}
<script type="text/javascript">
window.uploadLink = {
dirName: "{{dir_name|escapejs}}",
sharedBy: {
name: "{{username|email2nickname|escapejs}}",
avatar: '{% avatar username 16 %}'
},
noQuota: {% if no_quota %} true {% else %} false {% endif %},
{% if max_upload_file_size %}
maxUploadFileSize: "{{max_upload_file_size|filesizeformat}}",
{% endif %}
token: "{{uploadlink.token}}",
repoID: "{{uploadlink.repo_id}}",
path: "{{path|escapejs}}"
};
</script>
{% render_bundle 'uploadLink' 'js' %}
{% endblock %}

View File

@ -391,7 +391,8 @@ def view_shared_upload_link(request, uploadlink):
no_quota = True if seaserv.check_quota(repo_id) < 0 else False
return render(request, 'view_shared_upload_link.html', {
return render(request, 'view_shared_upload_link_react.html', {
#return render(request, 'view_shared_upload_link.html', {
'repo': repo,
'path': path,
'username': username,