mirror of
https://github.com/ascribe/onion.git
synced 2024-12-22 09:23:13 +01:00
Merge pull request #35 from ascribe/AD-1360-show-common-upload-errors-to-user
Show more detailed upload errors and suggest solutions
This commit is contained in:
commit
4c821bd744
13
js/actions/error_queue_actions.js
Normal file
13
js/actions/error_queue_actions.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { alt } from '../alt';
|
||||||
|
|
||||||
|
class ErrorQueueActions {
|
||||||
|
constructor() {
|
||||||
|
this.generateActions(
|
||||||
|
'shiftErrorQueue'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default alt.createActions(ErrorQueueActions);
|
@ -14,19 +14,24 @@ import { getCookie } from '../../utils/fetch_api_utils';
|
|||||||
import { getLangText } from '../../utils/lang_utils';
|
import { getLangText } from '../../utils/lang_utils';
|
||||||
|
|
||||||
|
|
||||||
|
const { func, bool, number, object, string, arrayOf } = React.PropTypes;
|
||||||
|
|
||||||
let FurtherDetailsFileuploader = React.createClass({
|
let FurtherDetailsFileuploader = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
pieceId: React.PropTypes.number.isRequired,
|
pieceId: number.isRequired,
|
||||||
|
|
||||||
areAssetsDownloadable: React.PropTypes.bool,
|
editable: bool,
|
||||||
editable: React.PropTypes.bool,
|
label: string,
|
||||||
isReadyForFormSubmission: React.PropTypes.func,
|
otherData: arrayOf(object),
|
||||||
label: React.PropTypes.string,
|
|
||||||
multiple: React.PropTypes.bool,
|
// Props for ReactS3FineUploader
|
||||||
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
|
areAssetsDownloadable: bool,
|
||||||
onValidationFailed: React.PropTypes.func,
|
isReadyForFormSubmission: func,
|
||||||
setIsUploadReady: React.PropTypes.func,
|
submitFile: func, // TODO: rename to onSubmitFile
|
||||||
submitFile: React.PropTypes.func,
|
onValidationFailed: func,
|
||||||
|
multiple: bool,
|
||||||
|
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
|
||||||
|
showErrorPrompt: bool,
|
||||||
validation: ReactS3FineUploader.propTypes.validation
|
validation: ReactS3FineUploader.propTypes.validation
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -40,36 +45,57 @@ let FurtherDetailsFileuploader = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
editable,
|
||||||
|
isReadyForFormSubmission,
|
||||||
|
multiple,
|
||||||
|
onValidationFailed,
|
||||||
|
otherData,
|
||||||
|
pieceId,
|
||||||
|
setIsUploadReady,
|
||||||
|
showErrorPrompt,
|
||||||
|
submitFile,
|
||||||
|
validation } = this.props;
|
||||||
|
|
||||||
// Essentially there a three cases important to the fileuploader
|
// Essentially there a three cases important to the fileuploader
|
||||||
//
|
//
|
||||||
// 1. there is no other_data => do not show the fileuploader at all (where otherData is now an array)
|
// 1. there is no other_data => do not show the fileuploader at all (where otherData is now an array)
|
||||||
// 2. there is other_data, but user has no edit rights => show fileuploader but without action buttons
|
// 2. there is other_data, but user has no edit rights => show fileuploader but without action buttons
|
||||||
// 3. both other_data and editable are defined or true => show fileuploader with all action buttons
|
// 3. both other_data and editable are defined or true => show fileuploader with all action buttons
|
||||||
if (!this.props.editable && (!this.props.otherData || this.props.otherData.length === 0)) {
|
if (!editable && (!otherData || otherData.length === 0)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let otherDataIds = this.props.otherData ? this.props.otherData.map((data) => data.id).join() : null;
|
let otherDataIds = otherData ? otherData.map((data) => data.id).join() : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Property
|
<Property
|
||||||
name="other_data_key"
|
name="other_data_key"
|
||||||
label={this.props.label}>
|
label={this.props.label}>
|
||||||
<ReactS3FineUploader
|
<ReactS3FineUploader
|
||||||
|
areAssetsDownloadable
|
||||||
|
areAssetsEditable={editable}
|
||||||
|
createBlobRoutine={{
|
||||||
|
url: ApiUrls.blob_otherdatas,
|
||||||
|
pieceId: pieceId
|
||||||
|
}}
|
||||||
|
deleteFile={{
|
||||||
|
enabled: true,
|
||||||
|
method: 'DELETE',
|
||||||
|
endpoint: AppConstants.serverUrl + 's3/delete',
|
||||||
|
customHeaders: {
|
||||||
|
'X-CSRFToken': getCookie(AppConstants.csrftoken)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isReadyForFormSubmission={isReadyForFormSubmission}
|
||||||
keyRoutine={{
|
keyRoutine={{
|
||||||
url: AppConstants.serverUrl + 's3/key/',
|
url: AppConstants.serverUrl + 's3/key/',
|
||||||
fileClass: 'otherdata',
|
fileClass: 'otherdata',
|
||||||
pieceId: this.props.pieceId
|
pieceId: pieceId
|
||||||
}}
|
}}
|
||||||
createBlobRoutine={{
|
multiple={multiple}
|
||||||
url: ApiUrls.blob_otherdatas,
|
onValidationFailed={onValidationFailed}
|
||||||
pieceId: this.props.pieceId
|
setIsUploadReady={setIsUploadReady}
|
||||||
}}
|
|
||||||
validation={this.props.validation}
|
|
||||||
submitFile={this.props.submitFile}
|
|
||||||
onValidationFailed={this.props.onValidationFailed}
|
|
||||||
setIsUploadReady={this.props.setIsUploadReady}
|
|
||||||
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
|
|
||||||
session={{
|
session={{
|
||||||
endpoint: AppConstants.serverUrl + 'api/blob/otherdatas/fineuploader_session/',
|
endpoint: AppConstants.serverUrl + 'api/blob/otherdatas/fineuploader_session/',
|
||||||
customHeaders: {
|
customHeaders: {
|
||||||
@ -89,17 +115,9 @@ let FurtherDetailsFileuploader = React.createClass({
|
|||||||
'X-CSRFToken': getCookie(AppConstants.csrftoken)
|
'X-CSRFToken': getCookie(AppConstants.csrftoken)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
deleteFile={{
|
submitFile={submitFile}
|
||||||
enabled: true,
|
showErrorPrompt={showErrorPrompt}
|
||||||
method: 'DELETE',
|
validation={validation} />
|
||||||
endpoint: AppConstants.serverUrl + 's3/delete',
|
|
||||||
customHeaders: {
|
|
||||||
'X-CSRFToken': getCookie(AppConstants.csrftoken)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
areAssetsDownloadable={this.props.areAssetsDownloadable}
|
|
||||||
areAssetsEditable={this.props.editable}
|
|
||||||
multiple={this.props.multiple} />
|
|
||||||
</Property>
|
</Property>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import InputFineUploader from './input_fineuploader';
|
|||||||
|
|
||||||
import FormSubmitButton from '../ascribe_buttons/form_submit_button';
|
import FormSubmitButton from '../ascribe_buttons/form_submit_button';
|
||||||
|
|
||||||
|
import { FileStatus } from '../ascribe_uploader/react_s3_fine_uploader_utils';
|
||||||
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
|
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
|
||||||
|
|
||||||
import AscribeSpinner from '../ascribe_spinner';
|
import AscribeSpinner from '../ascribe_spinner';
|
||||||
@ -72,7 +73,7 @@ let RegisterPieceForm = React.createClass({
|
|||||||
|
|
||||||
handleChangedDigitalWork(digitalWorkFile) {
|
handleChangedDigitalWork(digitalWorkFile) {
|
||||||
if (digitalWorkFile &&
|
if (digitalWorkFile &&
|
||||||
(digitalWorkFile.status === 'deleted' || digitalWorkFile.status === 'canceled')) {
|
(digitalWorkFile.status === FileStatus.DELETED || digitalWorkFile.status === FileStatus.CANCELED)) {
|
||||||
this.refs.form.refs.thumbnail_file.reset();
|
this.refs.form.refs.thumbnail_file.reset();
|
||||||
|
|
||||||
// Manually we need to set the ready state for `thumbnailKeyReady` back
|
// Manually we need to set the ready state for `thumbnailKeyReady` back
|
||||||
@ -91,8 +92,8 @@ let RegisterPieceForm = React.createClass({
|
|||||||
|
|
||||||
fineuploader.setThumbnailForFileId(
|
fineuploader.setThumbnailForFileId(
|
||||||
digitalWorkFile.id,
|
digitalWorkFile.id,
|
||||||
// if thumbnail was delete, we delete it from the display as well
|
// if thumbnail was deleted, we delete it from the display as well
|
||||||
thumbnailFile.status !== 'deleted' ? thumbnailFile.url : null
|
thumbnailFile.status !== FileStatus.DELETED ? thumbnailFile.url : null
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -175,7 +176,8 @@ let RegisterPieceForm = React.createClass({
|
|||||||
disabled={!isFineUploaderEditable}
|
disabled={!isFineUploaderEditable}
|
||||||
enableLocalHashing={hashLocally}
|
enableLocalHashing={hashLocally}
|
||||||
uploadMethod={location.query.method}
|
uploadMethod={location.query.method}
|
||||||
handleChangedFile={this.handleChangedDigitalWork}/>
|
handleChangedFile={this.handleChangedDigitalWork}
|
||||||
|
showErrorPrompt />
|
||||||
</Property>
|
</Property>
|
||||||
<Property
|
<Property
|
||||||
name="thumbnail_file"
|
name="thumbnail_file"
|
||||||
|
@ -10,48 +10,35 @@ import AppConstants from '../../constants/application_constants';
|
|||||||
import { getCookie } from '../../utils/fetch_api_utils';
|
import { getCookie } from '../../utils/fetch_api_utils';
|
||||||
|
|
||||||
|
|
||||||
const { func, bool, shape, string, number, arrayOf } = React.PropTypes;
|
const { func, bool, shape, string, number, element, oneOf, oneOfType, arrayOf } = React.PropTypes;
|
||||||
|
|
||||||
const InputFineUploader = React.createClass({
|
const InputFineUploader = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
setIsUploadReady: func,
|
|
||||||
isReadyForFormSubmission: func,
|
|
||||||
submitFile: func,
|
|
||||||
fileInputElement: func,
|
|
||||||
|
|
||||||
areAssetsDownloadable: bool,
|
|
||||||
|
|
||||||
keyRoutine: shape({
|
|
||||||
url: string,
|
|
||||||
fileClass: string
|
|
||||||
}),
|
|
||||||
createBlobRoutine: shape({
|
|
||||||
url: string
|
|
||||||
}),
|
|
||||||
validation: ReactS3FineUploader.propTypes.validation,
|
|
||||||
|
|
||||||
// isFineUploaderActive is used to lock react fine uploader in case
|
// isFineUploaderActive is used to lock react fine uploader in case
|
||||||
// a user is actually not logged in already to prevent him from droping files
|
// a user is actually not logged in already to prevent him from droping files
|
||||||
// before login in
|
// before login in
|
||||||
isFineUploaderActive: bool,
|
isFineUploaderActive: bool,
|
||||||
|
|
||||||
enableLocalHashing: bool,
|
|
||||||
uploadMethod: string,
|
|
||||||
|
|
||||||
// provided by Property
|
// provided by Property
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
|
onChange: func,
|
||||||
|
|
||||||
// A class of a file the user has to upload
|
// Props for ReactS3FineUploader
|
||||||
// Needs to be defined both in singular as well as in plural
|
areAssetsDownloadable: bool,
|
||||||
fileClassToUpload: shape({
|
createBlobRoutine: ReactS3FineUploader.propTypes.createBlobRoutine,
|
||||||
singular: string,
|
enableLocalHashing: bool,
|
||||||
plural: string
|
fileClassToUpload: ReactS3FineUploader.propTypes.fileClassToUpload,
|
||||||
}),
|
fileInputElement: ReactS3FineUploader.propTypes.fileInputElement,
|
||||||
handleChangedFile: func,
|
isReadyForFormSubmission: func,
|
||||||
|
keyRoutine: ReactS3FineUploader.propTypes.keyRoutine,
|
||||||
|
handleChangedFile: func, // TODO: rename to onChangedFile
|
||||||
|
submitFile: func, // TODO: rename to onSubmitFile
|
||||||
onValidationFailed: func,
|
onValidationFailed: func,
|
||||||
|
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
|
||||||
// Provided by `Property`
|
setWarning: func,
|
||||||
onChange: React.PropTypes.func
|
showErrorPrompt: bool,
|
||||||
|
uploadMethod: oneOf(['hash', 'upload']),
|
||||||
|
validation: ReactS3FineUploader.propTypes.validation,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
@ -96,19 +83,21 @@ const InputFineUploader = React.createClass({
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
areAssetsDownloadable,
|
areAssetsDownloadable,
|
||||||
enableLocalHashing,
|
|
||||||
createBlobRoutine,
|
createBlobRoutine,
|
||||||
|
enableLocalHashing,
|
||||||
disabled,
|
disabled,
|
||||||
fileClassToUpload,
|
fileClassToUpload,
|
||||||
fileInputElement,
|
fileInputElement,
|
||||||
|
handleChangedFile,
|
||||||
isFineUploaderActive,
|
isFineUploaderActive,
|
||||||
isReadyForFormSubmission,
|
isReadyForFormSubmission,
|
||||||
keyRoutine,
|
keyRoutine,
|
||||||
onValidationFailed,
|
onValidationFailed,
|
||||||
setIsUploadReady,
|
setIsUploadReady,
|
||||||
|
setWarning,
|
||||||
|
showErrorPrompt,
|
||||||
uploadMethod,
|
uploadMethod,
|
||||||
validation,
|
validation } = this.props;
|
||||||
handleChangedFile } = this.props;
|
|
||||||
let editable = isFineUploaderActive;
|
let editable = isFineUploaderActive;
|
||||||
|
|
||||||
// if disabled is actually set by property, we want to override
|
// if disabled is actually set by property, we want to override
|
||||||
@ -130,6 +119,8 @@ const InputFineUploader = React.createClass({
|
|||||||
isReadyForFormSubmission={isReadyForFormSubmission}
|
isReadyForFormSubmission={isReadyForFormSubmission}
|
||||||
areAssetsDownloadable={areAssetsDownloadable}
|
areAssetsDownloadable={areAssetsDownloadable}
|
||||||
areAssetsEditable={editable}
|
areAssetsEditable={editable}
|
||||||
|
setWarning={setWarning}
|
||||||
|
showErrorPrompt={showErrorPrompt}
|
||||||
signature={{
|
signature={{
|
||||||
endpoint: AppConstants.serverUrl + 's3/signature/',
|
endpoint: AppConstants.serverUrl + 's3/signature/',
|
||||||
customHeaders: {
|
customHeaders: {
|
||||||
@ -147,7 +138,7 @@ const InputFineUploader = React.createClass({
|
|||||||
enableLocalHashing={enableLocalHashing}
|
enableLocalHashing={enableLocalHashing}
|
||||||
uploadMethod={uploadMethod}
|
uploadMethod={uploadMethod}
|
||||||
fileClassToUpload={fileClassToUpload}
|
fileClassToUpload={fileClassToUpload}
|
||||||
handleChangedFile={handleChangedFile}/>
|
handleChangedFile={handleChangedFile} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -72,7 +72,8 @@ const Property = React.createClass({
|
|||||||
initialValue: null,
|
initialValue: null,
|
||||||
value: null,
|
value: null,
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
errors: null
|
errors: null,
|
||||||
|
hasWarning: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -218,17 +219,20 @@ const Property = React.createClass({
|
|||||||
this.setState({errors: null});
|
this.setState({errors: null});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setWarning(hasWarning) {
|
||||||
|
this.setState({ hasWarning });
|
||||||
|
},
|
||||||
|
|
||||||
getClassName() {
|
getClassName() {
|
||||||
if(!this.state.expanded && !this.props.checkboxLabel){
|
if (!this.state.expanded && !this.props.checkboxLabel) {
|
||||||
return 'is-hidden';
|
return 'is-hidden';
|
||||||
}
|
} else if (!this.props.editable) {
|
||||||
if(!this.props.editable){
|
|
||||||
return 'is-fixed';
|
return 'is-fixed';
|
||||||
}
|
} else if (this.state.errors) {
|
||||||
if (this.state.errors){
|
|
||||||
return 'is-error';
|
return 'is-error';
|
||||||
}
|
} else if (this.state.hasWarning) {
|
||||||
if(this.state.isFocused) {
|
return 'is-warning';
|
||||||
|
} else if (this.state.isFocused) {
|
||||||
return 'is-focused';
|
return 'is-focused';
|
||||||
} else {
|
} else {
|
||||||
return '';
|
return '';
|
||||||
@ -271,6 +275,7 @@ const Property = React.createClass({
|
|||||||
onChange: this.handleChange,
|
onChange: this.handleChange,
|
||||||
onFocus: this.handleFocus,
|
onFocus: this.handleFocus,
|
||||||
onBlur: this.handleBlur,
|
onBlur: this.handleBlur,
|
||||||
|
setWarning: this.setWarning,
|
||||||
disabled: !this.props.editable,
|
disabled: !this.props.editable,
|
||||||
ref: 'input',
|
ref: 'input',
|
||||||
name: this.props.name,
|
name: this.props.name,
|
||||||
|
@ -17,7 +17,7 @@ const WHEN_ENUM = ['loggedIn', 'loggedOut'];
|
|||||||
*
|
*
|
||||||
* @param {enum/string} options.when ('loggedIn' || 'loggedOut')
|
* @param {enum/string} options.when ('loggedIn' || 'loggedOut')
|
||||||
*/
|
*/
|
||||||
export function AuthRedirect({to, when}) {
|
export function AuthRedirect({ to, when }) {
|
||||||
// validate `when`, must be contained in `WHEN_ENUM`.
|
// validate `when`, must be contained in `WHEN_ENUM`.
|
||||||
// Throw an error otherwise.
|
// Throw an error otherwise.
|
||||||
if (WHEN_ENUM.indexOf(when) === -1) {
|
if (WHEN_ENUM.indexOf(when) === -1) {
|
||||||
@ -80,8 +80,8 @@ export function ProxyHandler(...redirectFunctions) {
|
|||||||
displayName: 'ProxyHandler',
|
displayName: 'ProxyHandler',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// Provided from AscribeApp
|
// Provided from AscribeApp, after the routes have been initialized
|
||||||
currentUser: React.PropTypes.object.isRequired,
|
currentUser: React.PropTypes.object,
|
||||||
whitelabel: React.PropTypes.object,
|
whitelabel: React.PropTypes.object,
|
||||||
|
|
||||||
// Provided from router
|
// Provided from router
|
||||||
|
@ -1,29 +1,33 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
|
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
|
||||||
|
|
||||||
import FileDragAndDropDialog from './file_drag_and_drop_dialog';
|
import FileDragAndDropDialog from './file_drag_and_drop_dialog';
|
||||||
|
import FileDragAndDropErrorDialog from './file_drag_and_drop_error_dialog';
|
||||||
import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator';
|
import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator';
|
||||||
|
|
||||||
|
import { FileStatus } from '../react_s3_fine_uploader_utils';
|
||||||
import { getLangText } from '../../../utils/lang_utils';
|
import { getLangText } from '../../../utils/lang_utils';
|
||||||
|
|
||||||
|
|
||||||
// Taken from: https://github.com/fedosejev/react-file-drag-and-drop
|
// Taken from: https://github.com/fedosejev/react-file-drag-and-drop
|
||||||
let FileDragAndDrop = React.createClass({
|
let FileDragAndDrop = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
className: React.PropTypes.string,
|
areAssetsDownloadable: React.PropTypes.bool,
|
||||||
|
areAssetsEditable: React.PropTypes.bool,
|
||||||
|
multiple: React.PropTypes.bool,
|
||||||
|
dropzoneInactive: React.PropTypes.bool,
|
||||||
|
filesToUpload: React.PropTypes.array,
|
||||||
|
|
||||||
onDrop: React.PropTypes.func.isRequired,
|
onDrop: React.PropTypes.func.isRequired,
|
||||||
onDragOver: React.PropTypes.func,
|
onDragOver: React.PropTypes.func,
|
||||||
filesToUpload: React.PropTypes.array,
|
|
||||||
handleDeleteFile: React.PropTypes.func,
|
handleDeleteFile: React.PropTypes.func,
|
||||||
handleCancelFile: React.PropTypes.func,
|
handleCancelFile: React.PropTypes.func,
|
||||||
handlePauseFile: React.PropTypes.func,
|
handlePauseFile: React.PropTypes.func,
|
||||||
handleResumeFile: React.PropTypes.func,
|
handleResumeFile: React.PropTypes.func,
|
||||||
multiple: React.PropTypes.bool,
|
handleRetryFiles: React.PropTypes.func,
|
||||||
dropzoneInactive: React.PropTypes.bool,
|
|
||||||
areAssetsDownloadable: React.PropTypes.bool,
|
|
||||||
areAssetsEditable: React.PropTypes.bool,
|
|
||||||
|
|
||||||
enableLocalHashing: React.PropTypes.bool,
|
enableLocalHashing: React.PropTypes.bool,
|
||||||
uploadMethod: React.PropTypes.string,
|
uploadMethod: React.PropTypes.string,
|
||||||
@ -34,6 +38,12 @@ let FileDragAndDrop = React.createClass({
|
|||||||
// to -1 which is code for: aborted
|
// to -1 which is code for: aborted
|
||||||
handleCancelHashing: React.PropTypes.func,
|
handleCancelHashing: React.PropTypes.func,
|
||||||
|
|
||||||
|
showError: React.PropTypes.bool,
|
||||||
|
errorClass: React.PropTypes.shape({
|
||||||
|
name: React.PropTypes.string,
|
||||||
|
prettifiedText: React.PropTypes.string
|
||||||
|
}),
|
||||||
|
|
||||||
// A class of a file the user has to upload
|
// A class of a file the user has to upload
|
||||||
// Needs to be defined both in singular as well as in plural
|
// Needs to be defined both in singular as well as in plural
|
||||||
fileClassToUpload: React.PropTypes.shape({
|
fileClassToUpload: React.PropTypes.shape({
|
||||||
@ -126,65 +136,93 @@ let FileDragAndDrop = React.createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getErrorDialog(failedFiles) {
|
||||||
|
const { errorClass } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileDragAndDropErrorDialog
|
||||||
|
errorClass={errorClass}
|
||||||
|
files={failedFiles}
|
||||||
|
handleRetryFiles={this.props.handleRetryFiles} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
getPreviewIterator() {
|
||||||
|
const { areAssetsDownloadable, areAssetsEditable, filesToUpload } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileDragAndDropPreviewIterator
|
||||||
|
files={filesToUpload}
|
||||||
|
handleDeleteFile={this.handleDeleteFile}
|
||||||
|
handleCancelFile={this.handleCancelFile}
|
||||||
|
handlePauseFile={this.handlePauseFile}
|
||||||
|
handleResumeFile={this.handleResumeFile}
|
||||||
|
areAssetsDownloadable={areAssetsDownloadable}
|
||||||
|
areAssetsEditable={areAssetsEditable}/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
getUploadDialog() {
|
||||||
|
const { enableLocalHashing, fileClassToUpload, multiple, uploadMethod } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileDragAndDropDialog
|
||||||
|
multipleFiles={multiple}
|
||||||
|
onClick={this.handleOnClick}
|
||||||
|
enableLocalHashing={enableLocalHashing}
|
||||||
|
uploadMethod={uploadMethod}
|
||||||
|
fileClassToUpload={fileClassToUpload} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
render: function () {
|
render: function () {
|
||||||
const {
|
const {
|
||||||
filesToUpload,
|
filesToUpload,
|
||||||
dropzoneInactive,
|
dropzoneInactive,
|
||||||
className,
|
|
||||||
hashingProgress,
|
hashingProgress,
|
||||||
handleCancelHashing,
|
handleCancelHashing,
|
||||||
multiple,
|
multiple,
|
||||||
enableLocalHashing,
|
showError,
|
||||||
uploadMethod,
|
errorClass,
|
||||||
fileClassToUpload,
|
fileClassToUpload,
|
||||||
areAssetsDownloadable,
|
|
||||||
areAssetsEditable,
|
|
||||||
allowedExtensions } = this.props;
|
allowedExtensions } = this.props;
|
||||||
|
|
||||||
// has files only is true if there are files that do not have the status deleted or canceled
|
// has files only is true if there are files that do not have the status deleted, canceled, or failed
|
||||||
let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0;
|
const hasFiles = filesToUpload
|
||||||
let updatedClassName = hasFiles ? 'has-files ' : '';
|
.filter((file) => {
|
||||||
updatedClassName += dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone';
|
return file.status !== FileStatus.DELETED &&
|
||||||
updatedClassName += ' file-drag-and-drop';
|
file.status !== FileStatus.CANCELED &&
|
||||||
|
file.status !== FileStatus.UPLOAD_FAILED &&
|
||||||
|
file.size !== -1;
|
||||||
|
})
|
||||||
|
.length > 0;
|
||||||
|
|
||||||
|
const failedFiles = filesToUpload.filter((file) => file.status === FileStatus.UPLOAD_FAILED);
|
||||||
|
let hasError = showError && errorClass && failedFiles.length > 0;
|
||||||
|
|
||||||
// if !== -2: triggers a FileDragAndDrop-global spinner
|
// if !== -2: triggers a FileDragAndDrop-global spinner
|
||||||
if(hashingProgress !== -2) {
|
if (hashingProgress !== -2) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className="file-drag-and-drop-hashing-dialog">
|
||||||
<div className="file-drag-and-drop-hashing-dialog">
|
<p>{getLangText('Computing hash(es)... This may take a few minutes.')}</p>
|
||||||
<p>{getLangText('Computing hash(es)... This may take a few minutes.')}</p>
|
<p>
|
||||||
<p>
|
<a onClick={handleCancelHashing}> {getLangText('Cancel hashing')}</a>
|
||||||
<a onClick={handleCancelHashing}> {getLangText('Cancel hashing')}</a>
|
</p>
|
||||||
</p>
|
<ProgressBar
|
||||||
<ProgressBar
|
now={Math.ceil(hashingProgress)}
|
||||||
now={Math.ceil(hashingProgress)}
|
label="%(percent)s%"
|
||||||
label="%(percent)s%"
|
className="ascribe-progress-bar"/>
|
||||||
className="ascribe-progress-bar"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={updatedClassName}
|
className={classNames('file-drag-and-drop', dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone', { 'has-files': hasFiles })}
|
||||||
onDrag={this.handleDrop}
|
onDrag={this.handleDrop}
|
||||||
onDragOver={this.handleDragOver}
|
onDragOver={this.handleDragOver}
|
||||||
onDrop={this.handleDrop}>
|
onDrop={this.handleDrop}>
|
||||||
<FileDragAndDropDialog
|
{hasError ? this.getErrorDialog(failedFiles) : this.getPreviewIterator()}
|
||||||
multipleFiles={multiple}
|
{!hasFiles && !hasError ? this.getUploadDialog() : null}
|
||||||
hasFiles={hasFiles}
|
|
||||||
onClick={this.handleOnClick}
|
|
||||||
enableLocalHashing={enableLocalHashing}
|
|
||||||
uploadMethod={uploadMethod}
|
|
||||||
fileClassToUpload={fileClassToUpload} />
|
|
||||||
<FileDragAndDropPreviewIterator
|
|
||||||
files={filesToUpload}
|
|
||||||
handleDeleteFile={this.handleDeleteFile}
|
|
||||||
handleCancelFile={this.handleCancelFile}
|
|
||||||
handlePauseFile={this.handlePauseFile}
|
|
||||||
handleResumeFile={this.handleResumeFile}
|
|
||||||
areAssetsDownloadable={areAssetsDownloadable}
|
|
||||||
areAssetsEditable={areAssetsEditable}/>
|
|
||||||
{/*
|
{/*
|
||||||
Opera doesn't trigger simulated click events
|
Opera doesn't trigger simulated click events
|
||||||
if the targeted input has `display:none` set.
|
if the targeted input has `display:none` set.
|
||||||
|
@ -9,7 +9,6 @@ import { getCurrentQueryParams } from '../../../utils/url_utils';
|
|||||||
|
|
||||||
let FileDragAndDropDialog = React.createClass({
|
let FileDragAndDropDialog = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
hasFiles: React.PropTypes.bool,
|
|
||||||
multipleFiles: React.PropTypes.bool,
|
multipleFiles: React.PropTypes.bool,
|
||||||
enableLocalHashing: React.PropTypes.bool,
|
enableLocalHashing: React.PropTypes.bool,
|
||||||
uploadMethod: React.PropTypes.string,
|
uploadMethod: React.PropTypes.string,
|
||||||
@ -36,92 +35,88 @@ let FileDragAndDropDialog = React.createClass({
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
hasFiles,
|
|
||||||
multipleFiles,
|
multipleFiles,
|
||||||
enableLocalHashing,
|
enableLocalHashing,
|
||||||
uploadMethod,
|
uploadMethod,
|
||||||
fileClassToUpload,
|
fileClassToUpload,
|
||||||
onClick } = this.props;
|
onClick } = this.props;
|
||||||
|
let dialogElement;
|
||||||
|
|
||||||
if (hasFiles) {
|
if (enableLocalHashing && !uploadMethod) {
|
||||||
return null;
|
const currentQueryParams = getCurrentQueryParams();
|
||||||
} else {
|
|
||||||
let dialogElement;
|
|
||||||
|
|
||||||
if (enableLocalHashing && !uploadMethod) {
|
const queryParamsHash = Object.assign({}, currentQueryParams);
|
||||||
const currentQueryParams = getCurrentQueryParams();
|
queryParamsHash.method = 'hash';
|
||||||
|
|
||||||
const queryParamsHash = Object.assign({}, currentQueryParams);
|
const queryParamsUpload = Object.assign({}, currentQueryParams);
|
||||||
queryParamsHash.method = 'hash';
|
queryParamsUpload.method = 'upload';
|
||||||
|
|
||||||
const queryParamsUpload = Object.assign({}, currentQueryParams);
|
dialogElement = (
|
||||||
queryParamsUpload.method = 'upload';
|
<div className="present-options">
|
||||||
|
<p className="file-drag-and-drop-dialog-title">{getLangText('Would you rather')}</p>
|
||||||
dialogElement = (
|
{/*
|
||||||
<div className="present-options">
|
The frontend in live is hosted under /app,
|
||||||
<p className="file-drag-and-drop-dialog-title">{getLangText('Would you rather')}</p>
|
Since `Link` is appending that base url, if its defined
|
||||||
{/*
|
by itself, we need to make sure to not set it at this point.
|
||||||
The frontend in live is hosted under /app,
|
Otherwise it will be appended twice.
|
||||||
Since `Link` is appending that base url, if its defined
|
*/}
|
||||||
by itself, we need to make sure to not set it at this point.
|
<Link
|
||||||
Otherwise it will be appended twice.
|
to={`/${window.location.pathname.split('/').pop()}`}
|
||||||
*/}
|
query={queryParamsHash}>
|
||||||
<Link
|
<span className="btn btn-default btn-sm">
|
||||||
to={`/${window.location.pathname.split('/').pop()}`}
|
{getLangText('Hash your work')}
|
||||||
query={queryParamsHash}>
|
|
||||||
<span className="btn btn-default btn-sm">
|
|
||||||
{getLangText('Hash your work')}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<span> or </span>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
to={`/${window.location.pathname.split('/').pop()}`}
|
|
||||||
query={queryParamsUpload}>
|
|
||||||
<span className="btn btn-default btn-sm">
|
|
||||||
{getLangText('Upload and hash your work')}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (multipleFiles) {
|
|
||||||
dialogElement = [
|
|
||||||
this.getDragDialog(fileClassToUpload.plural),
|
|
||||||
<span
|
|
||||||
className="btn btn-default"
|
|
||||||
onClick={onClick}>
|
|
||||||
{getLangText('choose %s to upload', fileClassToUpload.plural)}
|
|
||||||
</span>
|
</span>
|
||||||
];
|
</Link>
|
||||||
} else {
|
|
||||||
const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular)
|
|
||||||
: getLangText('choose a %s to upload', fileClassToUpload.singular);
|
|
||||||
|
|
||||||
dialogElement = [
|
<span> {getLangText('or')} </span>
|
||||||
this.getDragDialog(fileClassToUpload.singular),
|
|
||||||
<span
|
<Link
|
||||||
className="btn btn-default"
|
to={`/${window.location.pathname.split('/').pop()}`}
|
||||||
onClick={onClick}>
|
query={queryParamsUpload}>
|
||||||
{dialog}
|
<span className="btn btn-default btn-sm">
|
||||||
|
{getLangText('Upload and hash your work')}
|
||||||
</span>
|
</span>
|
||||||
];
|
</Link>
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="file-drag-and-drop-dialog">
|
|
||||||
<div className="hidden-print">
|
|
||||||
{dialogElement}
|
|
||||||
</div>
|
|
||||||
{/* Hide the uploader and just show that there's been on files uploaded yet when printing */}
|
|
||||||
<p className="text-align-center visible-print">
|
|
||||||
{getLangText('No files uploaded')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
if (multipleFiles) {
|
||||||
|
dialogElement = [
|
||||||
|
this.getDragDialog(fileClassToUpload.plural),
|
||||||
|
(<span
|
||||||
|
key='mutlipleFilesBtn'
|
||||||
|
className="btn btn-default"
|
||||||
|
onClick={onClick}>
|
||||||
|
{getLangText('choose %s to upload', fileClassToUpload.plural)}
|
||||||
|
</span>)
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular)
|
||||||
|
: getLangText('choose a %s to upload', fileClassToUpload.singular);
|
||||||
|
|
||||||
|
dialogElement = [
|
||||||
|
this.getDragDialog(fileClassToUpload.singular),
|
||||||
|
(<span
|
||||||
|
key='singleFileBtn'
|
||||||
|
className="btn btn-default"
|
||||||
|
onClick={onClick}>
|
||||||
|
{dialog}
|
||||||
|
</span>)
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-drag-and-drop-dialog">
|
||||||
|
<div className="hidden-print">
|
||||||
|
{dialogElement}
|
||||||
|
</div>
|
||||||
|
{/* Hide the uploader and just show that there's been on files uploaded yet when printing */}
|
||||||
|
<p className="text-align-center visible-print">
|
||||||
|
{getLangText('No files uploaded')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { ErrorClasses } from '../../../constants/error_constants';
|
||||||
|
|
||||||
|
import { getLangText } from '../../../utils/lang_utils';
|
||||||
|
|
||||||
|
let FileDragAndDropErrorDialog = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
errorClass: React.PropTypes.shape({
|
||||||
|
name: React.PropTypes.string,
|
||||||
|
prettifiedText: React.PropTypes.string
|
||||||
|
}).isRequired,
|
||||||
|
files: React.PropTypes.array.isRequired,
|
||||||
|
handleRetryFiles: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
getRetryButton(text, openIntercom) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className='btn btn-default'
|
||||||
|
onClick={() => {
|
||||||
|
if (openIntercom) {
|
||||||
|
window.Intercom('showNewMessage', getLangText("I'm having trouble uploading my file."));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.retryAllFiles()
|
||||||
|
}}>
|
||||||
|
{getLangText(text)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
getContactUsDetail() {
|
||||||
|
return (
|
||||||
|
<div className='file-drag-and-drop-error'>
|
||||||
|
<h4>{getLangText('Let us help you')}</h4>
|
||||||
|
<p>{getLangText('Still having problems? Send us a message.')}</p>
|
||||||
|
{this.getRetryButton('Contact us', true)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
getErrorDetail(multipleFiles) {
|
||||||
|
const { errorClass: { prettifiedText }, files } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='file-drag-and-drop-error'>
|
||||||
|
<div className={classNames('file-drag-and-drop-error-detail', { 'file-drag-and-drop-error-detail-multiple-files': multipleFiles })}>
|
||||||
|
<h4>{getLangText(multipleFiles ? 'Some files did not upload correctly'
|
||||||
|
: 'Error uploading the file!')}
|
||||||
|
</h4>
|
||||||
|
<p>{prettifiedText}</p>
|
||||||
|
{this.getRetryButton('Retry')}
|
||||||
|
</div>
|
||||||
|
<span className={classNames('file-drag-and-drop-error-icon-container', { 'file-drag-and-drop-error-icon-container-multiple-files': multipleFiles })}>
|
||||||
|
<span className='ascribe-icon icon-ascribe-thin-cross'></span>
|
||||||
|
</span>
|
||||||
|
<div className='file-drag-and-drop-error-file-names'>
|
||||||
|
<ul>
|
||||||
|
{files.map((file) => (<li key={file.id} className='file-name'>{file.originalName}</li>))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
retryAllFiles() {
|
||||||
|
const { files, handleRetryFiles } = this.props;
|
||||||
|
handleRetryFiles(files.map(file => file.id));
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { errorClass: { name: errorName }, files } = this.props;
|
||||||
|
|
||||||
|
const multipleFiles = files.length > 1;
|
||||||
|
const contactUs = errorName === ErrorClasses.upload.contactUs.name;
|
||||||
|
|
||||||
|
return contactUs ? this.getContactUsDetail() : this.getErrorDetail(multipleFiles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FileDragAndDropErrorDialog;
|
@ -5,6 +5,7 @@ import React from 'react';
|
|||||||
import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image';
|
import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image';
|
||||||
import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other';
|
import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other';
|
||||||
|
|
||||||
|
import { FileStatus } from '../react_s3_fine_uploader_utils';
|
||||||
import { getLangText } from '../../../utils/lang_utils';
|
import { getLangText } from '../../../utils/lang_utils';
|
||||||
import { truncateTextAtCharIndex } from '../../../utils/general_utils';
|
import { truncateTextAtCharIndex } from '../../../utils/general_utils';
|
||||||
import { extractFileExtensionFromString } from '../../../utils/file_utils';
|
import { extractFileExtensionFromString } from '../../../utils/file_utils';
|
||||||
@ -24,27 +25,29 @@ const FileDragAndDropPreview = React.createClass({
|
|||||||
s3UrlSafe: string
|
s3UrlSafe: string
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
|
||||||
|
areAssetsDownloadable: bool,
|
||||||
|
areAssetsEditable: bool,
|
||||||
handleDeleteFile: func,
|
handleDeleteFile: func,
|
||||||
handleCancelFile: func,
|
handleCancelFile: func,
|
||||||
handlePauseFile: func,
|
handlePauseFile: func,
|
||||||
handleResumeFile: func,
|
handleResumeFile: func,
|
||||||
areAssetsDownloadable: bool,
|
|
||||||
areAssetsEditable: bool,
|
|
||||||
numberOfDisplayedFiles: number
|
numberOfDisplayedFiles: number
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleUploadProcess() {
|
toggleUploadProcess() {
|
||||||
if (this.props.file.status === 'uploading') {
|
const { file, handlePauseFile, handleResumeFile } = this.props;
|
||||||
this.props.handlePauseFile(this.props.file.id);
|
|
||||||
} else if (this.props.file.status === 'paused') {
|
if (file.status === FileStatus.UPLOADING) {
|
||||||
this.props.handleResumeFile(this.props.file.id);
|
handlePauseFile(file.id);
|
||||||
|
} else if (file.status === FileStatus.PAUSED) {
|
||||||
|
handleResumeFile(file.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDeleteFile() {
|
handleDeleteFile() {
|
||||||
const { handleDeleteFile,
|
const { file,
|
||||||
handleCancelFile,
|
handleDeleteFile,
|
||||||
file } = this.props;
|
handleCancelFile } = this.props;
|
||||||
// `handleDeleteFile` is optional, so if its not submitted, don't run it
|
// `handleDeleteFile` is optional, so if its not submitted, don't run it
|
||||||
//
|
//
|
||||||
// For delete though, we only want to trigger it, when we're sure that
|
// For delete though, we only want to trigger it, when we're sure that
|
||||||
@ -52,7 +55,7 @@ const FileDragAndDropPreview = React.createClass({
|
|||||||
// deleted using an HTTP DELETE request.
|
// deleted using an HTTP DELETE request.
|
||||||
if (handleDeleteFile &&
|
if (handleDeleteFile &&
|
||||||
file.progress === 100 &&
|
file.progress === 100 &&
|
||||||
(file.status === 'upload successful' || file.status === 'online') &&
|
(file.status === FileStatus.UPLOAD_SUCCESSFUL || file.status === FileStatus.ONLINE) &&
|
||||||
file.s3UrlSafe) {
|
file.s3UrlSafe) {
|
||||||
handleDeleteFile(file.id);
|
handleDeleteFile(file.id);
|
||||||
} else if (handleCancelFile) {
|
} else if (handleCancelFile) {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils';
|
import { displayValidProgressFilesFilter, FileStatus } from '../react_s3_fine_uploader_utils';
|
||||||
import { getLangText } from '../../../utils/lang_utils';
|
import { getLangText } from '../../../utils/lang_utils';
|
||||||
import { truncateTextAtCharIndex } from '../../../utils/general_utils';
|
import { truncateTextAtCharIndex } from '../../../utils/general_utils';
|
||||||
|
|
||||||
@ -58,11 +58,11 @@ export default function UploadButton({ className = 'btn btn-default btn-sm', sho
|
|||||||
},
|
},
|
||||||
|
|
||||||
getUploadingFiles(filesToUpload = this.props.filesToUpload) {
|
getUploadingFiles(filesToUpload = this.props.filesToUpload) {
|
||||||
return filesToUpload.filter((file) => file.status === 'uploading');
|
return filesToUpload.filter((file) => file.status === FileStatus.UPLOADING);
|
||||||
},
|
},
|
||||||
|
|
||||||
getUploadedFile() {
|
getUploadedFile() {
|
||||||
return this.props.filesToUpload.filter((file) => file.status === 'upload successful')[0];
|
return this.props.filesToUpload.filter((file) => file.status === FileStatus.UPLOAD_SUCCESSFUL)[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
clearSelection() {
|
clearSelection() {
|
||||||
|
@ -8,12 +8,16 @@ import S3Fetcher from '../../fetchers/s3_fetcher';
|
|||||||
|
|
||||||
import FileDragAndDrop from './ascribe_file_drag_and_drop/file_drag_and_drop';
|
import FileDragAndDrop from './ascribe_file_drag_and_drop/file_drag_and_drop';
|
||||||
|
|
||||||
|
import ErrorQueueStore from '../../stores/error_queue_store';
|
||||||
|
|
||||||
import GlobalNotificationModel from '../../models/global_notification_model';
|
import GlobalNotificationModel from '../../models/global_notification_model';
|
||||||
import GlobalNotificationActions from '../../actions/global_notification_actions';
|
import GlobalNotificationActions from '../../actions/global_notification_actions';
|
||||||
|
|
||||||
import AppConstants from '../../constants/application_constants';
|
import AppConstants from '../../constants/application_constants';
|
||||||
|
import { ErrorClasses, testErrorAgainstAll } from '../../constants/error_constants';
|
||||||
|
import { RETRY_ATTEMPT_TO_SHOW_CONTACT_US } from '../../constants/uploader_constants';
|
||||||
|
|
||||||
import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils';
|
import { displayValidFilesFilter, FileStatus, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils';
|
||||||
import { getCookie } from '../../utils/fetch_api_utils';
|
import { getCookie } from '../../utils/fetch_api_utils';
|
||||||
import { computeHashOfFile, extractFileExtensionFromString } from '../../utils/file_utils';
|
import { computeHashOfFile, extractFileExtensionFromString } from '../../utils/file_utils';
|
||||||
import { getLangText } from '../../utils/lang_utils';
|
import { getLangText } from '../../utils/lang_utils';
|
||||||
@ -33,80 +37,18 @@ const { shape,
|
|||||||
|
|
||||||
const ReactS3FineUploader = React.createClass({
|
const ReactS3FineUploader = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
keyRoutine: shape({
|
|
||||||
url: string,
|
|
||||||
fileClass: string,
|
|
||||||
pieceId: number
|
|
||||||
}),
|
|
||||||
createBlobRoutine: shape({
|
|
||||||
url: string,
|
|
||||||
pieceId: number
|
|
||||||
}),
|
|
||||||
handleChangedFile: func, // is for when a file is dropped or selected
|
|
||||||
submitFile: func, // is for when a file has been successfully uploaded, TODO: rename to handleSubmitFile
|
|
||||||
onValidationFailed: func,
|
|
||||||
autoUpload: bool,
|
|
||||||
debug: bool,
|
|
||||||
objectProperties: shape({
|
|
||||||
acl: string
|
|
||||||
}),
|
|
||||||
request: shape({
|
|
||||||
endpoint: string,
|
|
||||||
accessKey: string,
|
|
||||||
params: shape({
|
|
||||||
csrfmiddlewaretoken: string
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
signature: shape({
|
|
||||||
endpoint: string
|
|
||||||
}).isRequired,
|
|
||||||
uploadSuccess: shape({
|
|
||||||
method: string,
|
|
||||||
endpoint: string,
|
|
||||||
params: shape({
|
|
||||||
isBrowserPreviewCapable: any, // maybe fix this later
|
|
||||||
bitcoin_ID_noPrefix: string
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
cors: shape({
|
|
||||||
expected: bool
|
|
||||||
}),
|
|
||||||
chunking: shape({
|
|
||||||
enabled: bool
|
|
||||||
}),
|
|
||||||
resume: shape({
|
|
||||||
enabled: bool
|
|
||||||
}),
|
|
||||||
deleteFile: shape({
|
|
||||||
enabled: bool,
|
|
||||||
method: string,
|
|
||||||
endpoint: string,
|
|
||||||
customHeaders: object
|
|
||||||
}).isRequired,
|
|
||||||
session: shape({
|
|
||||||
customHeaders: object,
|
|
||||||
endpoint: string,
|
|
||||||
params: object,
|
|
||||||
refreshOnRequests: bool
|
|
||||||
}),
|
|
||||||
validation: shape({
|
|
||||||
itemLimit: number,
|
|
||||||
sizeLimit: number,
|
|
||||||
allowedExtensions: arrayOf(string)
|
|
||||||
}),
|
|
||||||
messages: shape({
|
|
||||||
unsupportedBrowser: string
|
|
||||||
}),
|
|
||||||
formatFileName: func,
|
|
||||||
multiple: bool,
|
|
||||||
retry: shape({
|
|
||||||
enableAuto: bool
|
|
||||||
}),
|
|
||||||
setIsUploadReady: func,
|
|
||||||
isReadyForFormSubmission: func,
|
|
||||||
areAssetsDownloadable: bool,
|
areAssetsDownloadable: bool,
|
||||||
areAssetsEditable: bool,
|
areAssetsEditable: bool,
|
||||||
defaultErrorMessage: string,
|
errorNotificationMessage: string,
|
||||||
|
handleChangedFile: func, // for when a file is dropped or selected, TODO: rename to onChangedFile
|
||||||
|
submitFile: func, // for when a file has been successfully uploaded, TODO: rename to onSubmitFile
|
||||||
|
onValidationFailed: func,
|
||||||
|
setWarning: func, // for when the parent component wants to be notified of uploader warnings (ie. upload failed)
|
||||||
|
showErrorPrompt: bool,
|
||||||
|
|
||||||
|
// Handle form validation
|
||||||
|
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
|
||||||
|
isReadyForFormSubmission: func,
|
||||||
|
|
||||||
// We encountered some cases where people had difficulties to upload their
|
// We encountered some cases where people had difficulties to upload their
|
||||||
// works to ascribe due to a slow internet connection.
|
// works to ascribe due to a slow internet connection.
|
||||||
@ -135,13 +77,94 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
fileInputElement: oneOfType([
|
fileInputElement: oneOfType([
|
||||||
func,
|
func,
|
||||||
element
|
element
|
||||||
])
|
]),
|
||||||
|
|
||||||
|
// S3 helpers
|
||||||
|
createBlobRoutine: shape({
|
||||||
|
url: string,
|
||||||
|
pieceId: number
|
||||||
|
}),
|
||||||
|
keyRoutine: shape({
|
||||||
|
url: string,
|
||||||
|
fileClass: string,
|
||||||
|
pieceId: number
|
||||||
|
}),
|
||||||
|
|
||||||
|
// FineUploader options
|
||||||
|
debug: bool,
|
||||||
|
|
||||||
|
autoUpload: bool,
|
||||||
|
chunking: shape({
|
||||||
|
enabled: bool
|
||||||
|
}),
|
||||||
|
cors: shape({
|
||||||
|
expected: bool
|
||||||
|
}),
|
||||||
|
deleteFile: shape({
|
||||||
|
enabled: bool,
|
||||||
|
method: string,
|
||||||
|
endpoint: string,
|
||||||
|
customHeaders: object
|
||||||
|
}).isRequired,
|
||||||
|
formatFileName: func,
|
||||||
|
messages: shape({
|
||||||
|
unsupportedBrowser: string
|
||||||
|
}),
|
||||||
|
multiple: bool,
|
||||||
|
objectProperties: shape({
|
||||||
|
acl: string
|
||||||
|
}),
|
||||||
|
request: shape({
|
||||||
|
endpoint: string,
|
||||||
|
accessKey: string,
|
||||||
|
params: shape({
|
||||||
|
csrfmiddlewaretoken: string
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
resume: shape({
|
||||||
|
enabled: bool
|
||||||
|
}),
|
||||||
|
retry: shape({
|
||||||
|
enableAuto: bool
|
||||||
|
}),
|
||||||
|
session: shape({
|
||||||
|
customHeaders: object,
|
||||||
|
endpoint: string,
|
||||||
|
params: object,
|
||||||
|
refreshOnRequests: bool
|
||||||
|
}),
|
||||||
|
signature: shape({
|
||||||
|
endpoint: string
|
||||||
|
}).isRequired,
|
||||||
|
uploadSuccess: shape({
|
||||||
|
method: string,
|
||||||
|
endpoint: string,
|
||||||
|
params: shape({
|
||||||
|
isBrowserPreviewCapable: any, // maybe fix this later
|
||||||
|
bitcoin_ID_noPrefix: string
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
validation: shape({
|
||||||
|
itemLimit: number,
|
||||||
|
sizeLimit: number,
|
||||||
|
allowedExtensions: arrayOf(string)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
return {
|
return {
|
||||||
|
errorNotificationMessage: getLangText('Oops, we had a problem uploading your file. Please contact us if this happens repeatedly.'),
|
||||||
|
showErrorPrompt: false,
|
||||||
|
fileClassToUpload: {
|
||||||
|
singular: getLangText('file'),
|
||||||
|
plural: getLangText('files')
|
||||||
|
},
|
||||||
|
fileInputElement: FileDragAndDrop,
|
||||||
|
|
||||||
|
// FineUploader options
|
||||||
autoUpload: true,
|
autoUpload: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
|
multiple: false,
|
||||||
objectProperties: {
|
objectProperties: {
|
||||||
acl: 'public-read',
|
acl: 'public-read',
|
||||||
bucket: 'ascribe0'
|
bucket: 'ascribe0'
|
||||||
@ -178,27 +201,25 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
messages: {
|
messages: {
|
||||||
unsupportedBrowser: '<h3>' + getLangText('Upload is not functional in IE7 as IE7 has no support for CORS!') + '</h3>'
|
unsupportedBrowser: '<h3>' + getLangText('Upload is not functional in IE7 as IE7 has no support for CORS!') + '</h3>'
|
||||||
},
|
},
|
||||||
formatFileName: function(name){// fix maybe
|
formatFileName: function(name) { // fix maybe
|
||||||
if (name !== undefined && name.length > 26) {
|
if (name !== undefined && name.length > 26) {
|
||||||
name = name.slice(0, 15) + '...' + name.slice(-15);
|
name = name.slice(0, 15) + '...' + name.slice(-15);
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
},
|
}
|
||||||
multiple: false,
|
|
||||||
defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.'),
|
|
||||||
fileClassToUpload: {
|
|
||||||
singular: getLangText('file'),
|
|
||||||
plural: getLangText('files')
|
|
||||||
},
|
|
||||||
fileInputElement: FileDragAndDrop
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return {
|
return {
|
||||||
filesToUpload: [],
|
filesToUpload: [],
|
||||||
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()),
|
uploader: this.createNewFineUploader(),
|
||||||
csrfToken: getCookie(AppConstants.csrftoken),
|
csrfToken: getCookie(AppConstants.csrftoken),
|
||||||
|
errorState: {
|
||||||
|
manualRetryAttempt: 0,
|
||||||
|
errorClass: null
|
||||||
|
},
|
||||||
|
uploadInProgress: false,
|
||||||
|
|
||||||
// -1: aborted
|
// -1: aborted
|
||||||
// -2: uninitialized
|
// -2: uninitialized
|
||||||
@ -216,7 +237,7 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
let potentiallyNewCSRFToken = getCookie(AppConstants.csrftoken);
|
let potentiallyNewCSRFToken = getCookie(AppConstants.csrftoken);
|
||||||
if(this.state.csrfToken !== potentiallyNewCSRFToken) {
|
if(this.state.csrfToken !== potentiallyNewCSRFToken) {
|
||||||
this.setState({
|
this.setState({
|
||||||
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()),
|
uploader: this.createNewFineUploader(),
|
||||||
csrfToken: potentiallyNewCSRFToken
|
csrfToken: potentiallyNewCSRFToken
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -229,8 +250,12 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
this.state.uploader.cancelAll();
|
this.state.uploader.cancelAll();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createNewFineUploader() {
|
||||||
|
return new fineUploader.s3.FineUploaderBasic(this.propsToConfig());
|
||||||
|
},
|
||||||
|
|
||||||
propsToConfig() {
|
propsToConfig() {
|
||||||
let objectProperties = this.props.objectProperties;
|
const objectProperties = Object.assign({}, this.props.objectProperties);
|
||||||
objectProperties.key = this.requestKey;
|
objectProperties.key = this.requestKey;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -251,6 +276,7 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
multiple: this.props.multiple,
|
multiple: this.props.multiple,
|
||||||
retry: this.props.retry,
|
retry: this.props.retry,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
onAllComplete: this.onAllComplete,
|
||||||
onComplete: this.onComplete,
|
onComplete: this.onComplete,
|
||||||
onCancel: this.onCancel,
|
onCancel: this.onCancel,
|
||||||
onProgress: this.onProgress,
|
onProgress: this.onProgress,
|
||||||
@ -274,6 +300,9 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
// proclaim that upload is not ready
|
// proclaim that upload is not ready
|
||||||
this.props.setIsUploadReady(false);
|
this.props.setIsUploadReady(false);
|
||||||
|
|
||||||
|
// reset any warnings propagated to parent
|
||||||
|
this.setWarning(false);
|
||||||
|
|
||||||
// reset internal data structures of component
|
// reset internal data structures of component
|
||||||
this.setState(this.getInitialState());
|
this.setState(this.getInitialState());
|
||||||
},
|
},
|
||||||
@ -319,7 +348,7 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
// if createBlobRoutine is not defined,
|
// if createBlobRoutine is not defined,
|
||||||
// we're progressing right away without posting to S3
|
// we're progressing right away without posting to S3
|
||||||
// so that this can be done manually by the form
|
// so that this can be done manually by the form
|
||||||
if(!createBlobRoutine) {
|
if (!createBlobRoutine) {
|
||||||
// still we warn the user of this component
|
// still we warn the user of this component
|
||||||
console.warn('createBlobRoutine was not defined for ReactS3FineUploader. Continuing without creating the blob on the server.');
|
console.warn('createBlobRoutine was not defined for ReactS3FineUploader. Continuing without creating the blob on the server.');
|
||||||
resolve();
|
resolve();
|
||||||
@ -377,6 +406,19 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
this.clearFileSelection();
|
this.clearFileSelection();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
checkFormSubmissionReady() {
|
||||||
|
const { isReadyForFormSubmission, setIsUploadReady } = this.props;
|
||||||
|
|
||||||
|
// since the form validation props isReadyForFormSubmission and setIsUploadReady
|
||||||
|
// are optional, we'll only trigger them when they're actually defined
|
||||||
|
if (typeof isReadyForFormSubmission === 'function' && typeof setIsUploadReady === 'function') {
|
||||||
|
// set uploadReady to true if the uploader's ready for submission
|
||||||
|
setIsUploadReady(isReadyForFormSubmission(this.state.filesToUpload));
|
||||||
|
} else {
|
||||||
|
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
clearFileSelection() {
|
clearFileSelection() {
|
||||||
const { fileInput } = this.refs;
|
const { fileInput } = this.refs;
|
||||||
if (fileInput && typeof fileInput.clearSelection === 'function') {
|
if (fileInput && typeof fileInput.clearSelection === 'function') {
|
||||||
@ -394,6 +436,30 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getUploadErrorClass({ type = 'upload', reason, xhr }) {
|
||||||
|
const { manualRetryAttempt } = this.state.errorState;
|
||||||
|
let matchedErrorClass;
|
||||||
|
|
||||||
|
if ('onLine' in window.navigator && !window.navigator.onLine) {
|
||||||
|
// If the user's offline, this is definitely the most important error to show.
|
||||||
|
// TODO: use a better mechanism for checking network state, ie. offline.js
|
||||||
|
matchedErrorClass = ErrorClasses.upload.offline;
|
||||||
|
} else if (manualRetryAttempt === RETRY_ATTEMPT_TO_SHOW_CONTACT_US) {
|
||||||
|
// Use the contact us error class if they've retried a number of times
|
||||||
|
// and are still unsuccessful
|
||||||
|
matchedErrorClass = ErrorClasses.upload.contactUs;
|
||||||
|
} else {
|
||||||
|
matchedErrorClass = testErrorAgainstAll({ type, reason, xhr });
|
||||||
|
|
||||||
|
if (!matchedErrorClass) {
|
||||||
|
// If none found, show the next error message in the queue for upload errors
|
||||||
|
matchedErrorClass = ErrorQueueStore.getNextError('upload');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedErrorClass;
|
||||||
|
},
|
||||||
|
|
||||||
getXhrErrorComment(xhr) {
|
getXhrErrorComment(xhr) {
|
||||||
if (xhr) {
|
if (xhr) {
|
||||||
return {
|
return {
|
||||||
@ -406,13 +472,20 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
isDropzoneInactive() {
|
isDropzoneInactive() {
|
||||||
const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
|
const { areAssetsEditable, enableLocalHashing, multiple, showErrorPrompt, uploadMethod } = this.props;
|
||||||
|
const { errorState, filesToUpload } = this.state;
|
||||||
|
|
||||||
if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
|
const filesToDisplay = filesToUpload.filter((file) => {
|
||||||
return true;
|
return file.status !== FileStatus.DELETED &&
|
||||||
} else {
|
file.status !== FileStatus.CANCELED &&
|
||||||
return false;
|
file.status !== FileStatus.UPLOAD_FAILED &&
|
||||||
}
|
file.size !== -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (enableLocalHashing && !uploadMethod) ||
|
||||||
|
!areAssetsEditable ||
|
||||||
|
(showErrorPrompt && errorState.errorClass) ||
|
||||||
|
(!multiple && filesToDisplay.length > 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
isFileValid(file) {
|
isFileValid(file) {
|
||||||
@ -457,7 +530,7 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
return Q.Promise((resolve) => {
|
return Q.Promise((resolve) => {
|
||||||
let changeSet = {};
|
let changeSet = {};
|
||||||
|
|
||||||
if(status === 'deleted' || status === 'canceled') {
|
if (status === FileStatus.DELETED || status === FileStatus.CANCELED || status === FileStatus.UPLOAD_FAILED) {
|
||||||
changeSet.progress = { $set: 0 };
|
changeSet.progress = { $set: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,8 +557,13 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/* FineUploader specific callback function handlers */
|
setWarning(hasWarning) {
|
||||||
|
if (typeof this.props.setWarning === 'function') {
|
||||||
|
this.props.setWarning(hasWarning);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* FineUploader specific callback function handlers */
|
||||||
onUploadChunk(id, name, chunkData) {
|
onUploadChunk(id, name, chunkData) {
|
||||||
let chunks = this.state.chunks;
|
let chunks = this.state.chunks;
|
||||||
|
|
||||||
@ -514,7 +592,14 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
|
|
||||||
this.setState({ startedChunks });
|
this.setState({ startedChunks });
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onAllComplete(succeed, failed) {
|
||||||
|
if (this.state.uploadInProgress) {
|
||||||
|
this.setState({
|
||||||
|
uploadInProgress: false
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onComplete(id, name, res, xhr) {
|
onComplete(id, name, res, xhr) {
|
||||||
@ -526,12 +611,12 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
xhr: this.getXhrErrorComment(xhr)
|
xhr: this.getXhrErrorComment(xhr)
|
||||||
});
|
});
|
||||||
// onError will catch any errors, so we can ignore them here
|
// onError will catch any errors, so we can ignore them here
|
||||||
} else if (!res.error || res.success) {
|
} else if (!res.error && res.success) {
|
||||||
let files = this.state.filesToUpload;
|
let files = this.state.filesToUpload;
|
||||||
|
|
||||||
// Set the state of the completed file to 'upload successful' in order to
|
// Set the state of the completed file to 'upload successful' in order to
|
||||||
// remove it from the GUI
|
// remove it from the GUI
|
||||||
files[id].status = 'upload successful';
|
files[id].status = FileStatus.UPLOAD_SUCCESSFUL;
|
||||||
files[id].key = this.state.uploader.getKey(id);
|
files[id].key = this.state.uploader.getKey(id);
|
||||||
|
|
||||||
let filesToUpload = React.addons.update(this.state.filesToUpload, { $set: files });
|
let filesToUpload = React.addons.update(this.state.filesToUpload, { $set: files });
|
||||||
@ -540,29 +625,14 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
// Only after the blob has been created server-side, we can make the form submittable.
|
// Only after the blob has been created server-side, we can make the form submittable.
|
||||||
this.createBlob(files[id])
|
this.createBlob(files[id])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
|
if (typeof this.props.submitFile === 'function') {
|
||||||
// are optional, we'll only trigger them when they're actually defined
|
|
||||||
if(this.props.submitFile) {
|
|
||||||
this.props.submitFile(files[id]);
|
this.props.submitFile(files[id]);
|
||||||
} else {
|
} else {
|
||||||
console.warn('You didn\'t define submitFile as a prop in react-s3-fine-uploader');
|
console.warn('You didn\'t define submitFile as a prop in react-s3-fine-uploader');
|
||||||
}
|
}
|
||||||
|
|
||||||
// for explanation, check comment of if statement above
|
this.checkFormSubmissionReady();
|
||||||
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
|
});
|
||||||
// also, lets check if after the completion of this upload,
|
|
||||||
// the form is ready for submission or not
|
|
||||||
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
|
|
||||||
// if so, set uploadstatus to true
|
|
||||||
this.props.setIsUploadReady(true);
|
|
||||||
} else {
|
|
||||||
this.props.setIsUploadReady(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(this.onErrorPromiseProxy);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -578,22 +648,51 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onError(id, name, errorReason, xhr) {
|
onError(id, name, errorReason, xhr) {
|
||||||
|
const { errorNotificationMessage, showErrorPrompt } = this.props;
|
||||||
|
const { chunks, filesToUpload } = this.state;
|
||||||
|
|
||||||
console.logGlobal(errorReason, {
|
console.logGlobal(errorReason, {
|
||||||
files: this.state.filesToUpload,
|
files: filesToUpload,
|
||||||
chunks: this.state.chunks,
|
chunks: chunks,
|
||||||
xhr: this.getXhrErrorComment(xhr)
|
xhr: this.getXhrErrorComment(xhr)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.setIsUploadReady(true);
|
let notificationMessage;
|
||||||
this.cancelUploads();
|
|
||||||
|
|
||||||
let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000);
|
if (showErrorPrompt) {
|
||||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
this.setStatusOfFile(id, FileStatus.UPLOAD_FAILED);
|
||||||
|
|
||||||
|
// If we've already found an error on this upload, just ignore other errors
|
||||||
|
// that pop up. They'll likely pop up again when the user retries.
|
||||||
|
if (!this.state.errorState.errorClass) {
|
||||||
|
notificationMessage = errorNotificationMessage;
|
||||||
|
|
||||||
|
const errorState = React.addons.update(this.state.errorState, {
|
||||||
|
errorClass: {
|
||||||
|
$set: this.getUploadErrorClass({
|
||||||
|
reason: errorReason,
|
||||||
|
xhr
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({ errorState });
|
||||||
|
this.setWarning(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notificationMessage = errorReason || errorNotificationMessage;
|
||||||
|
this.cancelUploads();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationMessage) {
|
||||||
|
const notification = new GlobalNotificationModel(notificationMessage, 'danger', 5000);
|
||||||
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onCancel(id) {
|
onCancel(id) {
|
||||||
// when a upload is canceled, we need to update this components file array
|
// when a upload is canceled, we need to update this components file array
|
||||||
this.setStatusOfFile(id, 'canceled')
|
this.setStatusOfFile(id, FileStatus.CANCELED)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if(typeof this.props.handleChangedFile === 'function') {
|
if(typeof this.props.handleChangedFile === 'function') {
|
||||||
this.props.handleChangedFile(this.state.filesToUpload[id]);
|
this.props.handleChangedFile(this.state.filesToUpload[id]);
|
||||||
@ -603,17 +702,18 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000);
|
let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000);
|
||||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||||
|
|
||||||
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
|
this.checkFormSubmissionReady();
|
||||||
// are optional, we'll only trigger them when they're actually defined
|
|
||||||
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
|
// FineUploader's onAllComplete event doesn't fire if all files are cancelled
|
||||||
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
|
// so we need to double check if this is the last file getting cancelled.
|
||||||
// if so, set uploadstatus to true
|
//
|
||||||
this.props.setIsUploadReady(true);
|
// Because we're calling FineUploader.getInProgress() in a cancel callback,
|
||||||
} else {
|
// the current file getting cancelled is still considered to be in progress
|
||||||
this.props.setIsUploadReady(false);
|
// so there will be one file left in progress when we're cancelling the last file.
|
||||||
}
|
if (this.state.uploader.getInProgress() === 1) {
|
||||||
} else {
|
this.setState({
|
||||||
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
|
uploadInProgress: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -633,7 +733,7 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
// fetch blobs for images
|
// fetch blobs for images
|
||||||
response = response.map((file) => {
|
response = response.map((file) => {
|
||||||
file.url = file.s3UrlSafe;
|
file.url = file.s3UrlSafe;
|
||||||
file.status = 'online';
|
file.status = FileStatus.ONLINE;
|
||||||
file.progress = 100;
|
file.progress = 100;
|
||||||
return file;
|
return file;
|
||||||
});
|
});
|
||||||
@ -661,7 +761,7 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
|
|
||||||
onDeleteComplete(id, xhr, isError) {
|
onDeleteComplete(id, xhr, isError) {
|
||||||
if(isError) {
|
if(isError) {
|
||||||
this.setStatusOfFile(id, 'online');
|
this.setStatusOfFile(id, FileStatus.ONLINE);
|
||||||
|
|
||||||
let notification = new GlobalNotificationModel(getLangText('There was an error deleting your file.'), 'danger', 10000);
|
let notification = new GlobalNotificationModel(getLangText('There was an error deleting your file.'), 'danger', 10000);
|
||||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||||
@ -670,29 +770,16 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
|
this.checkFormSubmissionReady();
|
||||||
// are optional, we'll only trigger them when they're actually defined
|
|
||||||
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
|
|
||||||
// also, lets check if after the completion of this upload,
|
|
||||||
// the form is ready for submission or not
|
|
||||||
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
|
|
||||||
// if so, set uploadstatus to true
|
|
||||||
this.props.setIsUploadReady(true);
|
|
||||||
} else {
|
|
||||||
this.props.setIsUploadReady(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDeleteFile(fileId) {
|
handleDeleteFile(fileId) {
|
||||||
// We set the files state to 'deleted' immediately, so that the user is not confused with
|
// We set the files state to 'deleted' immediately, so that the user is not confused with
|
||||||
// the unresponsiveness of the UI
|
// the unresponsiveness of the UI
|
||||||
//
|
//
|
||||||
// If there is an error during the deletion, we will just change the status back to 'online'
|
// If there is an error during the deletion, we will just change the status back to FileStatus.ONLINE
|
||||||
// and display an error message
|
// and display an error message
|
||||||
this.setStatusOfFile(fileId, 'deleted')
|
this.setStatusOfFile(fileId, FileStatus.DELETED)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if(typeof this.props.handleChangedFile === 'function') {
|
if(typeof this.props.handleChangedFile === 'function') {
|
||||||
this.props.handleChangedFile(this.state.filesToUpload[fileId]);
|
this.props.handleChangedFile(this.state.filesToUpload[fileId]);
|
||||||
@ -708,7 +795,7 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
// To check which files are already uploaded from previous sessions we check their status.
|
// To check which files are already uploaded from previous sessions we check their status.
|
||||||
// If they are, it is "online"
|
// If they are, it is "online"
|
||||||
|
|
||||||
if(this.state.filesToUpload[fileId].status !== 'online') {
|
if(this.state.filesToUpload[fileId].status !== FileStatus.ONLINE) {
|
||||||
// delete file from server
|
// delete file from server
|
||||||
this.state.uploader.deleteFile(fileId);
|
this.state.uploader.deleteFile(fileId);
|
||||||
// this is being continued in onDeleteFile, as
|
// this is being continued in onDeleteFile, as
|
||||||
@ -736,7 +823,7 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
|
|
||||||
handlePauseFile(fileId) {
|
handlePauseFile(fileId) {
|
||||||
if(this.state.uploader.pauseUpload(fileId)) {
|
if(this.state.uploader.pauseUpload(fileId)) {
|
||||||
this.setStatusOfFile(fileId, 'paused');
|
this.setStatusOfFile(fileId, FileStatus.PAUSED);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(getLangText('File upload could not be paused.'));
|
throw new Error(getLangText('File upload could not be paused.'));
|
||||||
}
|
}
|
||||||
@ -744,12 +831,35 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
|
|
||||||
handleResumeFile(fileId) {
|
handleResumeFile(fileId) {
|
||||||
if(this.state.uploader.continueUpload(fileId)) {
|
if(this.state.uploader.continueUpload(fileId)) {
|
||||||
this.setStatusOfFile(fileId, 'uploading');
|
this.setStatusOfFile(fileId, FileStatus.UPLOADING);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(getLangText('File upload could not be resumed.'));
|
throw new Error(getLangText('File upload could not be resumed.'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleRetryFiles(fileIds) {
|
||||||
|
let filesToUpload = this.state.filesToUpload;
|
||||||
|
|
||||||
|
if (fileIds.constructor !== Array) {
|
||||||
|
fileIds = [ fileIds ];
|
||||||
|
}
|
||||||
|
|
||||||
|
fileIds.forEach((fileId) => {
|
||||||
|
this.state.uploader.retry(fileId);
|
||||||
|
filesToUpload = React.addons.update(filesToUpload, { [fileId]: { status: { $set: FileStatus.UPLOADING } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
// Reset the error class along with the retry
|
||||||
|
errorState: {
|
||||||
|
manualRetryAttempt: this.state.errorState.manualRetryAttempt + 1
|
||||||
|
},
|
||||||
|
filesToUpload
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setWarning(false);
|
||||||
|
},
|
||||||
|
|
||||||
handleUploadFile(files) {
|
handleUploadFile(files) {
|
||||||
// While files are being uploaded, the form cannot be ready
|
// While files are being uploaded, the form cannot be ready
|
||||||
// for submission
|
// for submission
|
||||||
@ -870,6 +980,9 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
if(files.length > 0) {
|
if(files.length > 0) {
|
||||||
this.state.uploader.addFiles(files);
|
this.state.uploader.addFiles(files);
|
||||||
this.synchronizeFileLists(files);
|
this.synchronizeFileLists(files);
|
||||||
|
this.setState({
|
||||||
|
uploadInProgress: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -910,12 +1023,12 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
//
|
//
|
||||||
// If the user deletes one of those files, then fineuploader will still keep it in his
|
// If the user deletes one of those files, then fineuploader will still keep it in his
|
||||||
// files array but with key, progress undefined and size === -1 but
|
// files array but with key, progress undefined and size === -1 but
|
||||||
// status === 'upload successful'.
|
// status === FileStatus.UPLOAD_SUCCESSFUL.
|
||||||
// This poses a problem as we depend on the amount of files that have
|
// This poses a problem as we depend on the amount of files that have
|
||||||
// status === 'upload successful', therefore once the file is synced,
|
// status === FileStatus.UPLOAD_SUCCESSFUL, therefore once the file is synced,
|
||||||
// we need to tag its status as 'deleted' (which basically happens here)
|
// we need to tag its status as FileStatus.DELETED (which basically happens here)
|
||||||
if(oldAndNewFiles[i].size === -1 && (!oldAndNewFiles[i].progress || oldAndNewFiles[i].progress === 0)) {
|
if(oldAndNewFiles[i].size === -1 && (!oldAndNewFiles[i].progress || oldAndNewFiles[i].progress === 0)) {
|
||||||
oldAndNewFiles[i].status = 'deleted';
|
oldAndNewFiles[i].status = FileStatus.DELETED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(oldAndNewFiles[i].originalName === oldFiles[j].name) {
|
if(oldAndNewFiles[i].originalName === oldFiles[j].name) {
|
||||||
@ -944,14 +1057,19 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { errorState: { errorClass }, filesToUpload, uploadInProgress } = this.state;
|
||||||
const {
|
const {
|
||||||
multiple,
|
areAssetsDownloadable,
|
||||||
areAssetsDownloadable,
|
areAssetsEditable,
|
||||||
areAssetsEditable,
|
enableLocalHashing,
|
||||||
enableLocalHashing,
|
fileClassToUpload,
|
||||||
fileClassToUpload,
|
fileInputElement: FileInputElement,
|
||||||
fileInputElement: FileInputElement,
|
multiple,
|
||||||
uploadMethod } = this.props;
|
showErrorPrompt,
|
||||||
|
uploadMethod } = this.props;
|
||||||
|
|
||||||
|
// Only show the error state once all files are finished
|
||||||
|
const showError = !uploadInProgress && showErrorPrompt && errorClass != null;
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
multiple,
|
multiple,
|
||||||
@ -960,12 +1078,16 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
enableLocalHashing,
|
enableLocalHashing,
|
||||||
uploadMethod,
|
uploadMethod,
|
||||||
fileClassToUpload,
|
fileClassToUpload,
|
||||||
|
filesToUpload,
|
||||||
|
uploadInProgress,
|
||||||
|
errorClass,
|
||||||
|
showError,
|
||||||
onDrop: this.handleUploadFile,
|
onDrop: this.handleUploadFile,
|
||||||
filesToUpload: this.state.filesToUpload,
|
|
||||||
handleDeleteFile: this.handleDeleteFile,
|
handleDeleteFile: this.handleDeleteFile,
|
||||||
handleCancelFile: this.handleCancelFile,
|
handleCancelFile: this.handleCancelFile,
|
||||||
handlePauseFile: this.handlePauseFile,
|
handlePauseFile: this.handlePauseFile,
|
||||||
handleResumeFile: this.handleResumeFile,
|
handleResumeFile: this.handleResumeFile,
|
||||||
|
handleRetryFiles: this.handleRetryFiles,
|
||||||
handleCancelHashing: this.handleCancelHashing,
|
handleCancelHashing: this.handleCancelHashing,
|
||||||
dropzoneInactive: this.isDropzoneInactive(),
|
dropzoneInactive: this.isDropzoneInactive(),
|
||||||
hashingProgress: this.state.hashingProgress,
|
hashingProgress: this.state.hashingProgress,
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import fineUploader from 'fineUploader';
|
||||||
import MimeTypes from '../../constants/mime_types';
|
import MimeTypes from '../../constants/mime_types';
|
||||||
|
|
||||||
|
|
||||||
|
// Re-export qq.status from FineUploader with an additional online
|
||||||
|
// state that we use to keep track of files from S3.
|
||||||
|
export const FileStatus = Object.assign({}, fineUploader.status, {
|
||||||
|
ONLINE: 'online'
|
||||||
|
});
|
||||||
|
|
||||||
export const formSubmissionValidation = {
|
export const formSubmissionValidation = {
|
||||||
/**
|
/**
|
||||||
* Returns a boolean if there has been at least one file uploaded
|
* Returns a boolean if there has been at least one file uploaded
|
||||||
@ -10,8 +18,13 @@ export const formSubmissionValidation = {
|
|||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
atLeastOneUploadedFile(files) {
|
atLeastOneUploadedFile(files) {
|
||||||
files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled');
|
files = files.filter((file) => {
|
||||||
if (files.length > 0 && files[0].status === 'upload successful') {
|
return file.status !== FileStatus.DELETED &&
|
||||||
|
file.status !== FileStatus.CANCELED &&
|
||||||
|
file.status != FileStatus.UPLOADED_FAILED
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.length && files[0].status === FileStatus.UPLOAD_SUCCESSFUL) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
@ -25,32 +38,32 @@ export const formSubmissionValidation = {
|
|||||||
* @return {boolean} [description]
|
* @return {boolean} [description]
|
||||||
*/
|
*/
|
||||||
fileOptional(files) {
|
fileOptional(files) {
|
||||||
let uploadingFiles = files.filter((file) => file.status === 'submitting');
|
const uploadingFiles = files.filter((file) => file.status === FileStatus.SUBMITTING);
|
||||||
|
|
||||||
if (uploadingFiles.length === 0) {
|
return uploadFiles.length === 0;
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter function for filtering all deleted and canceled files
|
* Filter function for filtering all deleted, canceled, and failed files
|
||||||
* @param {object} file A file from filesToUpload that has status as a prop.
|
* @param {object} file A file from filesToUpload that has status as a prop.
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
export function displayValidFilesFilter(file) {
|
export function displayValidFilesFilter(file) {
|
||||||
return file.status !== 'deleted' && file.status !== 'canceled';
|
return file.status !== FileStatus.DELETED &&
|
||||||
|
file.status !== FileStatus.CANCELED &&
|
||||||
|
file.status !== FileStatus.UPLOAD_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter function for filtering all files except for deleted and canceled files
|
* Filter function for filtering all files except for deleted, canceled, and failed files
|
||||||
* @param {object} file A file from filesToUpload that has status as a prop.
|
* @param {object} file A file from filesToUpload that has status as a prop.
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
export function displayRemovedFilesFilter(file) {
|
export function displayRemovedFilesFilter(file) {
|
||||||
return file.status === 'deleted' || file.status === 'canceled';
|
return file.status === FileStatus.DELETED ||
|
||||||
|
file.status === FileStatus.CANCELED ||
|
||||||
|
file.status === FileStatus.UPLOAD_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -60,7 +73,10 @@ export function displayRemovedFilesFilter(file) {
|
|||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
export function displayValidProgressFilesFilter(file) {
|
export function displayValidProgressFilesFilter(file) {
|
||||||
return file.status !== 'deleted' && file.status !== 'canceled' && file.status !== 'online';
|
return file.status !== FileStatus.DELETED &&
|
||||||
|
file.status !== FileStatus.CANCELED &&
|
||||||
|
file.status !== FileStatus.UPLOAD_FAILED &&
|
||||||
|
file.status !== FileStatus.ONLINE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -77,7 +93,7 @@ export function displayValidProgressFilesFilter(file) {
|
|||||||
export function transformAllowedExtensionsToInputAcceptProp(allowedExtensions) {
|
export function transformAllowedExtensionsToInputAcceptProp(allowedExtensions) {
|
||||||
// Get the mime type of the extension if it's defined or add a dot in front of the extension
|
// Get the mime type of the extension if it's defined or add a dot in front of the extension
|
||||||
// This is important for Safari as it doesn't understand just the extension.
|
// This is important for Safari as it doesn't understand just the extension.
|
||||||
let prefixedAllowedExtensions = allowedExtensions.map((ext) => {
|
const prefixedAllowedExtensions = allowedExtensions.map((ext) => {
|
||||||
return MimeTypes[ext] || ('.' + ext);
|
return MimeTypes[ext] || ('.' + ext);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
227
js/constants/error_constants.js
Normal file
227
js/constants/error_constants.js
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
import { validationParts } from './uploader_constants';
|
||||||
|
|
||||||
|
import { deepMatchObject } from '../utils/general_utils';
|
||||||
|
import { getLangText } from '../utils/lang_utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ErrorClasses
|
||||||
|
* ============
|
||||||
|
* Known error classes based on groupings (ie. where they happened, which component, etc).
|
||||||
|
*
|
||||||
|
* Error classes have a test object that can be used to test whether or not an error
|
||||||
|
* object matches that specific class. Properties in the test object will be recursively
|
||||||
|
* checked in the error object, and the error object is only matched to the class if all
|
||||||
|
* tests succeed. See testErrorAgainstClass() below for the implementation of the matching.
|
||||||
|
*
|
||||||
|
* ErrorClasses.default.default is the generic error for errors not identified under any
|
||||||
|
* grouping and class.
|
||||||
|
*
|
||||||
|
* Format:
|
||||||
|
* ErrorClasses = {
|
||||||
|
* 'errorGrouping': {
|
||||||
|
* 'errorClassName': ErrorClass
|
||||||
|
* ...
|
||||||
|
* },
|
||||||
|
* ...
|
||||||
|
* 'default': {
|
||||||
|
* ...
|
||||||
|
* 'default': generic error for errors that don't fall under any grouping and class
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Each class is of the format:
|
||||||
|
* ErrorClass = {
|
||||||
|
* 'name': name of the class
|
||||||
|
* 'group': grouping of the error,
|
||||||
|
* 'prettifiedText': prettified text for the class
|
||||||
|
* 'test': {
|
||||||
|
* prop1: property in the error object to recursively match against using
|
||||||
|
* either === or, if the property is a string, substring match
|
||||||
|
* (ie. indexOf() >= 0)
|
||||||
|
* ...
|
||||||
|
* },
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Test object examples
|
||||||
|
* ====================
|
||||||
|
* A class like this:
|
||||||
|
*
|
||||||
|
* 'errorClass': {
|
||||||
|
* 'test': {
|
||||||
|
* 'reason': 'Invalid server response',
|
||||||
|
* 'xhr': {
|
||||||
|
* 'response': 'Internal error',
|
||||||
|
* 'status': 500
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* will match this error object:
|
||||||
|
*
|
||||||
|
* error = {
|
||||||
|
* 'reason': 'Invalid server response',
|
||||||
|
* 'xhr': { // Simplified version of the XMLHttpRequest object responsible for the failure
|
||||||
|
* 'response': 'Internal error',
|
||||||
|
* 'status': 500
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* but will *NOT* match this error object:
|
||||||
|
*
|
||||||
|
* error = {
|
||||||
|
* 'reason': 'Invalid server response',
|
||||||
|
* 'xhr': {
|
||||||
|
* 'response': 'Unauthorized',
|
||||||
|
* 'status': 401
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* A common use case is for the test to just be against the error.reason string.
|
||||||
|
* In these cases, setting the test object to be just a string will enforce this test,
|
||||||
|
* so something like this:
|
||||||
|
*
|
||||||
|
* 'errorClass': {
|
||||||
|
* 'test': {
|
||||||
|
* 'reason': 'Invalid server response'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* is the same as:
|
||||||
|
*
|
||||||
|
* 'errorClass': {
|
||||||
|
* 'test': 'Invalid server response'
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
const ErrorClasses = {
|
||||||
|
'upload': {
|
||||||
|
'requestTimeTooSkewed': {
|
||||||
|
'prettifiedText': getLangText('Check your time and date preferences and select "set date and time ' +
|
||||||
|
'automatically." Being off by a few minutes from our servers can ' +
|
||||||
|
'prevent your upload.'),
|
||||||
|
'test': {
|
||||||
|
'xhr': {
|
||||||
|
'response': 'RequestTimeTooSkewed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'chunkSignatureError': {
|
||||||
|
'prettifiedText': getLangText("We're experiencing some problems with uploads at the moment and " +
|
||||||
|
'are working to resolve them. Please try again in a few hours.'),
|
||||||
|
'test': 'Problem signing the chunk'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fallback error tips
|
||||||
|
'largeFileSize': {
|
||||||
|
'prettifiedText': getLangText(`We handle files up to ${validationParts.sizeLimit.default / 1000000000}GB ` +
|
||||||
|
'but your Internet connection may not. With large files and limited ' +
|
||||||
|
'bandwith, it may take some time to complete. If it doesn’t seem to ' +
|
||||||
|
'progress at all, try restarting the process.')
|
||||||
|
},
|
||||||
|
'tryDifferentBrowser': {
|
||||||
|
'prettifiedText': getLangText("We're still having trouble uploading your file. It might be your " +
|
||||||
|
"browser; try a different browser or make sure you’re using the " +
|
||||||
|
'latest version.')
|
||||||
|
},
|
||||||
|
'contactUs': {
|
||||||
|
'prettifiedText': getLangText("We're having a really hard time with your upload. Please contact us for more help.")
|
||||||
|
},
|
||||||
|
'offline': {
|
||||||
|
'prettifiedText': getLangText('It looks like your Internet connection might have gone down during the upload. Please check your connection and try again.')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'default': {
|
||||||
|
'default': {
|
||||||
|
'prettifiedText': getLangText("It looks like there's been a problem on our end. If you keep experiencing this error, please contact us.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dynamically inject the name and group properties into the classes
|
||||||
|
Object.keys(ErrorClasses).forEach((errorGroupKey) => {
|
||||||
|
const errorGroup = ErrorClasses[errorGroupKey];
|
||||||
|
Object.keys(errorGroup).forEach((errorClassKey) => {
|
||||||
|
const errorClass = errorGroup[errorClassKey];
|
||||||
|
errorClass.name = errorGroupKey + '-' + errorClassKey;
|
||||||
|
errorClass.group = errorGroupKey;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns prettified text for a given error by trying to match it to
|
||||||
|
* a known error in ErrorClasses or the given class.
|
||||||
|
*
|
||||||
|
* One should provide a class (eg. ErrorClasses.upload.requestTimeTooSkewed)
|
||||||
|
* if they already have an error in mind that they want to match against rather
|
||||||
|
* than all the available error classes.
|
||||||
|
*
|
||||||
|
* @param {object} error An error with the following:
|
||||||
|
* @param {string} error.type Type of error
|
||||||
|
* @param {string} error.reason Reason of error
|
||||||
|
* @param {(XMLHttpRequest)} error.xhr XHR associated with the error
|
||||||
|
* @param {(any)} error.* Any other property as necessary
|
||||||
|
*
|
||||||
|
* @param {(object)} errorClass ErrorClass to match against the given error.
|
||||||
|
* Signature should be similar to ErrorClasses' classes (see above).
|
||||||
|
* @param {object|string} errorClass.test Test object to recursively match against the given error
|
||||||
|
* @param {string} errorClass.prettifiedText Prettified text to return if the test matches
|
||||||
|
*
|
||||||
|
* @return {string} Prettified error string. Returns the default error string if no
|
||||||
|
* error class was matched to the given error.
|
||||||
|
*/
|
||||||
|
function getPrettifiedError(error, errorClass) {
|
||||||
|
const matchedClass = errorClass ? testErrorAgainstClass(error, errorClass) : testErrorAgainstAll(error);
|
||||||
|
return (matchedClass && matchedClass.prettifiedText) || ErrorClasses.default.default.prettifiedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the given error against all items in ErrorClasses and returns
|
||||||
|
* the matching class if available.
|
||||||
|
* See getPrettifiedError() for the signature of @param error.
|
||||||
|
* @return {(object)} Matched error class
|
||||||
|
*/
|
||||||
|
function testErrorAgainstAll(error) {
|
||||||
|
const type = error.type || 'default';
|
||||||
|
const errorGroup = ErrorClasses[type];
|
||||||
|
|
||||||
|
return Object
|
||||||
|
.keys(errorGroup)
|
||||||
|
.reduce((result, key) => {
|
||||||
|
return result || testErrorAgainstClass(error, errorGroup[key]);
|
||||||
|
}, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the error against the class by recursively testing the
|
||||||
|
* class's test object against the error.
|
||||||
|
* Implements the test matching behaviour described in ErrorClasses.
|
||||||
|
*
|
||||||
|
* See getPrettifiedError() for the signatures of @param error and @param errorClass.
|
||||||
|
* @return {(object)} Returns the given class if the test succeeds.
|
||||||
|
*/
|
||||||
|
function testErrorAgainstClass(error, errorClass) {
|
||||||
|
// Automatically fail classes if no tests present, since some of the error classes
|
||||||
|
// may not have an error to test against.
|
||||||
|
if (!errorClass.test) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof errorClass.test === 'string') {
|
||||||
|
errorClass.test = {
|
||||||
|
reason: errorClass.test
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return deepMatchObject(error, errorClass.test, (objProp, matchProp) => {
|
||||||
|
return (objProp === matchProp || (typeof objProp === 'string' && objProp.indexOf(matchProp) >= 0));
|
||||||
|
}) ? errorClass : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to export with the clause syntax as we change ErrorClasses after its declaration.
|
||||||
|
export {
|
||||||
|
ErrorClasses,
|
||||||
|
getPrettifiedError,
|
||||||
|
testErrorAgainstAll,
|
||||||
|
testErrorAgainstClass
|
||||||
|
};
|
@ -30,3 +30,6 @@ export const validationTypes = {
|
|||||||
sizeLimit: sizeLimit.thumbnail
|
sizeLimit: sizeLimit.thumbnail
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Number of manual retries before showing a contact us screen on the uploader.
|
||||||
|
export const RETRY_ATTEMPT_TO_SHOW_CONTACT_US = 5;
|
||||||
|
56
js/stores/error_queue_store.js
Normal file
56
js/stores/error_queue_store.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { alt } from '../alt';
|
||||||
|
|
||||||
|
import ErrorQueueActions from '../actions/error_queue_actions.js';
|
||||||
|
|
||||||
|
import { ErrorClasses } from '../constants/error_constants.js';
|
||||||
|
|
||||||
|
class ErrorQueueStore {
|
||||||
|
constructor() {
|
||||||
|
const { upload: { largeFileSize, tryDifferentBrowser } } = ErrorClasses;
|
||||||
|
|
||||||
|
this.errorQueues = {
|
||||||
|
'upload': {
|
||||||
|
queue: [largeFileSize, tryDifferentBrowser],
|
||||||
|
loop: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add intial index to each error queue
|
||||||
|
Object
|
||||||
|
.keys(this.errorQueues)
|
||||||
|
.forEach((type) => {
|
||||||
|
this.errorQueues[type].index = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind the exported functions to this instance
|
||||||
|
this.getNextError = this.getNextError.bind(this);
|
||||||
|
|
||||||
|
this.exportPublicMethods({
|
||||||
|
getNextError: this.getNextError
|
||||||
|
});
|
||||||
|
this.bindActions(ErrorQueueActions);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextError(type) {
|
||||||
|
const errorQueue = this.errorQueues[type];
|
||||||
|
const { queue, index } = errorQueue;
|
||||||
|
|
||||||
|
ErrorQueueActions.shiftErrorQueue(type);
|
||||||
|
return queue[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
onShiftErrorQueue(type) {
|
||||||
|
const errorQueue = this.errorQueues[type];
|
||||||
|
const { queue, loop } = errorQueue;
|
||||||
|
|
||||||
|
++errorQueue.index;
|
||||||
|
if (loop) {
|
||||||
|
// Loop back to the beginning if all errors have been exhausted
|
||||||
|
errorQueue.index %= queue.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default alt.createStore(ErrorQueueStore, 'ErrorQueueStore');
|
@ -4,7 +4,6 @@ import Raven from 'raven-js';
|
|||||||
|
|
||||||
import AppConstants from '../constants/application_constants';
|
import AppConstants from '../constants/application_constants';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs an error in to the console but also sends it to
|
* Logs an error in to the console but also sends it to
|
||||||
* Sentry.
|
* Sentry.
|
||||||
|
@ -223,15 +223,12 @@ export function omitFromObject(obj, filter) {
|
|||||||
* By default, applies strict equality using ===
|
* By default, applies strict equality using ===
|
||||||
* @return {boolean} True if obj matches the "match" object
|
* @return {boolean} True if obj matches the "match" object
|
||||||
*/
|
*/
|
||||||
export function deepMatchObject(obj, match, testFn) {
|
export function deepMatchObject(obj, match, testFn = (objProp, matchProp) => objProp === matchProp) {
|
||||||
if (typeof match !== 'object') {
|
if (typeof match !== 'object') {
|
||||||
throw new Error('Your specified match argument was not an object');
|
throw new Error('Your specified match argument was not an object');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof testFn !== 'function') {
|
if (typeof testFn !== 'function') {
|
||||||
testFn = (objProp, matchProp) => {
|
throw new Error('Your specified test function was not a function');
|
||||||
return objProp === matchProp;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object
|
return Object
|
||||||
@ -239,7 +236,7 @@ export function deepMatchObject(obj, match, testFn) {
|
|||||||
.reduce((result, matchKey) => {
|
.reduce((result, matchKey) => {
|
||||||
if (!result) { return false; }
|
if (!result) { return false; }
|
||||||
|
|
||||||
const objProp = obj[matchKey];
|
const objProp = obj && obj[matchKey];
|
||||||
const matchProp = match[matchKey];
|
const matchProp = match[matchKey];
|
||||||
|
|
||||||
if (typeof matchProp === 'object') {
|
if (typeof matchProp === 'object') {
|
||||||
|
@ -48,6 +48,10 @@ $ascribe-red-error: rgb(169, 68, 66);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-warning {
|
||||||
|
border-left: 3px solid $ascribe-pink
|
||||||
|
}
|
||||||
|
|
||||||
.is-fixed {
|
.is-fixed {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
|
@ -160,6 +160,165 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-drag-and-drop-error {
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: $ascribe-pink;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding-left: 45px;
|
||||||
|
padding-right: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make button larger on mobile */
|
||||||
|
@media screen and (max-width: 625px) {
|
||||||
|
.btn {
|
||||||
|
padding: 10px 100px 10px 100px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-drag-and-drop-error-detail {
|
||||||
|
float: left;
|
||||||
|
padding-right: 25px;
|
||||||
|
text-align: left;
|
||||||
|
width: 40%;
|
||||||
|
|
||||||
|
&.file-drag-and-drop-error-detail-multiple-files {
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Have detail fill up entire width on mobile */
|
||||||
|
@media screen and (max-width: 625px) {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.file-drag-and-drop-error-detail-multiple-files {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-drag-and-drop-error-file-names {
|
||||||
|
float: left;
|
||||||
|
height: 104px;
|
||||||
|
max-width: 35%;
|
||||||
|
padding-left: 25px;
|
||||||
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
-webkit-transform: translateY(-50%);
|
||||||
|
-ms-transform: translateY(-50%);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
|
||||||
|
li {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drop down file names under the retry button on mobile */
|
||||||
|
@media screen and (max-width: 625px) {
|
||||||
|
height: auto;
|
||||||
|
max-width: none;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
top: 0;
|
||||||
|
-webkit-transform: none;
|
||||||
|
-ms-transform: none;
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 25%;
|
||||||
|
padding-right: 10px;
|
||||||
|
|
||||||
|
&:not(:last-child)::after {
|
||||||
|
content: ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-drag-and-drop-error-icon-container {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
display: table;
|
||||||
|
float: left;
|
||||||
|
height: 104px;
|
||||||
|
position: relative;
|
||||||
|
width: 104px;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
.icon-ascribe-thin-cross {
|
||||||
|
display: table-cell;
|
||||||
|
font-size: 5.5em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.file-drag-and-drop-error-icon-container-multiple-files {
|
||||||
|
background-color: #d7d7d7;
|
||||||
|
left: -15px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
display: block;
|
||||||
|
height: 104px;
|
||||||
|
left: 15px;
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
width: 104px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
display: block;
|
||||||
|
height: 104px;
|
||||||
|
left: 30px;
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
width: 104px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ascribe-thin-cross {
|
||||||
|
left: 44px;
|
||||||
|
position: absolute;
|
||||||
|
top: 38px;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the icon when the screen is too small */
|
||||||
|
@media screen and (max-width: 625px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ascribe-progress-bar {
|
.ascribe-progress-bar {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
> .progress-bar {
|
> .progress-bar {
|
||||||
|
Loading…
Reference in New Issue
Block a user