1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-02 07:27:04 +00:00

Share link file upload (#4633)

* [share dialog] fixed permission bugs; added 'download and upload'
permission

* share link: fixed permission bug for library
* upload link: fixed permission bug
* share link: added 'download and upload' permission for folder

* [folder share link] added 'file upload' for folder share link with 'can_upload' permission
This commit is contained in:
llj
2020-10-20 16:24:58 +08:00
committed by GitHub
parent 25cee90eda
commit 7000c1f364
11 changed files with 1107 additions and 52 deletions

View File

@@ -83,8 +83,9 @@ class GenerateShareLink extends React.Component {
});
if (isPro) {
if (this.props.itemType === 'library') {
let permissionOptions = Utils.getShareLinkPermissionList(this.props.itemType, '', path);
const { itemType, userPerm } = this.props;
if (itemType == 'library') {
let permissionOptions = Utils.getShareLinkPermissionList(itemType, userPerm, path);
this.setState({
permissionOptions: permissionOptions,
currentPermission: permissionOptions[0],

View File

@@ -54,7 +54,7 @@ class ShareDialog extends React.Component {
getInitialActiveTab = () => {
let { repoEncrypted, userPerm, enableDirPrivateShare, itemType } = this.props;
const enableShareLink = !repoEncrypted && canGenerateShareLink;
const enableUploadLink = !repoEncrypted && canGenerateUploadLink && userPerm == 'rw';
const enableUploadLink = !repoEncrypted && canGenerateUploadLink && (userPerm == 'rw' || userPerm == 'admin');
// for encrypted repo, 'dir private share' is only enabled for the repo itself,
// not for the folders in it.
@@ -88,7 +88,7 @@ class ShareDialog extends React.Component {
let activeTab = this.state.activeTab;
let { repoEncrypted, userPerm, enableDirPrivateShare, itemType } = this.props;
const enableShareLink = !repoEncrypted && canGenerateShareLink;
const enableUploadLink = !repoEncrypted && canGenerateUploadLink && userPerm == 'rw';
const enableUploadLink = !repoEncrypted && canGenerateUploadLink && (userPerm == 'rw' || userPerm == 'admin');
// for encrypted repo, 'dir private share' is only enabled for the repo itself,
// not for the folders in it.
@@ -160,6 +160,7 @@ class ShareDialog extends React.Component {
repoID={this.props.repoID}
closeShareDialog={this.props.toggleDialog}
itemType={itemType}
userPerm={userPerm}
/>
</TabPane>
}

View File

@@ -0,0 +1,700 @@
// the parent folder of this file is copied from components/file-uploader
// this file is modified according to the requirements of 'file upload' in 'shared dir link'
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 '../toast';
import '../../css/file-uploader.css';
const propTypes = {
token: PropTypes.string.isRequired,
repoID: PropTypes.string.isRequired,
//direntList: PropTypes.array.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,
dragAndDrop: PropTypes.bool.isRequired,
path: PropTypes.string.isRequired,
onFileUploadSuccess: PropTypes.func.isRequired
};
class FileUploader extends React.Component {
constructor(props) {
super(props);
this.state = {
retryFileList: [],
uploadFileList: [],
forbidUploadFileList: [],
totalProgress: 0,
isUploadProgressDialogShow: false,
isUploadRemindDialogShow: 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
window.onbeforeunload = this.onbeforeunload;
this.isUploadLinkLoaded = false;
}
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);
//Enable or Disable DragAnd Drop
if (this.props.dragAndDrop === true) {
this.resumable.enableDropOnDocument();
}
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('filesAddedComplete', this.filesAddedComplete.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('pause', this.onPause.bind(this));
this.resumable.on('fileRetry', this.onFileRetry.bind(this));
this.resumable.on('fileError', this.onFileError.bind(this));
this.resumable.on('error', this.onError.bind(this));
this.resumable.on('beforeCancel', this.onBeforeCancel.bind(this));
this.resumable.on('cancel', this.onCancel.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});
}
//get parent_dir relative_path
let path = this.props.path === '/' ? '/' : this.props.path + '/';
let fileName = resumableFile.fileName;
let relativePath = resumableFile.relativePath;
let isFile = fileName === relativePath;
//update formdata
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;
// 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);
let { token } = this.props;
seafileAPI.sharedLinkGetFileUploadUrl(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;
let { token } = this.props;
seafileAPI.sharedLinkGetFileUploadUrl(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
// 'upload folder' is not supported
/*
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;
*/
}
// replacing file is not allowed in shared link with 'can_upload' permission
/*
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); // this contance: no repetition file
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});
}
onPause = () => {
}
onError = (message) => {
// reset upload link loaded
this.isUploadLinkLoaded = false;
// After the error, the user can switch windows
Utils.registerGlobalVariable('uploader', 'totalProgress', 100);
}
onFileRetry = () => {
// todo, cancel upload file, uploded again;
}
onBeforeCancel = () => {
// todo, giving a pop message ?
}
onCancel = () => {
}
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) => {
let { token } = this.props;
seafileAPI.sharedLinkGetFileUploadUrl(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);
});
}
onUploadRetryAll = () => {
let { token } = this.props;
seafileAPI.sharedLinkGetFileUploadUrl(token).then(res => {
this.resumable.opts.target = res.data.upload_link + '?ret-json=1';
this.state.retryFileList.forEach(item => {
item.error = false;
this.retryUploadFile(item);
});
let uploadFileList = this.state.uploadFileList.slice(0);
this.setState({
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;
});
}
uploadFile = () => {
let resumableFile = this.resumable.files[this.resumable.files.length - 1];
let { token } = this.props;
seafileAPI.sharedLinkGetFileUploadUrl(token).then(res => {
this.resumable.opts.target = res.data.upload_link + '?ret-json=1';
this.setState({
isUploadRemindDialogShow: false,
isUploadProgressDialogShow: true,
uploadFileList: [...this.state.uploadFileList, resumableFile]
}, () => {
this.resumable.upload();
});
Utils.registerGlobalVariable('uploader', 'isUploadProgressDialogShow', true);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
cancelFileUpload = () => {
this.resumable.files.pop(); //delete latest file
this.setState({isUploadRemindDialogShow: false});
}
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>
{this.state.isUploadProgressDialogShow &&
<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}
onUploadRetryAll={this.onUploadRetryAll}
/>
}
</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,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,126 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import UploadListItem from './upload-list-item';
import ForbidUploadListItem from './forbid-upload-list-item';
const propTypes = {
uploadBitrate: PropTypes.number.isRequired,
totalProgress: PropTypes.number.isRequired,
retryFileList: PropTypes.array.isRequired,
uploadFileList: PropTypes.array.isRequired,
forbidUploadFileList: 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,
};
class UploadProgressDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
isMinimized: false
};
}
onCancelAllUploading = () => {
this.props.onCancelAllUploading();
}
onMinimizeUpload = (e) => {
e.nativeEvent.stopImmediatePropagation();
this.setState({isMinimized: !this.state.isMinimized});
}
onCloseUpload = (e) => {
e.nativeEvent.stopImmediatePropagation();
this.props.onCloseUploadDialog();
}
render() {
let uploadBitrate = Utils.formatBitRate(this.props.uploadBitrate);
let uploadedMessage = gettext('File Upload');
let uploadingMessage = gettext('File Uploading...') + ' ' + this.props.totalProgress + '%' + ' (' + uploadBitrate + ')';
let uploadingOptions = (<span className="sf2-icon-minus" onClick={this.onMinimizeUpload}></span>);
let uploadedOptions = (
<Fragment>
<span className="sf2-icon-minus" onClick={this.onMinimizeUpload}></span>
<span className="sf2-icon-x1" onClick={this.onCloseUpload}></span>
</Fragment>
);
let { totalProgress, allFilesUploaded, retryFileList } = this.props;
return (
<div className="uploader-list-view" style={{height: this.state.isMinimized ? '2.25rem' : '20rem'}}>
<div className="uploader-list-header">
<div className="title">
{totalProgress === 100 ? uploadedMessage : uploadingMessage}
</div>
<div className="uploader-options">
{totalProgress === 100 || allFilesUploaded ? uploadedOptions : uploadingOptions}
</div>
</div>
<div className="uploader-list-content">
<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>
<tr>
<td className="text-right" colSpan={3}>
{retryFileList.length > 0 ?
<span className="cursor-pointer" onClick={this.props.onUploadRetryAll}>{gettext('Retry All')}</span>
:
<span className="cursor-pointer disabled-link">{gettext('Retry All')}</span>
}
</td>
<td className="text-right" colSpan={1}>
{!allFilesUploaded ?
<span className="cursor-pointer" onClick={this.onCancelAllUploading}>{gettext('Cancel All')}</span>
:
<span className="cursor-pointer disabled-link" >{gettext('Cancel All')}</span>
}
</td>
</tr>
{
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>
</div>
);
}
}
UploadProgressDialog.propTypes = propTypes;
export default UploadProgressDialog;