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';
|
||||
|
||||
|
||||
const { func, bool, number, object, string, arrayOf } = React.PropTypes;
|
||||
|
||||
let FurtherDetailsFileuploader = React.createClass({
|
||||
propTypes: {
|
||||
pieceId: React.PropTypes.number.isRequired,
|
||||
pieceId: number.isRequired,
|
||||
|
||||
areAssetsDownloadable: React.PropTypes.bool,
|
||||
editable: React.PropTypes.bool,
|
||||
isReadyForFormSubmission: React.PropTypes.func,
|
||||
label: React.PropTypes.string,
|
||||
multiple: React.PropTypes.bool,
|
||||
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
onValidationFailed: React.PropTypes.func,
|
||||
setIsUploadReady: React.PropTypes.func,
|
||||
submitFile: React.PropTypes.func,
|
||||
editable: bool,
|
||||
label: string,
|
||||
otherData: arrayOf(object),
|
||||
|
||||
// Props for ReactS3FineUploader
|
||||
areAssetsDownloadable: bool,
|
||||
isReadyForFormSubmission: func,
|
||||
submitFile: func, // TODO: rename to onSubmitFile
|
||||
onValidationFailed: func,
|
||||
multiple: bool,
|
||||
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
|
||||
showErrorPrompt: bool,
|
||||
validation: ReactS3FineUploader.propTypes.validation
|
||||
},
|
||||
|
||||
@ -40,36 +45,57 @@ let FurtherDetailsFileuploader = React.createClass({
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
editable,
|
||||
isReadyForFormSubmission,
|
||||
multiple,
|
||||
onValidationFailed,
|
||||
otherData,
|
||||
pieceId,
|
||||
setIsUploadReady,
|
||||
showErrorPrompt,
|
||||
submitFile,
|
||||
validation } = this.props;
|
||||
|
||||
// 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)
|
||||
// 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
|
||||
if (!this.props.editable && (!this.props.otherData || this.props.otherData.length === 0)) {
|
||||
if (!editable && (!otherData || otherData.length === 0)) {
|
||||
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 (
|
||||
<Property
|
||||
name="other_data_key"
|
||||
label={this.props.label}>
|
||||
<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={{
|
||||
url: AppConstants.serverUrl + 's3/key/',
|
||||
fileClass: 'otherdata',
|
||||
pieceId: this.props.pieceId
|
||||
pieceId: pieceId
|
||||
}}
|
||||
createBlobRoutine={{
|
||||
url: ApiUrls.blob_otherdatas,
|
||||
pieceId: this.props.pieceId
|
||||
}}
|
||||
validation={this.props.validation}
|
||||
submitFile={this.props.submitFile}
|
||||
onValidationFailed={this.props.onValidationFailed}
|
||||
setIsUploadReady={this.props.setIsUploadReady}
|
||||
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
|
||||
multiple={multiple}
|
||||
onValidationFailed={onValidationFailed}
|
||||
setIsUploadReady={setIsUploadReady}
|
||||
session={{
|
||||
endpoint: AppConstants.serverUrl + 'api/blob/otherdatas/fineuploader_session/',
|
||||
customHeaders: {
|
||||
@ -89,17 +115,9 @@ let FurtherDetailsFileuploader = React.createClass({
|
||||
'X-CSRFToken': getCookie(AppConstants.csrftoken)
|
||||
}
|
||||
}}
|
||||
deleteFile={{
|
||||
enabled: true,
|
||||
method: 'DELETE',
|
||||
endpoint: AppConstants.serverUrl + 's3/delete',
|
||||
customHeaders: {
|
||||
'X-CSRFToken': getCookie(AppConstants.csrftoken)
|
||||
}
|
||||
}}
|
||||
areAssetsDownloadable={this.props.areAssetsDownloadable}
|
||||
areAssetsEditable={this.props.editable}
|
||||
multiple={this.props.multiple} />
|
||||
submitFile={submitFile}
|
||||
showErrorPrompt={showErrorPrompt}
|
||||
validation={validation} />
|
||||
</Property>
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import InputFineUploader from './input_fineuploader';
|
||||
|
||||
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 AscribeSpinner from '../ascribe_spinner';
|
||||
@ -72,7 +73,7 @@ let RegisterPieceForm = React.createClass({
|
||||
|
||||
handleChangedDigitalWork(digitalWorkFile) {
|
||||
if (digitalWorkFile &&
|
||||
(digitalWorkFile.status === 'deleted' || digitalWorkFile.status === 'canceled')) {
|
||||
(digitalWorkFile.status === FileStatus.DELETED || digitalWorkFile.status === FileStatus.CANCELED)) {
|
||||
this.refs.form.refs.thumbnail_file.reset();
|
||||
|
||||
// Manually we need to set the ready state for `thumbnailKeyReady` back
|
||||
@ -91,8 +92,8 @@ let RegisterPieceForm = React.createClass({
|
||||
|
||||
fineuploader.setThumbnailForFileId(
|
||||
digitalWorkFile.id,
|
||||
// if thumbnail was delete, we delete it from the display as well
|
||||
thumbnailFile.status !== 'deleted' ? thumbnailFile.url : null
|
||||
// if thumbnail was deleted, we delete it from the display as well
|
||||
thumbnailFile.status !== FileStatus.DELETED ? thumbnailFile.url : null
|
||||
);
|
||||
},
|
||||
|
||||
@ -175,7 +176,8 @@ let RegisterPieceForm = React.createClass({
|
||||
disabled={!isFineUploaderEditable}
|
||||
enableLocalHashing={hashLocally}
|
||||
uploadMethod={location.query.method}
|
||||
handleChangedFile={this.handleChangedDigitalWork}/>
|
||||
handleChangedFile={this.handleChangedDigitalWork}
|
||||
showErrorPrompt />
|
||||
</Property>
|
||||
<Property
|
||||
name="thumbnail_file"
|
||||
|
@ -10,48 +10,35 @@ import AppConstants from '../../constants/application_constants';
|
||||
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({
|
||||
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
|
||||
// a user is actually not logged in already to prevent him from droping files
|
||||
// before login in
|
||||
isFineUploaderActive: bool,
|
||||
|
||||
enableLocalHashing: bool,
|
||||
uploadMethod: string,
|
||||
|
||||
// provided by Property
|
||||
disabled: bool,
|
||||
onChange: func,
|
||||
|
||||
// A class of a file the user has to upload
|
||||
// Needs to be defined both in singular as well as in plural
|
||||
fileClassToUpload: shape({
|
||||
singular: string,
|
||||
plural: string
|
||||
}),
|
||||
handleChangedFile: func,
|
||||
// Props for ReactS3FineUploader
|
||||
areAssetsDownloadable: bool,
|
||||
createBlobRoutine: ReactS3FineUploader.propTypes.createBlobRoutine,
|
||||
enableLocalHashing: bool,
|
||||
fileClassToUpload: ReactS3FineUploader.propTypes.fileClassToUpload,
|
||||
fileInputElement: ReactS3FineUploader.propTypes.fileInputElement,
|
||||
isReadyForFormSubmission: func,
|
||||
keyRoutine: ReactS3FineUploader.propTypes.keyRoutine,
|
||||
handleChangedFile: func, // TODO: rename to onChangedFile
|
||||
submitFile: func, // TODO: rename to onSubmitFile
|
||||
onValidationFailed: func,
|
||||
|
||||
// Provided by `Property`
|
||||
onChange: React.PropTypes.func
|
||||
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
|
||||
setWarning: func,
|
||||
showErrorPrompt: bool,
|
||||
uploadMethod: oneOf(['hash', 'upload']),
|
||||
validation: ReactS3FineUploader.propTypes.validation,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
@ -96,19 +83,21 @@ const InputFineUploader = React.createClass({
|
||||
render() {
|
||||
const {
|
||||
areAssetsDownloadable,
|
||||
enableLocalHashing,
|
||||
createBlobRoutine,
|
||||
enableLocalHashing,
|
||||
disabled,
|
||||
fileClassToUpload,
|
||||
fileInputElement,
|
||||
handleChangedFile,
|
||||
isFineUploaderActive,
|
||||
isReadyForFormSubmission,
|
||||
keyRoutine,
|
||||
onValidationFailed,
|
||||
setIsUploadReady,
|
||||
setWarning,
|
||||
showErrorPrompt,
|
||||
uploadMethod,
|
||||
validation,
|
||||
handleChangedFile } = this.props;
|
||||
validation } = this.props;
|
||||
let editable = isFineUploaderActive;
|
||||
|
||||
// if disabled is actually set by property, we want to override
|
||||
@ -130,6 +119,8 @@ const InputFineUploader = React.createClass({
|
||||
isReadyForFormSubmission={isReadyForFormSubmission}
|
||||
areAssetsDownloadable={areAssetsDownloadable}
|
||||
areAssetsEditable={editable}
|
||||
setWarning={setWarning}
|
||||
showErrorPrompt={showErrorPrompt}
|
||||
signature={{
|
||||
endpoint: AppConstants.serverUrl + 's3/signature/',
|
||||
customHeaders: {
|
||||
@ -147,7 +138,7 @@ const InputFineUploader = React.createClass({
|
||||
enableLocalHashing={enableLocalHashing}
|
||||
uploadMethod={uploadMethod}
|
||||
fileClassToUpload={fileClassToUpload}
|
||||
handleChangedFile={handleChangedFile}/>
|
||||
handleChangedFile={handleChangedFile} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -72,7 +72,8 @@ const Property = React.createClass({
|
||||
initialValue: null,
|
||||
value: null,
|
||||
isFocused: false,
|
||||
errors: null
|
||||
errors: null,
|
||||
hasWarning: false
|
||||
};
|
||||
},
|
||||
|
||||
@ -218,17 +219,20 @@ const Property = React.createClass({
|
||||
this.setState({errors: null});
|
||||
},
|
||||
|
||||
setWarning(hasWarning) {
|
||||
this.setState({ hasWarning });
|
||||
},
|
||||
|
||||
getClassName() {
|
||||
if(!this.state.expanded && !this.props.checkboxLabel){
|
||||
if (!this.state.expanded && !this.props.checkboxLabel) {
|
||||
return 'is-hidden';
|
||||
}
|
||||
if(!this.props.editable){
|
||||
} else if (!this.props.editable) {
|
||||
return 'is-fixed';
|
||||
}
|
||||
if (this.state.errors){
|
||||
} else if (this.state.errors) {
|
||||
return 'is-error';
|
||||
}
|
||||
if(this.state.isFocused) {
|
||||
} else if (this.state.hasWarning) {
|
||||
return 'is-warning';
|
||||
} else if (this.state.isFocused) {
|
||||
return 'is-focused';
|
||||
} else {
|
||||
return '';
|
||||
@ -271,6 +275,7 @@ const Property = React.createClass({
|
||||
onChange: this.handleChange,
|
||||
onFocus: this.handleFocus,
|
||||
onBlur: this.handleBlur,
|
||||
setWarning: this.setWarning,
|
||||
disabled: !this.props.editable,
|
||||
ref: 'input',
|
||||
name: this.props.name,
|
||||
|
@ -17,7 +17,7 @@ const WHEN_ENUM = ['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`.
|
||||
// Throw an error otherwise.
|
||||
if (WHEN_ENUM.indexOf(when) === -1) {
|
||||
@ -80,8 +80,8 @@ export function ProxyHandler(...redirectFunctions) {
|
||||
displayName: 'ProxyHandler',
|
||||
|
||||
propTypes: {
|
||||
// Provided from AscribeApp
|
||||
currentUser: React.PropTypes.object.isRequired,
|
||||
// Provided from AscribeApp, after the routes have been initialized
|
||||
currentUser: React.PropTypes.object,
|
||||
whitelabel: React.PropTypes.object,
|
||||
|
||||
// Provided from router
|
||||
|
@ -1,29 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
|
||||
|
||||
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 { FileStatus } from '../react_s3_fine_uploader_utils';
|
||||
import { getLangText } from '../../../utils/lang_utils';
|
||||
|
||||
|
||||
// Taken from: https://github.com/fedosejev/react-file-drag-and-drop
|
||||
let FileDragAndDrop = React.createClass({
|
||||
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,
|
||||
onDragOver: React.PropTypes.func,
|
||||
filesToUpload: React.PropTypes.array,
|
||||
handleDeleteFile: React.PropTypes.func,
|
||||
handleCancelFile: React.PropTypes.func,
|
||||
handlePauseFile: React.PropTypes.func,
|
||||
handleResumeFile: React.PropTypes.func,
|
||||
multiple: React.PropTypes.bool,
|
||||
dropzoneInactive: React.PropTypes.bool,
|
||||
areAssetsDownloadable: React.PropTypes.bool,
|
||||
areAssetsEditable: React.PropTypes.bool,
|
||||
handleRetryFiles: React.PropTypes.func,
|
||||
|
||||
enableLocalHashing: React.PropTypes.bool,
|
||||
uploadMethod: React.PropTypes.string,
|
||||
@ -34,6 +38,12 @@ let FileDragAndDrop = React.createClass({
|
||||
// to -1 which is code for: aborted
|
||||
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
|
||||
// Needs to be defined both in singular as well as in plural
|
||||
fileClassToUpload: React.PropTypes.shape({
|
||||
@ -126,31 +136,73 @@ 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 () {
|
||||
const {
|
||||
filesToUpload,
|
||||
dropzoneInactive,
|
||||
className,
|
||||
hashingProgress,
|
||||
handleCancelHashing,
|
||||
multiple,
|
||||
enableLocalHashing,
|
||||
uploadMethod,
|
||||
showError,
|
||||
errorClass,
|
||||
fileClassToUpload,
|
||||
areAssetsDownloadable,
|
||||
areAssetsEditable,
|
||||
allowedExtensions } = this.props;
|
||||
|
||||
// has files only is true if there are files that do not have the status deleted or canceled
|
||||
let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0;
|
||||
let updatedClassName = hasFiles ? 'has-files ' : '';
|
||||
updatedClassName += dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone';
|
||||
updatedClassName += ' file-drag-and-drop';
|
||||
// has files only is true if there are files that do not have the status deleted, canceled, or failed
|
||||
const hasFiles = filesToUpload
|
||||
.filter((file) => {
|
||||
return file.status !== FileStatus.DELETED &&
|
||||
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(hashingProgress !== -2) {
|
||||
if (hashingProgress !== -2) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="file-drag-and-drop-hashing-dialog">
|
||||
<p>{getLangText('Computing hash(es)... This may take a few minutes.')}</p>
|
||||
<p>
|
||||
@ -161,30 +213,16 @@ let FileDragAndDrop = React.createClass({
|
||||
label="%(percent)s%"
|
||||
className="ascribe-progress-bar"/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={updatedClassName}
|
||||
className={classNames('file-drag-and-drop', dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone', { 'has-files': hasFiles })}
|
||||
onDrag={this.handleDrop}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDrop={this.handleDrop}>
|
||||
<FileDragAndDropDialog
|
||||
multipleFiles={multiple}
|
||||
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}/>
|
||||
{hasError ? this.getErrorDialog(failedFiles) : this.getPreviewIterator()}
|
||||
{!hasFiles && !hasError ? this.getUploadDialog() : null}
|
||||
{/*
|
||||
Opera doesn't trigger simulated click events
|
||||
if the targeted input has `display:none` set.
|
||||
|
@ -9,7 +9,6 @@ import { getCurrentQueryParams } from '../../../utils/url_utils';
|
||||
|
||||
let FileDragAndDropDialog = React.createClass({
|
||||
propTypes: {
|
||||
hasFiles: React.PropTypes.bool,
|
||||
multipleFiles: React.PropTypes.bool,
|
||||
enableLocalHashing: React.PropTypes.bool,
|
||||
uploadMethod: React.PropTypes.string,
|
||||
@ -36,16 +35,11 @@ let FileDragAndDropDialog = React.createClass({
|
||||
|
||||
render() {
|
||||
const {
|
||||
hasFiles,
|
||||
multipleFiles,
|
||||
enableLocalHashing,
|
||||
uploadMethod,
|
||||
fileClassToUpload,
|
||||
onClick } = this.props;
|
||||
|
||||
if (hasFiles) {
|
||||
return null;
|
||||
} else {
|
||||
let dialogElement;
|
||||
|
||||
if (enableLocalHashing && !uploadMethod) {
|
||||
@ -74,7 +68,7 @@ let FileDragAndDropDialog = React.createClass({
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<span> or </span>
|
||||
<span> {getLangText('or')} </span>
|
||||
|
||||
<Link
|
||||
to={`/${window.location.pathname.split('/').pop()}`}
|
||||
@ -89,11 +83,12 @@ let FileDragAndDropDialog = React.createClass({
|
||||
if (multipleFiles) {
|
||||
dialogElement = [
|
||||
this.getDragDialog(fileClassToUpload.plural),
|
||||
<span
|
||||
(<span
|
||||
key='mutlipleFilesBtn'
|
||||
className="btn btn-default"
|
||||
onClick={onClick}>
|
||||
{getLangText('choose %s to upload', fileClassToUpload.plural)}
|
||||
</span>
|
||||
</span>)
|
||||
];
|
||||
} else {
|
||||
const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular)
|
||||
@ -101,11 +96,12 @@ let FileDragAndDropDialog = React.createClass({
|
||||
|
||||
dialogElement = [
|
||||
this.getDragDialog(fileClassToUpload.singular),
|
||||
<span
|
||||
(<span
|
||||
key='singleFileBtn'
|
||||
className="btn btn-default"
|
||||
onClick={onClick}>
|
||||
{dialog}
|
||||
</span>
|
||||
</span>)
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -122,7 +118,6 @@ let FileDragAndDropDialog = React.createClass({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default FileDragAndDropDialog;
|
||||
|
@ -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 FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other';
|
||||
|
||||
import { FileStatus } from '../react_s3_fine_uploader_utils';
|
||||
import { getLangText } from '../../../utils/lang_utils';
|
||||
import { truncateTextAtCharIndex } from '../../../utils/general_utils';
|
||||
import { extractFileExtensionFromString } from '../../../utils/file_utils';
|
||||
@ -24,27 +25,29 @@ const FileDragAndDropPreview = React.createClass({
|
||||
s3UrlSafe: string
|
||||
}).isRequired,
|
||||
|
||||
areAssetsDownloadable: bool,
|
||||
areAssetsEditable: bool,
|
||||
handleDeleteFile: func,
|
||||
handleCancelFile: func,
|
||||
handlePauseFile: func,
|
||||
handleResumeFile: func,
|
||||
areAssetsDownloadable: bool,
|
||||
areAssetsEditable: bool,
|
||||
numberOfDisplayedFiles: number
|
||||
},
|
||||
|
||||
toggleUploadProcess() {
|
||||
if (this.props.file.status === 'uploading') {
|
||||
this.props.handlePauseFile(this.props.file.id);
|
||||
} else if (this.props.file.status === 'paused') {
|
||||
this.props.handleResumeFile(this.props.file.id);
|
||||
const { file, handlePauseFile, handleResumeFile } = this.props;
|
||||
|
||||
if (file.status === FileStatus.UPLOADING) {
|
||||
handlePauseFile(file.id);
|
||||
} else if (file.status === FileStatus.PAUSED) {
|
||||
handleResumeFile(file.id);
|
||||
}
|
||||
},
|
||||
|
||||
handleDeleteFile() {
|
||||
const { handleDeleteFile,
|
||||
handleCancelFile,
|
||||
file } = this.props;
|
||||
const { file,
|
||||
handleDeleteFile,
|
||||
handleCancelFile } = this.props;
|
||||
// `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
|
||||
@ -52,7 +55,7 @@ const FileDragAndDropPreview = React.createClass({
|
||||
// deleted using an HTTP DELETE request.
|
||||
if (handleDeleteFile &&
|
||||
file.progress === 100 &&
|
||||
(file.status === 'upload successful' || file.status === 'online') &&
|
||||
(file.status === FileStatus.UPLOAD_SUCCESSFUL || file.status === FileStatus.ONLINE) &&
|
||||
file.s3UrlSafe) {
|
||||
handleDeleteFile(file.id);
|
||||
} else if (handleCancelFile) {
|
||||
|
@ -3,7 +3,7 @@
|
||||
import React from 'react';
|
||||
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 { 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) {
|
||||
return filesToUpload.filter((file) => file.status === 'uploading');
|
||||
return filesToUpload.filter((file) => file.status === FileStatus.UPLOADING);
|
||||
},
|
||||
|
||||
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() {
|
||||
|
@ -8,12 +8,16 @@ import S3Fetcher from '../../fetchers/s3_fetcher';
|
||||
|
||||
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 GlobalNotificationActions from '../../actions/global_notification_actions';
|
||||
|
||||
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 { computeHashOfFile, extractFileExtensionFromString } from '../../utils/file_utils';
|
||||
import { getLangText } from '../../utils/lang_utils';
|
||||
@ -33,80 +37,18 @@ const { shape,
|
||||
|
||||
const ReactS3FineUploader = React.createClass({
|
||||
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,
|
||||
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
|
||||
// works to ascribe due to a slow internet connection.
|
||||
@ -135,13 +77,94 @@ const ReactS3FineUploader = React.createClass({
|
||||
fileInputElement: oneOfType([
|
||||
func,
|
||||
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() {
|
||||
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,
|
||||
debug: false,
|
||||
multiple: false,
|
||||
objectProperties: {
|
||||
acl: 'public-read',
|
||||
bucket: 'ascribe0'
|
||||
@ -178,27 +201,25 @@ const ReactS3FineUploader = React.createClass({
|
||||
messages: {
|
||||
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) {
|
||||
name = name.slice(0, 15) + '...' + name.slice(-15);
|
||||
}
|
||||
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() {
|
||||
return {
|
||||
filesToUpload: [],
|
||||
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()),
|
||||
uploader: this.createNewFineUploader(),
|
||||
csrfToken: getCookie(AppConstants.csrftoken),
|
||||
errorState: {
|
||||
manualRetryAttempt: 0,
|
||||
errorClass: null
|
||||
},
|
||||
uploadInProgress: false,
|
||||
|
||||
// -1: aborted
|
||||
// -2: uninitialized
|
||||
@ -216,7 +237,7 @@ const ReactS3FineUploader = React.createClass({
|
||||
let potentiallyNewCSRFToken = getCookie(AppConstants.csrftoken);
|
||||
if(this.state.csrfToken !== potentiallyNewCSRFToken) {
|
||||
this.setState({
|
||||
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()),
|
||||
uploader: this.createNewFineUploader(),
|
||||
csrfToken: potentiallyNewCSRFToken
|
||||
});
|
||||
}
|
||||
@ -229,8 +250,12 @@ const ReactS3FineUploader = React.createClass({
|
||||
this.state.uploader.cancelAll();
|
||||
},
|
||||
|
||||
createNewFineUploader() {
|
||||
return new fineUploader.s3.FineUploaderBasic(this.propsToConfig());
|
||||
},
|
||||
|
||||
propsToConfig() {
|
||||
let objectProperties = this.props.objectProperties;
|
||||
const objectProperties = Object.assign({}, this.props.objectProperties);
|
||||
objectProperties.key = this.requestKey;
|
||||
|
||||
return {
|
||||
@ -251,6 +276,7 @@ const ReactS3FineUploader = React.createClass({
|
||||
multiple: this.props.multiple,
|
||||
retry: this.props.retry,
|
||||
callbacks: {
|
||||
onAllComplete: this.onAllComplete,
|
||||
onComplete: this.onComplete,
|
||||
onCancel: this.onCancel,
|
||||
onProgress: this.onProgress,
|
||||
@ -274,6 +300,9 @@ const ReactS3FineUploader = React.createClass({
|
||||
// proclaim that upload is not ready
|
||||
this.props.setIsUploadReady(false);
|
||||
|
||||
// reset any warnings propagated to parent
|
||||
this.setWarning(false);
|
||||
|
||||
// reset internal data structures of component
|
||||
this.setState(this.getInitialState());
|
||||
},
|
||||
@ -319,7 +348,7 @@ const ReactS3FineUploader = React.createClass({
|
||||
// if createBlobRoutine is not defined,
|
||||
// we're progressing right away without posting to S3
|
||||
// so that this can be done manually by the form
|
||||
if(!createBlobRoutine) {
|
||||
if (!createBlobRoutine) {
|
||||
// still we warn the user of this component
|
||||
console.warn('createBlobRoutine was not defined for ReactS3FineUploader. Continuing without creating the blob on the server.');
|
||||
resolve();
|
||||
@ -377,6 +406,19 @@ const ReactS3FineUploader = React.createClass({
|
||||
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() {
|
||||
const { fileInput } = this.refs;
|
||||
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) {
|
||||
if (xhr) {
|
||||
return {
|
||||
@ -406,13 +472,20 @@ const ReactS3FineUploader = React.createClass({
|
||||
},
|
||||
|
||||
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) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
const filesToDisplay = filesToUpload.filter((file) => {
|
||||
return file.status !== FileStatus.DELETED &&
|
||||
file.status !== FileStatus.CANCELED &&
|
||||
file.status !== FileStatus.UPLOAD_FAILED &&
|
||||
file.size !== -1;
|
||||
});
|
||||
|
||||
return (enableLocalHashing && !uploadMethod) ||
|
||||
!areAssetsEditable ||
|
||||
(showErrorPrompt && errorState.errorClass) ||
|
||||
(!multiple && filesToDisplay.length > 0);
|
||||
},
|
||||
|
||||
isFileValid(file) {
|
||||
@ -457,7 +530,7 @@ const ReactS3FineUploader = React.createClass({
|
||||
return Q.Promise((resolve) => {
|
||||
let changeSet = {};
|
||||
|
||||
if(status === 'deleted' || status === 'canceled') {
|
||||
if (status === FileStatus.DELETED || status === FileStatus.CANCELED || status === FileStatus.UPLOAD_FAILED) {
|
||||
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) {
|
||||
let chunks = this.state.chunks;
|
||||
|
||||
@ -514,7 +592,14 @@ const ReactS3FineUploader = React.createClass({
|
||||
|
||||
this.setState({ startedChunks });
|
||||
}
|
||||
},
|
||||
|
||||
onAllComplete(succeed, failed) {
|
||||
if (this.state.uploadInProgress) {
|
||||
this.setState({
|
||||
uploadInProgress: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onComplete(id, name, res, xhr) {
|
||||
@ -526,12 +611,12 @@ const ReactS3FineUploader = React.createClass({
|
||||
xhr: this.getXhrErrorComment(xhr)
|
||||
});
|
||||
// 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;
|
||||
|
||||
// Set the state of the completed file to 'upload successful' in order to
|
||||
// remove it from the GUI
|
||||
files[id].status = 'upload successful';
|
||||
files[id].status = FileStatus.UPLOAD_SUCCESSFUL;
|
||||
files[id].key = this.state.uploader.getKey(id);
|
||||
|
||||
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.
|
||||
this.createBlob(files[id])
|
||||
.then(() => {
|
||||
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
|
||||
// are optional, we'll only trigger them when they're actually defined
|
||||
if(this.props.submitFile) {
|
||||
if (typeof this.props.submitFile === 'function') {
|
||||
this.props.submitFile(files[id]);
|
||||
} else {
|
||||
console.warn('You didn\'t define submitFile as a prop in react-s3-fine-uploader');
|
||||
}
|
||||
|
||||
// for explanation, check comment of if statement above
|
||||
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);
|
||||
this.checkFormSubmissionReady();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -578,22 +648,51 @@ const ReactS3FineUploader = React.createClass({
|
||||
},
|
||||
|
||||
onError(id, name, errorReason, xhr) {
|
||||
const { errorNotificationMessage, showErrorPrompt } = this.props;
|
||||
const { chunks, filesToUpload } = this.state;
|
||||
|
||||
console.logGlobal(errorReason, {
|
||||
files: this.state.filesToUpload,
|
||||
chunks: this.state.chunks,
|
||||
files: filesToUpload,
|
||||
chunks: chunks,
|
||||
xhr: this.getXhrErrorComment(xhr)
|
||||
});
|
||||
|
||||
this.props.setIsUploadReady(true);
|
||||
this.cancelUploads();
|
||||
let notificationMessage;
|
||||
|
||||
let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000);
|
||||
if (showErrorPrompt) {
|
||||
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) {
|
||||
// when a upload is canceled, we need to update this components file array
|
||||
this.setStatusOfFile(id, 'canceled')
|
||||
this.setStatusOfFile(id, FileStatus.CANCELED)
|
||||
.then(() => {
|
||||
if(typeof this.props.handleChangedFile === 'function') {
|
||||
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);
|
||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||
|
||||
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
|
||||
// are optional, we'll only trigger them when they're actually defined
|
||||
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
|
||||
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');
|
||||
this.checkFormSubmissionReady();
|
||||
|
||||
// FineUploader's onAllComplete event doesn't fire if all files are cancelled
|
||||
// so we need to double check if this is the last file getting cancelled.
|
||||
//
|
||||
// Because we're calling FineUploader.getInProgress() in a cancel callback,
|
||||
// the current file getting cancelled is still considered to be in progress
|
||||
// so there will be one file left in progress when we're cancelling the last file.
|
||||
if (this.state.uploader.getInProgress() === 1) {
|
||||
this.setState({
|
||||
uploadInProgress: false
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -633,7 +733,7 @@ const ReactS3FineUploader = React.createClass({
|
||||
// fetch blobs for images
|
||||
response = response.map((file) => {
|
||||
file.url = file.s3UrlSafe;
|
||||
file.status = 'online';
|
||||
file.status = FileStatus.ONLINE;
|
||||
file.progress = 100;
|
||||
return file;
|
||||
});
|
||||
@ -661,7 +761,7 @@ const ReactS3FineUploader = React.createClass({
|
||||
|
||||
onDeleteComplete(id, xhr, 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);
|
||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||
@ -670,29 +770,16 @@ const ReactS3FineUploader = React.createClass({
|
||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||
}
|
||||
|
||||
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
|
||||
// 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');
|
||||
}
|
||||
this.checkFormSubmissionReady();
|
||||
},
|
||||
|
||||
handleDeleteFile(fileId) {
|
||||
// We set the files state to 'deleted' immediately, so that the user is not confused with
|
||||
// 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
|
||||
this.setStatusOfFile(fileId, 'deleted')
|
||||
this.setStatusOfFile(fileId, FileStatus.DELETED)
|
||||
.then(() => {
|
||||
if(typeof this.props.handleChangedFile === 'function') {
|
||||
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.
|
||||
// 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
|
||||
this.state.uploader.deleteFile(fileId);
|
||||
// this is being continued in onDeleteFile, as
|
||||
@ -736,7 +823,7 @@ const ReactS3FineUploader = React.createClass({
|
||||
|
||||
handlePauseFile(fileId) {
|
||||
if(this.state.uploader.pauseUpload(fileId)) {
|
||||
this.setStatusOfFile(fileId, 'paused');
|
||||
this.setStatusOfFile(fileId, FileStatus.PAUSED);
|
||||
} else {
|
||||
throw new Error(getLangText('File upload could not be paused.'));
|
||||
}
|
||||
@ -744,12 +831,35 @@ const ReactS3FineUploader = React.createClass({
|
||||
|
||||
handleResumeFile(fileId) {
|
||||
if(this.state.uploader.continueUpload(fileId)) {
|
||||
this.setStatusOfFile(fileId, 'uploading');
|
||||
this.setStatusOfFile(fileId, FileStatus.UPLOADING);
|
||||
} else {
|
||||
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) {
|
||||
// While files are being uploaded, the form cannot be ready
|
||||
// for submission
|
||||
@ -870,6 +980,9 @@ const ReactS3FineUploader = React.createClass({
|
||||
if(files.length > 0) {
|
||||
this.state.uploader.addFiles(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
|
||||
// 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
|
||||
// status === 'upload successful', therefore once the file is synced,
|
||||
// we need to tag its status as 'deleted' (which basically happens here)
|
||||
// status === FileStatus.UPLOAD_SUCCESSFUL, therefore once the file is synced,
|
||||
// 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)) {
|
||||
oldAndNewFiles[i].status = 'deleted';
|
||||
oldAndNewFiles[i].status = FileStatus.DELETED;
|
||||
}
|
||||
|
||||
if(oldAndNewFiles[i].originalName === oldFiles[j].name) {
|
||||
@ -944,15 +1057,20 @@ const ReactS3FineUploader = React.createClass({
|
||||
},
|
||||
|
||||
render() {
|
||||
const { errorState: { errorClass }, filesToUpload, uploadInProgress } = this.state;
|
||||
const {
|
||||
multiple,
|
||||
areAssetsDownloadable,
|
||||
areAssetsEditable,
|
||||
enableLocalHashing,
|
||||
fileClassToUpload,
|
||||
fileInputElement: FileInputElement,
|
||||
multiple,
|
||||
showErrorPrompt,
|
||||
uploadMethod } = this.props;
|
||||
|
||||
// Only show the error state once all files are finished
|
||||
const showError = !uploadInProgress && showErrorPrompt && errorClass != null;
|
||||
|
||||
const props = {
|
||||
multiple,
|
||||
areAssetsDownloadable,
|
||||
@ -960,12 +1078,16 @@ const ReactS3FineUploader = React.createClass({
|
||||
enableLocalHashing,
|
||||
uploadMethod,
|
||||
fileClassToUpload,
|
||||
filesToUpload,
|
||||
uploadInProgress,
|
||||
errorClass,
|
||||
showError,
|
||||
onDrop: this.handleUploadFile,
|
||||
filesToUpload: this.state.filesToUpload,
|
||||
handleDeleteFile: this.handleDeleteFile,
|
||||
handleCancelFile: this.handleCancelFile,
|
||||
handlePauseFile: this.handlePauseFile,
|
||||
handleResumeFile: this.handleResumeFile,
|
||||
handleRetryFiles: this.handleRetryFiles,
|
||||
handleCancelHashing: this.handleCancelHashing,
|
||||
dropzoneInactive: this.isDropzoneInactive(),
|
||||
hashingProgress: this.state.hashingProgress,
|
||||
|
@ -1,7 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
import fineUploader from 'fineUploader';
|
||||
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 = {
|
||||
/**
|
||||
* Returns a boolean if there has been at least one file uploaded
|
||||
@ -10,8 +18,13 @@ export const formSubmissionValidation = {
|
||||
* @return {boolean}
|
||||
*/
|
||||
atLeastOneUploadedFile(files) {
|
||||
files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled');
|
||||
if (files.length > 0 && files[0].status === 'upload successful') {
|
||||
files = files.filter((file) => {
|
||||
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;
|
||||
} else {
|
||||
return false;
|
||||
@ -25,32 +38,32 @@ export const formSubmissionValidation = {
|
||||
* @return {boolean} [description]
|
||||
*/
|
||||
fileOptional(files) {
|
||||
let uploadingFiles = files.filter((file) => file.status === 'submitting');
|
||||
const uploadingFiles = files.filter((file) => file.status === FileStatus.SUBMITTING);
|
||||
|
||||
if (uploadingFiles.length === 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return uploadFiles.length === 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return {boolean}
|
||||
*/
|
||||
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.
|
||||
* @return {boolean}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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) {
|
||||
// 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.
|
||||
let prefixedAllowedExtensions = allowedExtensions.map((ext) => {
|
||||
const prefixedAllowedExtensions = allowedExtensions.map((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
|
||||
}
|
||||
};
|
||||
|
||||
// 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';
|
||||
|
||||
|
||||
/**
|
||||
* Logs an error in to the console but also sends it to
|
||||
* Sentry.
|
||||
|
@ -223,15 +223,12 @@ export function omitFromObject(obj, filter) {
|
||||
* By default, applies strict equality using ===
|
||||
* @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') {
|
||||
throw new Error('Your specified match argument was not an object');
|
||||
}
|
||||
|
||||
if (typeof testFn !== 'function') {
|
||||
testFn = (objProp, matchProp) => {
|
||||
return objProp === matchProp;
|
||||
};
|
||||
throw new Error('Your specified test function was not a function');
|
||||
}
|
||||
|
||||
return Object
|
||||
@ -239,7 +236,7 @@ export function deepMatchObject(obj, match, testFn) {
|
||||
.reduce((result, matchKey) => {
|
||||
if (!result) { return false; }
|
||||
|
||||
const objProp = obj[matchKey];
|
||||
const objProp = obj && obj[matchKey];
|
||||
const matchProp = match[matchKey];
|
||||
|
||||
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 {
|
||||
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 {
|
||||
margin-bottom: 0;
|
||||
> .progress-bar {
|
||||
|
Loading…
Reference in New Issue
Block a user