diff --git a/client/modules/IDE/actions/uploader.js b/client/modules/IDE/actions/uploader.js index 5a300179f1..e2831df75f 100644 --- a/client/modules/IDE/actions/uploader.js +++ b/client/modules/IDE/actions/uploader.js @@ -1,126 +1,68 @@ +import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils'; import apiClient from '../../../utils/apiClient'; import getConfig from '../../../utils/getConfig'; import { handleCreateFile } from './files'; -import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils'; -const s3BucketHttps = +export const s3BucketHttps = getConfig('S3_BUCKET_URL_BASE') || `https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig( 'S3_BUCKET' )}/`; const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB -function localIntercept(file, options = {}) { - return new Promise((resolve, reject) => { - if (!options.readType) { - // const mime = file.type; - // const textType = a(_textTypes).any(type => { - // const re = new RegExp(type); - // return re.test(mime); - // }); - // options.readType = textType ? 'readAsText' : 'readAsDataURL'; - options.readType = 'readAsText'; // eslint-disable-line - } - const reader = new window.FileReader(); - reader.onload = () => { - resolve(reader.result); - }; - reader.onerror = () => { - reject(reader.result); - }; - - // run the reader - reader[options.readType](file); - }); -} - -function toBinary(string) { - const codeUnits = new Uint16Array(string.length); - for (let i = 0; i < codeUnits.length; i += 1) { - codeUnits[i] = string.charCodeAt(i); - } - return String.fromCharCode(...new Uint8Array(codeUnits.buffer)); +function isS3Upload(file) { + return !TEXT_FILE_REGEX.test(file.name) || file.size >= MAX_LOCAL_FILE_SIZE; } -export function dropzoneAcceptCallback(userId, file, done) { - return () => { - // if a user would want to edit this file as text, local interceptor - if (file.name.match(TEXT_FILE_REGEX) && file.size < MAX_LOCAL_FILE_SIZE) { - localIntercept(file) - .then((result) => { - file.content = result; // eslint-disable-line - done('Uploading plaintext file locally.'); - file.previewElement.classList.remove('dz-error'); - file.previewElement.classList.add('dz-success'); - file.previewElement.classList.add('dz-processing'); - file.previewElement.querySelector('.dz-upload').style.width = '100%'; - }) - .catch((result) => { - done(`Failed to download file ${file.name}: ${result}`); - console.warn(file); - }); - } else { - file.postData = []; // eslint-disable-line - apiClient - .post('/S3/sign', { - name: file.name, - type: file.type, - size: file.size, - userId - // _csrf: document.getElementById('__createPostToken').value - }) - .then((response) => { - file.custom_status = 'ready'; // eslint-disable-line - file.postData = response.data; // eslint-disable-line - file.s3 = response.data.key; // eslint-disable-line - file.previewTemplate.className += ' uploading'; // eslint-disable-line - done(); - }) - .catch((error) => { - const { response } = error; - file.custom_status = 'rejected'; // eslint-disable-line - if ( - response.data && - response.data.responseText && - response.data.responseText.message - ) { - done(response.data.responseText.message); - } - done('Error: Reached upload limit.'); - }); +export async function dropzoneAcceptCallback(userId, file, done) { + // if a user would want to edit this file as text, local interceptor + if (!isS3Upload(file)) { + try { + // eslint-disable-next-line no-param-reassign + file.content = await file.text(); + // Make it an error so that it won't be sent to S3, but style as a success. + done('Uploading plaintext file locally.'); + file.previewElement.classList.remove('dz-error'); + file.previewElement.classList.add('dz-success'); + file.previewElement.classList.add('dz-processing'); + file.previewElement.querySelector('.dz-upload').style.width = '100%'; + } catch (error) { + done(`Failed to download file ${file.name}: ${error}`); + console.warn(file); } - }; + } else { + try { + const response = await apiClient.post('/S3/sign', { + name: file.name, + type: file.type, + size: file.size, + userId + // _csrf: document.getElementById('__createPostToken').value + }); + // eslint-disable-next-line no-param-reassign + file.postData = response.data; + done(); + } catch (error) { + done( + error?.response?.data?.responseText?.message || + error?.message || + 'Error: Reached upload limit.' + ); + } + } } export function dropzoneSendingCallback(file, xhr, formData) { - return () => { - if (!file.name.match(TEXT_FILE_REGEX) || file.size >= MAX_LOCAL_FILE_SIZE) { - Object.keys(file.postData).forEach((key) => { - formData.append(key, file.postData[key]); - }); - } - }; + if (isS3Upload(file)) { + Object.keys(file.postData).forEach((key) => { + formData.append(key, file.postData[key]); + }); + } } export function dropzoneCompleteCallback(file) { - return (dispatch) => { // eslint-disable-line - if ( - (!file.name.match(TEXT_FILE_REGEX) || file.size >= MAX_LOCAL_FILE_SIZE) && - file.status !== 'error' - ) { - let inputHidden = '`; - document.getElementById('uploader').innerHTML += inputHidden; - + return (dispatch) => { + if (isS3Upload(file) && file.postData && file.status !== 'error') { const formParams = { name: file.name, url: `${s3BucketHttps}${file.postData.key}` diff --git a/client/modules/IDE/components/FileUploader.jsx b/client/modules/IDE/components/FileUploader.jsx index ac0014f987..8b2587aa9e 100644 --- a/client/modules/IDE/components/FileUploader.jsx +++ b/client/modules/IDE/components/FileUploader.jsx @@ -1,34 +1,42 @@ -import PropTypes from 'prop-types'; -import React from 'react'; import Dropzone from 'dropzone'; -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import { withTranslation } from 'react-i18next'; -import * as UploaderActions from '../actions/uploader'; -import getConfig from '../../../utils/getConfig'; +import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils'; +import { remSize } from '../../../theme'; +import { + dropzoneAcceptCallback, + dropzoneCompleteCallback, + dropzoneSendingCallback, + s3BucketHttps +} from '../actions/uploader'; -const s3Bucket = - getConfig('S3_BUCKET_URL_BASE') || - `https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig( - 'S3_BUCKET' - )}/`; +Dropzone.autoDiscover = false; -class FileUploader extends React.Component { - componentDidMount() { - this.createDropzone(); - Dropzone.autoDiscover = false; +// TODO: theming for dark vs. light theme +// TODO: include color and background-color settings after migrating the themify variables. +const StyledUploader = styled.div` + min-height: ${remSize(200)}; + width: 100%; + text-align: center; + .dz-preview.dz-image-preview { + background-color: transparent; } +`; - createDropzone() { - const userId = this.props.project.owner - ? this.props.project.owner.id - : this.props.user.id; - this.uploader = new Dropzone('div#uploader', { - url: s3Bucket, +function FileUploader() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const userId = useSelector((state) => state.user.id); + + useEffect(() => { + const uploader = new Dropzone('div#uploader', { + url: s3BucketHttps, method: 'post', autoProcessQueue: true, clickable: true, + hiddenInputContainer: '#hidden-input-container', maxFiles: 6, parallelUploads: 2, maxFilesize: 5, // in mb @@ -36,59 +44,26 @@ class FileUploader extends React.Component { thumbnailWidth: 200, thumbnailHeight: 200, acceptedFiles: fileExtensionsAndMimeTypes, - dictDefaultMessage: this.props.t('FileUploader.DictDefaultMessage'), - accept: this.props.dropzoneAcceptCallback.bind(this, userId), - sending: this.props.dropzoneSendingCallback, - complete: this.props.dropzoneCompleteCallback - // error: (file, errorMessage) => { - // console.log(file); - // console.log(errorMessage); - // } + dictDefaultMessage: t('FileUploader.DictDefaultMessage'), + accept: (file, done) => { + dropzoneAcceptCallback(userId, file, done); + }, + sending: dropzoneSendingCallback }); - } - - render() { - return
; - } -} - -FileUploader.propTypes = { - dropzoneAcceptCallback: PropTypes.func.isRequired, - dropzoneSendingCallback: PropTypes.func.isRequired, - dropzoneCompleteCallback: PropTypes.func.isRequired, - project: PropTypes.shape({ - owner: PropTypes.shape({ - id: PropTypes.string - }) - }), - user: PropTypes.shape({ - id: PropTypes.string - }), - t: PropTypes.func.isRequired -}; - -FileUploader.defaultProps = { - project: { - id: undefined, - owner: undefined - }, - user: { - id: undefined - } -}; - -function mapStateToProps(state) { - return { - files: state.files, - project: state.project, - user: state.user - }; -} + uploader.on('complete', (file) => { + dispatch(dropzoneCompleteCallback(file)); + }); + return () => { + uploader.destroy(); + }; + }, [userId, t, dispatch]); -function mapDispatchToProps(dispatch) { - return bindActionCreators(UploaderActions, dispatch); + return ( +