From 059994458527992cc002fadd1c25786bc091b961 Mon Sep 17 00:00:00 2001 From: gopienkonikita Date: Fri, 29 May 2020 09:30:49 +0300 Subject: [PATCH] Web: Files: added dropzone uploading, code refactoring --- .../components/Article/MainButton/index.js | 223 +---------- .../Article/locales/en/translation.json | 4 +- .../Article/locales/ru/translation.json | 4 +- .../Client/src/components/pages/Home/index.js | 354 ++++++++++++++++-- .../pages/Home/locales/en/translation.json | 4 +- .../pages/Home/locales/ru/translation.json | 4 +- 6 files changed, 344 insertions(+), 249 deletions(-) diff --git a/products/ASC.Files/Client/src/components/Article/MainButton/index.js b/products/ASC.Files/Client/src/components/Article/MainButton/index.js index 5df0379d01..bf42260f37 100644 --- a/products/ASC.Files/Client/src/components/Article/MainButton/index.js +++ b/products/ASC.Files/Client/src/components/Article/MainButton/index.js @@ -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) ); diff --git a/products/ASC.Files/Client/src/components/Article/locales/en/translation.json b/products/ASC.Files/Client/src/components/Article/locales/en/translation.json index 0951c09681..ded5c5926d 100644 --- a/products/ASC.Files/Client/src/components/Article/locales/en/translation.json +++ b/products/ASC.Files/Client/src/components/Article/locales/en/translation.json @@ -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" } \ No newline at end of file diff --git a/products/ASC.Files/Client/src/components/Article/locales/ru/translation.json b/products/ASC.Files/Client/src/components/Article/locales/ru/translation.json index 4a47c51254..a0f30eb426 100644 --- a/products/ASC.Files/Client/src/components/Article/locales/ru/translation.json +++ b/products/ASC.Files/Client/src/components/Article/locales/ru/translation.json @@ -5,7 +5,5 @@ "NewPresentation": "Новая презентация", "NewFolder": "Новая папка", "UploadFiles": "Загрузить файлы", - "UploadFolder": "Загрузить папку", - "ErrorUploadMessage": "Нельзя загрузить папку или пустой файл", - "UploadingLabel": "Загружено файлов: {{file}} из {{totalFiles}}" + "UploadFolder": "Загрузить папку" } \ No newline at end of file diff --git a/products/ASC.Files/Client/src/components/pages/Home/index.js b/products/ASC.Files/Client/src/components/pages/Home/index.js index 2b4128fbdb..57cf62dfa0 100644 --- a/products/ASC.Files/Client/src/components/pages/Home/index.js +++ b/products/ASC.Files/Client/src/components/pages/Home/index.js @@ -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 { } - articleBodyContent={} + /> + } + articleBodyContent={ + + } sectionHeaderContent={ } - sectionFilterContent={} + sectionFilterContent={ + + } sectionBodyContent={