Web: Files: added dropzone uploading, code refactoring

This commit is contained in:
Nikita Gopienko 2020-05-29 09:30:49 +03:00
parent cb98c4ccbb
commit 0599944585
6 changed files with 344 additions and 249 deletions

View File

@ -2,28 +2,17 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
import { MainButton, DropDownItem, toastr, utils } from "asc-web-components";
import { MainButton, DropDownItem } from "asc-web-components";
import { withTranslation, I18nextProvider } from "react-i18next";
import {
setAction,
fetchFiles,
setTreeFolders,
} from "../../../store/files/actions";
import { isCanCreate, loopTreeFolders } from "../../../store/files/selectors";
import store from "../../../store/store";
import { setAction } from "../../../store/files/actions";
import { isCanCreate } from "../../../store/files/selectors";
import i18n from "../i18n";
import { utils as commonUtils, constants, api } from "asc-web-common";
import { utils as commonUtils, constants } from "asc-web-common";
const { changeLanguage } = commonUtils;
const { FileAction } = constants;
class PureArticleMainButtonContent extends React.Component {
state = {
files: [],
uploadedFiles: 0,
totalSize: 0,
percent: 0
};
onCreate = (format) => {
this.props.setAction({
@ -34,199 +23,13 @@ class PureArticleMainButtonContent extends React.Component {
};
onUploadFileClick = () => this.inputFilesElement.click();
onUploadFolderClick = () => this.inputFolderElement.click();
updateFiles = () => {
const { onLoading, filter, currentFolderId, treeFolders, setTreeFolders } = this.props;
onLoading(true);
const newFilter = filter.clone();
fetchFiles(currentFolderId, newFilter, store.dispatch, treeFolders)
.then((data) => {
const path = data.selectedFolder.pathParts;
const newTreeFolders = treeFolders;
const folders = data.selectedFolder.folders;
const foldersCount = data.selectedFolder.foldersCount;
loopTreeFolders(path, newTreeFolders, folders, foldersCount);
setTreeFolders(newTreeFolders);
})
.catch((err) => toastr.error(err))
.finally(() => {
onLoading(false);
});
};
sendChunk = (files, location, requestsDataArray, isLatestFile, indexOfFile) => {
const sendRequestFunc = (index) => {
let newState = {};
api.files
.uploadFile(location, requestsDataArray[index])
.then((res) => {
let newPercent = this.state.percent;
const percent = (newPercent +=
(files[indexOfFile].size / this.state.totalSize) * 100);
if (res.data.data && res.data.data.uploaded) {
files[indexOfFile].uploaded = true;
newState = { files, percent };
}
if (index + 1 !== requestsDataArray.length) {
sendRequestFunc(index + 1);
} else if (isLatestFile) {
this.updateFiles();
newState = Object.assign({}, newState, {
uploadedFiles: this.state.uploadedFiles + 1,
});
return;
} else {
newState = Object.assign({}, newState, {
uploadedFiles: this.state.uploadedFiles + 1,
});
this.startSessionFunc(indexOfFile + 1);
}
})
.catch((err) => toastr.error(err))
.finally(() => {
if (
newState.hasOwnProperty("files") ||
newState.hasOwnProperty("percent") ||
newState.hasOwnProperty("uploadedFiles")
) {
let progressVisible = true;
let uploadedFiles = newState.uploadedFiles;
let percent = newState.percent;
if (newState.uploadedFiles === files.length) {
percent = 100;
newState.percent = 0;
newState.uploadedFiles = 0;
progressVisible = false;
}
this.setState(newState, () => {
this.props.setProgressValue(percent);
this.props.setProgressLabel(
this.props.t("UploadingLabel", {
file: uploadedFiles,
totalFiles: files.length,
})
);
if (!progressVisible) {
this.props.setProgressVisible(false);
}
});
}
});
};
sendRequestFunc(0);
};
startSessionFunc = indexOfFile => {
const { files } = this.state;
const { currentFolderId } = this.props;
const file = files[indexOfFile];
const isLatestFile = indexOfFile === files.length - 1;
const fileName = file.name;
const fileSize = file.size;
const relativePath = file.webkitRelativePath
? file.webkitRelativePath.slice(0, -file.name.length)
: "";
let location;
const requestsDataArray = [];
const chunkSize = 1024 * 1023; //~0.999mb
const chunks = Math.ceil(file.size / chunkSize, chunkSize);
let chunk = 0;
api.files
.startUploadSession(currentFolderId, fileName, fileSize, relativePath)
.then((res) => {
location = res.data.location;
while (chunk < chunks) {
const offset = chunk * chunkSize;
//console.log("current chunk..", chunk);
//console.log("file blob from offset...", offset);
//console.log(file.slice(offset, offset + chunkSize));
const formData = new FormData();
formData.append("file", file.slice(offset, offset + chunkSize));
requestsDataArray.push(formData);
chunk++;
}
})
.then(() =>
this.sendChunk(
files,
location,
requestsDataArray,
isLatestFile,
indexOfFile
)
)
.catch((err) => {
this.props.setProgressVisible(false, 0);
toastr.error(err);
});
};
onFileChange = (e) => {
const { t, setProgressVisible, setProgressLabel } = this.props;
const files = e.target.files;
//console.log("files", files);
const newFiles = [];
if(files) {
let total = 0;
for (let item of files) {
if (item.size !== 0) {
newFiles.push(item);
total += item.size;
} else {
toastr.error(t("ErrorUploadMessage"));
}
}
if (newFiles.length > 0) {
this.setState({ files: newFiles, totalSize: total }, () => {
setProgressVisible(true);
setProgressLabel(
this.props.t("UploadingLabel", {
file: 0,
totalFiles: newFiles.length,
})
);
this.startSessionFunc(0);
});
}
}
};
onInputClick = e => {
e.target.value = null;
}
onFileChange = (e) => this.props.startUpload(e.target.files);
onInputClick = e => e.target.value = null;
shouldComponentUpdate(nextProps, nextState) {
const { files, uploadedFiles, totalSize, percent } = this.state;
if (nextProps.isCanCreate !== this.props.isCanCreate) {
return true;
}
if (!utils.array.isArrayEqual(nextState.files, files)) {
return true;
}
if (nextState.uploadedFiles !== uploadedFiles) {
return true;
}
if (nextState.totalSize !== totalSize) {
return true;
}
if (nextState.percent !== percent) {
return true;
}
return false;
return nextProps.isCanCreate !== this.props.isCanCreate;
}
render() {
@ -309,18 +112,14 @@ ArticleMainButtonContent.propTypes = {
};
const mapStateToProps = (state) => {
const { selectedFolder, filter, treeFolders } = state.files;
const { settings, user } = state.auth;
const { selectedFolder } = state.files;
const { user } = state.auth;
return {
settings,
isCanCreate: isCanCreate(selectedFolder, user),
currentFolderId: selectedFolder.id,
filter,
treeFolders,
isCanCreate: isCanCreate(selectedFolder, user)
};
};
export default connect(mapStateToProps, { setAction, setTreeFolders })(
export default connect(mapStateToProps, { setAction })(
withRouter(ArticleMainButtonContent)
);

View File

@ -5,7 +5,5 @@
"NewPresentation": "New Presentation",
"NewFolder": "New Folder",
"UploadFiles": "Upload files",
"UploadFolder": "Upload folder",
"ErrorUploadMessage": "You cannot upload a folder or an empty file",
"UploadingLabel": "Uploading files: {{file}} of {{totalFiles}}"
"UploadFolder": "Upload folder"
}

View File

@ -5,7 +5,5 @@
"NewPresentation": "Новая презентация",
"NewFolder": "Новая папка",
"UploadFiles": "Загрузить файлы",
"UploadFolder": "Загрузить папку",
"ErrorUploadMessage": "Нельзя загрузить папку или пустой файл",
"UploadingLabel": "Загружено файлов: {{file}} из {{totalFiles}}"
"UploadFolder": "Загрузить папку"
}

View File

@ -3,7 +3,7 @@ import { connect } from "react-redux";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import { RequestLoader, Checkbox, toastr } from "asc-web-components";
import { PageLayout, utils } from "asc-web-common";
import { PageLayout, utils, api } from "asc-web-common";
import { withTranslation, I18nextProvider } from 'react-i18next';
import i18n from "./i18n";
@ -18,7 +18,9 @@ import {
SectionFilterContent,
SectionPagingContent
} from "./Section";
import { setSelected } from "../../../store/files/actions";
import { setSelected, fetchFiles, setTreeFolders } from "../../../store/files/actions";
import { loopTreeFolders } from "../../../store/files/selectors";
import store from "../../../store/store";
const { changeLanguage } = utils;
class PureHome extends React.Component {
@ -36,7 +38,12 @@ class PureHome extends React.Component {
progressBarLabel: "",
overwriteSetting: false,
uploadOriginalFormatSetting: false,
hideWindowSetting: false
hideWindowSetting: false,
files: [],
uploadedFiles: 0,
totalSize: 0,
percent: 0,
};
}
@ -45,8 +52,11 @@ class PureHome extends React.Component {
const headerVisible = selection.length > 0;
const headerIndeterminate =
headerVisible && selection.length > 0 && selection.length < files.length + folders.length;
const headerChecked = headerVisible && selection.length === files.length + folders.length;
headerVisible &&
selection.length > 0 &&
selection.length < files.length + folders.length;
const headerChecked =
headerVisible && selection.length === files.length + folders.length;
/*console.log(`renderGroupButtonMenu()
headerVisible=${headerVisible}
@ -71,17 +81,277 @@ class PureHome extends React.Component {
this.setState(newState);
};
//TODO: Refactor this block
updateFiles = () => {
const { filter, currentFolderId, treeFolders, setTreeFolders } = this.props;
this.onLoading(true);
const newFilter = filter.clone();
fetchFiles(currentFolderId, newFilter, store.dispatch, treeFolders)
.then((data) => {
const path = data.selectedFolder.pathParts;
const newTreeFolders = treeFolders;
const folders = data.selectedFolder.folders;
const foldersCount = data.selectedFolder.foldersCount;
loopTreeFolders(path, newTreeFolders, folders, foldersCount);
setTreeFolders(newTreeFolders);
})
.catch((err) => toastr.error(err))
.finally(() => {
this.onLoading(false);
});
};
sendChunk = (files, location, requestsDataArray, isLatestFile, indexOfFile) => {
const sendRequestFunc = (index) => {
let newState = {};
api.files
.uploadFile(location, requestsDataArray[index])
.then((res) => {
let newPercent = this.state.percent;
const percent = (newPercent +=
(files[indexOfFile].size / this.state.totalSize) * 100);
if (res.data.data && res.data.data.uploaded) {
files[indexOfFile].uploaded = true;
newState = { files, percent };
}
if (index + 1 !== requestsDataArray.length) {
sendRequestFunc(index + 1);
} else if (isLatestFile) {
this.updateFiles();
newState = Object.assign({}, newState, {
uploadedFiles: this.state.uploadedFiles + 1,
});
return;
} else {
newState = Object.assign({}, newState, {
uploadedFiles: this.state.uploadedFiles + 1,
});
this.startSessionFunc(indexOfFile + 1);
}
})
.catch((err) => toastr.error(err))
.finally(() => {
if (
newState.hasOwnProperty("files") ||
newState.hasOwnProperty("percent") ||
newState.hasOwnProperty("uploadedFiles")
) {
let progressVisible = true;
let uploadedFiles = newState.uploadedFiles;
let percent = newState.percent;
if (newState.uploadedFiles === files.length) {
percent = 100;
newState.percent = 0;
newState.uploadedFiles = 0;
progressVisible = false;
}
newState.progressBarValue = percent;
newState.progressBarLabel = this.props.t("UploadingLabel", {
file: uploadedFiles,
totalFiles: files.length,
});
this.setState(newState, () => {
if (!progressVisible) {
this.setProgressVisible(false);
}
});
}
});
};
sendRequestFunc(0);
};
//TODO: Refactor this block
startSessionFunc = (indexOfFile) => {
const { files } = this.state;
const { currentFolderId } = this.props;
const file = files[indexOfFile];
const isLatestFile = indexOfFile === files.length - 1;
const fileName = file.name;
const fileSize = file.size;
const relativePath = file.relativePath
? file.relativePath.slice(1, -file.name.length)
: file.webkitRelativePath
? file.webkitRelativePath.slice(0, -file.name.length)
: "";
let location;
const requestsDataArray = [];
const chunkSize = 1024 * 1023; //~0.999mb
const chunks = Math.ceil(file.size / chunkSize, chunkSize);
let chunk = 0;
api.files
.startUploadSession(currentFolderId, fileName, fileSize, relativePath)
.then((res) => {
location = res.data.location;
while (chunk < chunks) {
const offset = chunk * chunkSize;
//console.log("current chunk..", chunk);
//console.log("file blob from offset...", offset);
//console.log(file.slice(offset, offset + chunkSize));
const formData = new FormData();
formData.append("file", file.slice(offset, offset + chunkSize));
requestsDataArray.push(formData);
chunk++;
}
})
.then(
() => this.sendChunk(files, location, requestsDataArray, isLatestFile, indexOfFile)
)
.catch((err) => {
this.setProgressVisible(false, 0);
toastr.error(err);
});
};
onDrop = (e) => {
// ev.currentTarget.style.background = "lightyellow";
const items = e.dataTransfer.items;
let files = [];
const inSeries = (queue, callback) => {
let i = 0;
let length = queue.length;
if (!queue || !queue.length) {
callback();
}
const callNext = (i) => {
if (typeof queue[i] === "function") {
queue[i](() => i+1 < length ? callNext(i+1) : callback());
}
};
callNext(i);
};
const readDirEntry = (dirEntry, callback) => {
let entries = [];
const dirReader = dirEntry.createReader();
// keep quering recursively till no more entries
const getEntries = (func) => {
dirReader.readEntries((moreEntries) => {
if (moreEntries.length) {
entries = [...entries, ...moreEntries];
getEntries(func);
} else {
func();
}
});
};
getEntries(() => readEntries(entries, callback));
};
const readEntry = (entry, callback) => {
if (entry.isFile) {
entry.file(file => {
addFile(file, entry.fullPath);
callback();
});
} else if (entry.isDirectory) {
readDirEntry(entry, callback);
}
};
const readEntries = (entries, callback) => {
const queue = [];
loop(entries, (entry) => {
queue.push((func) => readEntry(entry, func));
});
inSeries(queue, () => callback());
};
const addFile = (file, relativePath) => {
file.relativePath = relativePath || "";
files.push(file);
};
const loop = (items, callback) => {
let length;
if (items) {
length = items.length;
// Loop array items
for (let i = 0; i < length; i++) {
callback(items[i], i);
}
}
};
const readItems = (items, func) => {
const entries = [];
loop(items, (item) => {
const entry = item.webkitGetAsEntry();
if (entry) {
if (entry.isFile) {
addFile(item.getAsFile(), entry.fullPath);
} else {
entries.push(entry);
}
}
});
if (entries.length) {
readEntries(entries, func);
} else {
func();
}
};
this.setState({ isLoading: true }, () =>
readItems(items, () => this.startUpload(files))
);
};
startUpload = files => {
const newFiles = [];
let total = 0;
for (let item of files) {
if (item.size !== 0) {
newFiles.push(item);
total += item.size;
} else {
toastr.error(this.props.t("ErrorUploadMessage"));
}
}
this.startUploadFiles(newFiles, total);
}
startUploadFiles = (files, totalSize) => {
if (files.length > 0) {
const progressBarLabel = this.props.t("UploadingLabel", {
file: 0,
totalFiles: files.length,
});
this.setState({ files, totalSize, progressBarLabel, showProgressBar: true, isLoading: true },
() => { this.startSessionFunc(0); });
} else if(this.state.isLoading) {
this.setState({ isLoading: false });
}
};
componentDidUpdate(prevProps) {
if (this.props.selection !== prevProps.selection) {
this.renderGroupButtonMenu();
}
}
onSectionHeaderContentCheck = checked => {
onSectionHeaderContentCheck = (checked) => {
this.props.setSelected(checked ? "all" : "none");
};
onSectionHeaderContentSelect = selected => {
onSectionHeaderContentSelect = (selected) => {
this.props.setSelected(selected);
};
@ -96,32 +366,43 @@ class PureHome extends React.Component {
}
};
onLoading = status => {
onLoading = (status) => {
this.setState({ isLoading: status });
};
setProgressVisible = (visible, timeout) => {
const newTimeout = timeout ? timeout : 10000;
if(visible) {this.setState({ showProgressBar: visible })}
else { setTimeout(() => this.setState({ showProgressBar: visible, progressBarValue: 0 }), newTimeout)};
if (visible) {
this.setState({ showProgressBar: visible });
} else {
setTimeout(
() => this.setState({ showProgressBar: visible, progressBarValue: 0 }),
newTimeout
);
}
};
setProgressValue = value => this.setState({ progressBarValue: value });
setProgressLabel = label => this.setState({ progressBarLabel: label });
setProgressValue = (value) => this.setState({ progressBarValue: value });
setProgressLabel = (label) => this.setState({ progressBarLabel: label });
onChangeOverwrite = () => this.setState({overwriteSetting: !this.state.overwriteSetting})
onChangeOriginalFormat = () => this.setState({uploadOriginalFormatSetting: !this.state.uploadOriginalFormatSetting})
onChangeWindowVisible = () => this.setState({hideWindowSetting: !this.state.hideWindowSetting})
onChangeOverwrite = () =>
this.setState({ overwriteSetting: !this.state.overwriteSetting });
onChangeOriginalFormat = () =>
this.setState({
uploadOriginalFormatSetting: !this.state.uploadOriginalFormatSetting,
});
onChangeWindowVisible = () =>
this.setState({ hideWindowSetting: !this.state.hideWindowSetting });
startFilesOperations = progressBarLabel => {
this.setState({ isLoading: true, progressBarLabel, showProgressBar: true })
}
startFilesOperations = (progressBarLabel) => {
this.setState({ isLoading: true, progressBarLabel, showProgressBar: true });
};
finishFilesOperations = err => {
finishFilesOperations = (err) => {
const timeout = err ? 0 : null;
err && toastr.error(err);
this.onLoading(false);
this.setProgressVisible(false, timeout);
}
};
render() {
const {
@ -135,7 +416,7 @@ class PureHome extends React.Component {
progressBarLabel,
overwriteSetting,
uploadOriginalFormatSetting,
hideWindowSetting
hideWindowSetting,
} = this.state;
const { t } = this.props;
@ -164,15 +445,17 @@ class PureHome extends React.Component {
<RequestLoader
visible={isLoading}
zIndex={256}
loaderSize='16px'
loaderSize="16px"
loaderColor={"#999"}
label={`${t('LoadingProcessing')} ${t('LoadingDescription')}`}
fontSize='12px'
label={`${t("LoadingProcessing")} ${t("LoadingDescription")}`}
fontSize="12px"
fontColor={"#999"}
/>
<PageLayout
withBodyScroll
withBodyAutoFocus
uploadFiles
onDrop={this.onDrop}
showProgressBar={showProgressBar}
progressBarValue={progressBarValue}
progressBarDropDownContent={progressBarContent}
@ -181,11 +464,18 @@ class PureHome extends React.Component {
articleMainButtonContent={
<ArticleMainButtonContent
onLoading={this.onLoading}
startUpload={this.startUpload}
setProgressVisible={this.setProgressVisible}
setProgressValue={this.setProgressValue}
setProgressLabel={this.setProgressLabel}
/>}
articleBodyContent={<ArticleBodyContent onLoading={this.onLoading} isLoading={isLoading} />}
/>
}
articleBodyContent={
<ArticleBodyContent
onLoading={this.onLoading}
isLoading={isLoading}
/>
}
sectionHeaderContent={
<SectionHeaderContent
isHeaderVisible={isHeaderVisible}
@ -201,7 +491,9 @@ class PureHome extends React.Component {
finishFilesOperations={this.finishFilesOperations}
/>
}
sectionFilterContent={<SectionFilterContent onLoading={this.onLoading} />}
sectionFilterContent={
<SectionFilterContent onLoading={this.onLoading} />
}
sectionBodyContent={
<SectionBodyContent
selected={selected}
@ -241,11 +533,15 @@ function mapStateToProps(state) {
folders: state.files.folders,
selection: state.files.selection,
selected: state.files.selected,
isLoaded: state.auth.isLoaded
isLoaded: state.auth.isLoaded,
currentFolderId: state.files.selectedFolder.id,
filter: state.files.filter,
treeFolders: state.files.treeFolders
};
}
export default connect(
mapStateToProps,
{ setSelected }
{ setSelected, setTreeFolders }
)(withRouter(Home));

View File

@ -74,5 +74,7 @@
"HideWindowSetting": "Show this window minimized",
"ArchivingData": "Archiving data",
"DeleteOperation": "Deleting",
"Restore": "Restore"
"Restore": "Restore",
"ErrorUploadMessage": "You cannot upload a folder or an empty file",
"UploadingLabel": "Uploading files: {{file}} of {{totalFiles}}"
}

View File

@ -74,5 +74,7 @@
"HideWindowSetting": "Показывать это окно минимизированным",
"ArchivingData": "Архивирование данных",
"DeleteOperation": "Удаление",
"Restore": "Восстановить"
"Restore": "Восстановить",
"ErrorUploadMessage": "Нельзя загрузить папку или пустой файл",
"UploadingLabel": "Загружено файлов: {{file}} из {{totalFiles}}"
}