mirror of
https://github.com/ascribe/onion.git
synced 2024-12-22 17:33:14 +01:00
Add rewritten uploader and UI elements
This commit is contained in:
parent
1069121e07
commit
d5469b3efa
126
js/components/ascribe_uploader/ui/upload_button.js
Normal file
126
js/components/ascribe_uploader/ui/upload_button.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import Uploadify from 'react-utility-belt/es6/uploader/uploadify';
|
||||||
|
import { UploadButtonBase } from 'react-utility-belt/es6/uploader/upload_button';
|
||||||
|
import { uploadedFilesFilter, uploadingFilesFilter } from 'react-utility-belt/es6/uploader/utils/file_filters';
|
||||||
|
import uploaderSpecExtender from 'react-utility-belt/es6/uploader/utils/uploader_spec_extender';
|
||||||
|
|
||||||
|
import Uploader from '../uploader';
|
||||||
|
|
||||||
|
import { safeInvoke } from '../../../utils/general';
|
||||||
|
import { getLangText } from '../../../utils/lang';
|
||||||
|
import { truncateText } from '../../../utils/text';
|
||||||
|
|
||||||
|
|
||||||
|
const { arrayOf, func, object, shape, string } = React.PropTypes;
|
||||||
|
|
||||||
|
const FileLabel = ({ files, handleRemoveFiles }) => {
|
||||||
|
let label;
|
||||||
|
|
||||||
|
if (files.length) {
|
||||||
|
const uploadedFiles = files.filter(uploadedFilesFilter);
|
||||||
|
const uploadingFiles = files.filter(uploadingFilesFilter);
|
||||||
|
|
||||||
|
const uploadedIcon = uploadedFiles.length && !uploadingFiles.length
|
||||||
|
? (<span className="ascribe-icon icon-ascribe-ok uploader-button--label-icon" />)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const labelText = files.length > 1 ? `${files.length} ${getLangText('files')}`
|
||||||
|
: truncateText(files[0].name, 40);
|
||||||
|
|
||||||
|
const removeActionText = getLangText(
|
||||||
|
uploadingFiles.length ? `cancel ${files.length > 1 ? 'uploads' : 'upload'}`
|
||||||
|
: 'remove'
|
||||||
|
);
|
||||||
|
|
||||||
|
label = [
|
||||||
|
uploadedIcon,
|
||||||
|
labelText,
|
||||||
|
' [',
|
||||||
|
(<a key="remove-link" onClick={handleRemoveFiles} tabIndex={0}>{removeActionText}</a>),
|
||||||
|
']'
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
label = getLangText('No file selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<span className="upload-button--label">{label}</span>);
|
||||||
|
};
|
||||||
|
|
||||||
|
FileLabel.propTypes = {
|
||||||
|
files: arrayOf(shape({
|
||||||
|
name: string.isRequired
|
||||||
|
})).isRequired,
|
||||||
|
|
||||||
|
handleRemoveFiles: func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const UploadButton = ({ className, ...props }) => (
|
||||||
|
<UploadButtonBase {...props} className={classNames(className, 'upload-button')} />
|
||||||
|
);
|
||||||
|
|
||||||
|
UploadButton.propTypes = {
|
||||||
|
className: string
|
||||||
|
};
|
||||||
|
|
||||||
|
UploadButton.defaultProps = {
|
||||||
|
buttonType: 'button',
|
||||||
|
getUploadingButtonLabel: (uploaderFiles, progress) => (
|
||||||
|
`${getLangText('Upload progress')}: ${progress}%`
|
||||||
|
),
|
||||||
|
fileLabelType: FileLabel
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We want to add some additional default uploader functionality on top of the UI defaults we've
|
||||||
|
* added to the base upload button, so we wrap another class around the Uploadified instance of our
|
||||||
|
* UploadButton and export that as our default export.
|
||||||
|
*/
|
||||||
|
const UploadifiedUploadButton = Uploadify(UploadButton);
|
||||||
|
const UploadButtonUploadifyWrapper = React.createClass(uploaderSpecExtender({
|
||||||
|
displayName: 'UploadbuttonUploadifyWrapper',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
uploaderProps: object,
|
||||||
|
uploaderType: func
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps() {
|
||||||
|
return {
|
||||||
|
uploaderType: Uploader
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onError(file, ...args) {
|
||||||
|
// Automatically cancel any files that fail.
|
||||||
|
this.refs.uploader.handleCancelFile(file);
|
||||||
|
|
||||||
|
safeInvoke(this.props.uploaderProps.onError, file, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { uploaderProps, ...restProps } = this.props;
|
||||||
|
const props = {
|
||||||
|
...restProps,
|
||||||
|
|
||||||
|
uploaderProps: {
|
||||||
|
...uploaderProps,
|
||||||
|
onError: this.onError
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UploadifiedUploadButton ref="uploader" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default UploadButtonUploadifyWrapper;
|
||||||
|
|
||||||
|
// Also export the non-uploadify version for extension
|
||||||
|
export {
|
||||||
|
UploadButton as UploadButtonBase
|
||||||
|
};
|
@ -0,0 +1,5 @@
|
|||||||
|
// Make it easier for users to import this component by default exporting the container in an
|
||||||
|
// index.js
|
||||||
|
export { default } from './upload_file_dialog';
|
||||||
|
|
||||||
|
// Components originally adapted from https://github.com/fedosejev/react-file-drag-and-drop
|
@ -0,0 +1,281 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import update from 'react-addons-update';
|
||||||
|
|
||||||
|
import UploadDragAndDropArea from 'react-utility-belt/es6/uploader/upload_drag_and_drop_area';
|
||||||
|
import uploaderSpecExtender from 'react-utility-belt/es6/uploader/utils/uploader_spec_extender';
|
||||||
|
|
||||||
|
import ErrorQueueStore from '../../../../stores/error_queue_store';
|
||||||
|
|
||||||
|
import UploadFileDialogUI from './upload_file_dialog_ui';
|
||||||
|
|
||||||
|
import Uploader from '../../uploader';
|
||||||
|
|
||||||
|
import { ErrorClasses, testErrorAgainstAll } from '../../../../constants/error_constants';
|
||||||
|
import { UploadMethods, RETRY_ATTEMPT_TO_SHOW_CONTACT_US } from '../../../../constants/uploader_constants';
|
||||||
|
|
||||||
|
import { safeInvoke } from '../../../../utils/general';
|
||||||
|
import { getCurrentQueryParams } from '../../../../utils/url';
|
||||||
|
|
||||||
|
|
||||||
|
const { bool, func, object, oneOf, shape, string } = React.PropTypes;
|
||||||
|
|
||||||
|
const UploadFileDialog = React.createClass(uploaderSpecExtender({
|
||||||
|
propTypes: {
|
||||||
|
/**
|
||||||
|
* Upload method to use. Defaults to normal uploading of files as-received.
|
||||||
|
*
|
||||||
|
* If UploadMethods.USE_URL_PARAM is set, check the current url's upload_method parameter to
|
||||||
|
* determine the upload method. For example, `https://www.ascribe.io/app/register_work?upload_method=hash`
|
||||||
|
* will use file hashing before uploading.
|
||||||
|
*/
|
||||||
|
uploadMethod: oneOf([
|
||||||
|
UploadMethods.HASH,
|
||||||
|
UploadMethods.NORMAL,
|
||||||
|
UploadMethods.USE_URL_PARAM
|
||||||
|
]).isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the case that this dialog is used to load uploaded files from a previous session, it
|
||||||
|
* can sometimes be useful to disallow the ability to download or edit (ie. delete) those
|
||||||
|
* files.
|
||||||
|
*/
|
||||||
|
areAssetsDownloadable: bool,
|
||||||
|
areAssetsEditable: bool,
|
||||||
|
|
||||||
|
className: string,
|
||||||
|
disabled: bool,
|
||||||
|
fileTypeNames: shape({
|
||||||
|
plural: string.isRequired,
|
||||||
|
singular: string.isRequired
|
||||||
|
}),
|
||||||
|
|
||||||
|
// For Uploadify (and UploadDragAndDropArea)
|
||||||
|
// eslint-disable-next-line react/sort-prop-types
|
||||||
|
uploaderProps: object.isRequired,
|
||||||
|
|
||||||
|
uploaderType: func
|
||||||
|
|
||||||
|
// Note that any drag event callbacks specified through the props will be properly attached
|
||||||
|
// to their events by UploadDragAndDropArea.
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps() {
|
||||||
|
return {
|
||||||
|
areAssetsDownloadable: true,
|
||||||
|
areAssetsEditable: true,
|
||||||
|
fileTypeNames: {
|
||||||
|
plural: 'files',
|
||||||
|
singular: 'file'
|
||||||
|
},
|
||||||
|
uploadMethod: UploadMethods.NORMAL,
|
||||||
|
uploaderType: Uploader
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState() {
|
||||||
|
return {
|
||||||
|
errorClass: null,
|
||||||
|
hashingFiles: [],
|
||||||
|
manualRetryAttempt: 0,
|
||||||
|
thumbnailMapping: {},
|
||||||
|
uploadInProgress: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getUploadErrorClass({ type = 'upload', reason, xhr }) {
|
||||||
|
const { manualRetryAttempt } = this.state;
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
|
setThumbnailForFile(file, thumbnailUrl) {
|
||||||
|
if ('id' in file) {
|
||||||
|
this.setState({
|
||||||
|
thumbnailMapping: update(this.state.thumbnailMapping, {
|
||||||
|
[file.id]: { $set: thumbnailUrl }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.logGlobal(
|
||||||
|
new Error('Attempt to set the thumbnail of a file without an id'),
|
||||||
|
{ file, thumbnailUrl }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Override Uploader's showErrorNotification to always show its generic message since we are
|
||||||
|
// already displaying the error in this dialog
|
||||||
|
showErrorNotification() {
|
||||||
|
Uploader.showErrorNotification();
|
||||||
|
},
|
||||||
|
|
||||||
|
onAllComplete(...args) {
|
||||||
|
this.setState({ uploadInProgress: false });
|
||||||
|
|
||||||
|
safeInvoke(this.props.uploaderProps.onAllComplete, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
onError(file, errorReason, xhr, ...args) {
|
||||||
|
// If we've already found an error, just ignore other errors that pop up. They'll likely
|
||||||
|
// pop up again when the user retries.
|
||||||
|
if (!this.state.errorClass) {
|
||||||
|
this.setState({
|
||||||
|
errorClass: this.getUploadErrorClass({
|
||||||
|
reason: errorReason,
|
||||||
|
xhr
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
safeInvoke(this.props.uploaderProps.onError, file, errorReason, xhr, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
onFileHashError(error, ...args) {
|
||||||
|
// Clear our tracked hashing files since they've failed
|
||||||
|
this.setState({
|
||||||
|
hashingFiles: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const { onFileHashError } = this.props.uploaderProps;
|
||||||
|
const { invoked, result } = safeInvoke(onFileHashError, error, ...args);
|
||||||
|
|
||||||
|
if (invoked) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
// Just rethrow the error if no other onFileHashError was specified
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onFileHashProgress(file, hashId, progress, ...args) {
|
||||||
|
const { hashingFiles } = this.state;
|
||||||
|
|
||||||
|
// Note that if we've previously cleared our tracked hashing files before (because they've
|
||||||
|
// previously succeeded or failed), the hashIds won't start from 0 to fill up the array from
|
||||||
|
// the start. This is OK since Javascript's arrays are sparse, and the usual array
|
||||||
|
// operations (besides basic `for` loop) will skip any holes.
|
||||||
|
let updatedHashingFiles;
|
||||||
|
if (hashingFiles[hashId]) {
|
||||||
|
updatedHashingFiles = update(hashingFiles, {
|
||||||
|
[hashId]: {
|
||||||
|
progress: { $set: progress }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updatedHashingFiles = update(hashingFiles, {
|
||||||
|
[hashId]: {
|
||||||
|
$set: { file, progress }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
hashingFiles: updatedHashingFiles
|
||||||
|
});
|
||||||
|
|
||||||
|
const { onFileHashProgress } = this.props.uploaderProps;
|
||||||
|
const { invoked, result } = safeInvoke(onFileHashProgress, file, hashId, progress, ...args);
|
||||||
|
|
||||||
|
return invoked ? result : undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
onFileHashSuccess(files, ...args) {
|
||||||
|
// Clear our tracked hashing files since they've succeeded
|
||||||
|
this.setState({
|
||||||
|
hashingFiles: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const { onFileHashSuccess } = this.props.uploaderProps;
|
||||||
|
const { invoked, result } = safeInvoke(onFileHashSuccess, files, ...args);
|
||||||
|
|
||||||
|
return invoked ? result : files;
|
||||||
|
},
|
||||||
|
|
||||||
|
onManualRetry(...args) {
|
||||||
|
this.setState({
|
||||||
|
manualRetryAttempt: this.state.manualRetryAttempt + 1
|
||||||
|
});
|
||||||
|
|
||||||
|
safeInvoke(this.props.uploaderProps.onManualRetry, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpload(...args) {
|
||||||
|
if (!this.state.uploadInProgress) {
|
||||||
|
this.setState({ uploadInProgress: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
safeInvoke(this.props.uploaderProps.onUpload, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
areAssetsEditable,
|
||||||
|
className,
|
||||||
|
disabled: isDisabled,
|
||||||
|
uploaderProps,
|
||||||
|
uploadMethod: method,
|
||||||
|
...restProps
|
||||||
|
} = this.props;
|
||||||
|
const { uploadInProgress, errorClass } = this.state;
|
||||||
|
|
||||||
|
const uploadMethod = method === UploadMethods.USE_URL_PARAM
|
||||||
|
// If `upload_method` isn't in the current query parameters, tell the UI that we want
|
||||||
|
// it to control which method gets used by setting the url parameter
|
||||||
|
? getCurrentQueryParams().uploadMethod || UploadMethods.USE_URL_PARAM
|
||||||
|
: method;
|
||||||
|
|
||||||
|
const disabled = isDisabled || uploadInProgress || errorClass || !uploadMethod;
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
...restProps,
|
||||||
|
|
||||||
|
className,
|
||||||
|
uploadMethod,
|
||||||
|
areAssetsEditable: !isDisabled && areAssetsEditable,
|
||||||
|
|
||||||
|
// Only show the error state once all files are finished
|
||||||
|
showError: !uploadInProgress && errorClass,
|
||||||
|
|
||||||
|
uploaderProps: {
|
||||||
|
...uploaderProps,
|
||||||
|
hashLocally: uploadMethod === UploadMethods.HASH,
|
||||||
|
onAllComplete: this.onAllComplete,
|
||||||
|
onError: this.onError,
|
||||||
|
onFileHashError: this.onFileHashError,
|
||||||
|
onFileHashProgress: this.onFileHashProgress,
|
||||||
|
onFileHashSuccess: this.onFileHashSuccess,
|
||||||
|
onManualRetry: this.onManualRetry,
|
||||||
|
onUpload: this.onUpload,
|
||||||
|
showErrorNotification: this.showErrorNotification
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// All props meant for UploadFileDialogUI will be passed through by
|
||||||
|
// UploadDragAndDropArea.
|
||||||
|
return (
|
||||||
|
<UploadDragAndDropArea ref="uploader" {...props} disabled={disabled}>
|
||||||
|
<UploadFileDialogUI {...this.state} />
|
||||||
|
</UploadDragAndDropArea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default UploadFileDialog;
|
@ -0,0 +1,126 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { ErrorClasses } from '../../../../constants/error_constants';
|
||||||
|
|
||||||
|
import { getLangText } from '../../../../utils/lang';
|
||||||
|
|
||||||
|
|
||||||
|
const { arrayOf, bool, func, node, number, object, shape, string } = React.PropTypes;
|
||||||
|
|
||||||
|
/** HELPER COMPONENTS **/
|
||||||
|
// Retry button
|
||||||
|
const UploadErrorRetryButton = ({ children, handleRetryFiles, openIntercom }) => (
|
||||||
|
<button
|
||||||
|
className='btn btn-default'
|
||||||
|
onClick={() => {
|
||||||
|
if (openIntercom) {
|
||||||
|
window.Intercom('showNewMessage', getLangText("I'm having trouble uploading my file."));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRetryFiles();
|
||||||
|
}}
|
||||||
|
type="button">
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
UploadErrorRetryButton.propTypes = {
|
||||||
|
handleRetryFiles: func.isRequired,
|
||||||
|
|
||||||
|
children: node,
|
||||||
|
openIntercom: bool
|
||||||
|
};
|
||||||
|
|
||||||
|
// Contact us dialog
|
||||||
|
const UploadErrorContactUs = ({ handleRetryFiles }) => (
|
||||||
|
<div className='file-drag-and-drop-error'>
|
||||||
|
<h4>{getLangText('Let us help you')}</h4>
|
||||||
|
<p>{getLangText('Still having problems? Send us a message.')}</p>
|
||||||
|
<UploadErrorRetryButton openIntercom handleRetryFiles={handleRetryFiles}>
|
||||||
|
{getLangText('Contact us')}
|
||||||
|
</UploadErrorRetryButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
UploadErrorContactUs.propTypes = {
|
||||||
|
handleRetryFiles: func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
// Error details dialog
|
||||||
|
const UploadErrorDetails = ({ errorClass: { prettifiedText }, failedFiles, handleRetryFiles }) => (
|
||||||
|
<div className='file-drag-and-drop-error'>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'file-drag-and-drop-error-detail', {
|
||||||
|
'file-drag-and-drop-error-detail-multiple-files': failedFiles.length
|
||||||
|
}
|
||||||
|
)}>
|
||||||
|
<h4>{getLangText(failedFiles.length ? 'Some files did not upload correctly'
|
||||||
|
: 'Error uploading the file!')}
|
||||||
|
</h4>
|
||||||
|
<p>{prettifiedText}</p>
|
||||||
|
<UploadErrorRetryButton handleRetryFiles={handleRetryFiles}>
|
||||||
|
{getLangText('Retry')}
|
||||||
|
</UploadErrorRetryButton>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'file-drag-and-drop-error-icon-container', {
|
||||||
|
'file-drag-and-drop-error-icon-container-multiple-files': failedFiles.length
|
||||||
|
}
|
||||||
|
)}>
|
||||||
|
<span className='ascribe-icon icon-ascribe-thin-cross'></span>
|
||||||
|
</span>
|
||||||
|
<div className='file-drag-and-drop-error-file-names'>
|
||||||
|
<ul>
|
||||||
|
{failedFiles.map(({ id, originalName }) => (
|
||||||
|
<li key={id} className='file-name'>{originalName}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
UploadErrorDetails.propTypes = {
|
||||||
|
errorClass: shape({
|
||||||
|
prettifiedText: string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
failedFiles: arrayOf(shape({
|
||||||
|
id: number.isRequired,
|
||||||
|
originalName: string.isRequired
|
||||||
|
})).isRequired,
|
||||||
|
handleRetryFiles: func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/** CONTAINER COMPONENT **/
|
||||||
|
// Displays an error detail dialog or a contact us dialog depending on the type of upload error
|
||||||
|
// encountered.
|
||||||
|
const UploadFileDialogErrorHandler = ({ errorClass, failedFiles, handleRetryFile, ...props }) => {
|
||||||
|
// Just go through and retry all the files if the user wants to retry
|
||||||
|
const handleRetryFiles = () => {
|
||||||
|
failedFiles.forEach(handleRetryFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogProps = {
|
||||||
|
...props,
|
||||||
|
errorClass,
|
||||||
|
failedFiles,
|
||||||
|
handleRetryFiles
|
||||||
|
};
|
||||||
|
|
||||||
|
return errorClass.name === ErrorClasses.upload.contactUs.name
|
||||||
|
? (<UploadErrorContactUs {...dialogProps} />)
|
||||||
|
: (<UploadErrorDetails {...dialogProps} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
UploadFileDialogErrorHandler.propTypes = {
|
||||||
|
errorClass: shape({
|
||||||
|
name: string
|
||||||
|
}).isRequired,
|
||||||
|
failedFiles: arrayOf(object).isRequired,
|
||||||
|
handleRetryFile: func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UploadFileDialogErrorHandler;
|
@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { dragAndDropAvailable } from 'js-utility-belt/es6/feature_detection';
|
||||||
|
|
||||||
|
import UploadFileDialogPrintWrapper from './upload_file_dialog_print_wrapper';
|
||||||
|
|
||||||
|
import { UploadMethods } from '../../../../constants/uploader_constants';
|
||||||
|
|
||||||
|
import { getLangText } from '../../../../utils/lang';
|
||||||
|
|
||||||
|
|
||||||
|
const { bool, func, oneOf, shape, string } = React.PropTypes;
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
fileTypeNames: shape({
|
||||||
|
plural: string.isRequired,
|
||||||
|
singular: string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
uploadMethod: oneOf([
|
||||||
|
UploadMethods.HASH,
|
||||||
|
UploadMethods.NORMAL,
|
||||||
|
UploadMethods.USE_URL_PARAM
|
||||||
|
]).isRequired,
|
||||||
|
|
||||||
|
handleSelectFiles: func,
|
||||||
|
multiple: bool
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDragDialog = (fileTypeName) => {
|
||||||
|
if (dragAndDropAvailable) {
|
||||||
|
return [
|
||||||
|
<p key="upload-drag-title" className="file-drag-and-drop-dialog-title">
|
||||||
|
{getLangText('Drag %s here', fileTypeName)}
|
||||||
|
</p>,
|
||||||
|
<p key="upload-drag-subtitle">{getLangText('or')}</p>
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadFileDialogFileSelector = ({ fileTypeNames, handleSelectFiles, multiple, uploadMethod }) => {
|
||||||
|
const uploadMethodName = uploadMethod === UploadMethods.HASH ? 'hash' : 'upload';
|
||||||
|
|
||||||
|
if (uploadMethod !== UploadMethods.HASH || uploadMethod !== UploadMethods.NORMAL) {
|
||||||
|
console.logGlobal(
|
||||||
|
new Error('Unsupported upload method given to UploadFileSelector'),
|
||||||
|
{ uploadMethod }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonMsg = multiple
|
||||||
|
? getLangText(`choose %s to ${uploadMethodName}`, fileTypeNames.plural)
|
||||||
|
: getLangText(`choose a %s to ${uploadMethodName}`, fileTypeNames.singular);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-drag-and-drop-dialog">
|
||||||
|
{getDragDialog(multiple ? fileTypeNames.plural : fileTypeNames.singular)}
|
||||||
|
<button
|
||||||
|
className="btn btn-default"
|
||||||
|
onClick={handleSelectFiles}>
|
||||||
|
{buttonMsg}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UploadFileDialogFileSelector.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UploadFileDialogPrintWrapper(UploadFileDialogFileSelector);
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
|
||||||
|
|
||||||
|
import UploadFileDialogPrintWrapper from './upload_file_dialog_print_wrapper';
|
||||||
|
|
||||||
|
import { getLangText } from '../../../../utils/lang';
|
||||||
|
|
||||||
|
|
||||||
|
const { arrayOf, func, number, shape } = React.PropTypes;
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
handleCancelHashing: func.isRequired,
|
||||||
|
hashingFiles: arrayOf(shape({
|
||||||
|
progress: number.isRequired
|
||||||
|
})).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadFileDialogHashProgress = ({ handleCancelHashing, hashingFiles }) => {
|
||||||
|
const hashingProgress = 100 * hashingFiles
|
||||||
|
.reduce((total, { progress }) => total + (progress / hashingFiles.length), 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-drag-and-drop-hashing-dialog">
|
||||||
|
<p>
|
||||||
|
{getLangText(`Computing ${hashingFiles.length > 1 ? 'hashes' : 'hash'}... ` +
|
||||||
|
'This may take a few minutes.')}
|
||||||
|
</p>
|
||||||
|
<a onClick={handleCancelHashing} tabIndex={0}>
|
||||||
|
{getLangText('Cancel hashing')}
|
||||||
|
</a>
|
||||||
|
<ProgressBar
|
||||||
|
className="ascribe-progress-bar"
|
||||||
|
label="%(percent)s%"
|
||||||
|
now={Math.ceil(hashingProgress)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UploadFileDialogHashProgress.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UploadFileDialogPrintWrapper(UploadFileDialogHashProgress);
|
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Link from 'react-router/es6/Link';
|
||||||
|
|
||||||
|
import UploadFileDialogPrintWrapper from './upload_file_dialog_print_wrapper';
|
||||||
|
|
||||||
|
import withContext from '../../../context/with_context';
|
||||||
|
|
||||||
|
import { locationShape } from '../../../prop_types';
|
||||||
|
|
||||||
|
import { UploadMethods } from '../../../../constants/uploader_constants';
|
||||||
|
|
||||||
|
import { getLangText } from '../../../../utils/lang';
|
||||||
|
import { getCurrentQueryParams } from '../../../../utils/url';
|
||||||
|
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
location: locationShape.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadFileDialogMethodSelector = ({ location }) => {
|
||||||
|
const currentQueryParams = getCurrentQueryParams();
|
||||||
|
const queryParamsHash = Object.assign({}, currentQueryParams, {
|
||||||
|
uploadMethod: UploadMethods.HASH
|
||||||
|
});
|
||||||
|
const queryParamsNormal = Object.assign({}, currentQueryParams, {
|
||||||
|
uploadMethod: UploadMethods.NORMAL
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-drag-and-drop-dialog">
|
||||||
|
<div className="present-options">
|
||||||
|
<p className="file-drag-and-drop-dialog-title">
|
||||||
|
{getLangText('Would you rather')}
|
||||||
|
</p>
|
||||||
|
<Link query={queryParamsHash} to={location.pathname}>
|
||||||
|
<button className="btn btn-default btn-sm">
|
||||||
|
{getLangText('Hash your work')}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
<span>{getLangText('or')}</span>
|
||||||
|
<Link query={queryParamsNormal} to={location.pathname}>
|
||||||
|
<button className="btn btn-default btn-sm">
|
||||||
|
{getLangText('Upload and hash your work')}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UploadFileDialogMethodSelector.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UploadFileDialogPrintWrapper(withContext(UploadFileDialogMethodSelector, 'location'));
|
@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import UploadFilePreview from '../upload_file_preview';
|
||||||
|
|
||||||
|
|
||||||
|
const { arrayOf, func, object } = React.PropTypes;
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
files: arrayOf(object).isRequired,
|
||||||
|
|
||||||
|
// Mapping of file ids to thumbnail urls for the file previews.
|
||||||
|
// If the file already has a `thumbnailUrl` property, prefer that over checking this mapping.
|
||||||
|
thumbnailMapping: object,
|
||||||
|
|
||||||
|
// Props used by UploadFilePreview
|
||||||
|
handleCancelFile: func.isRequired,
|
||||||
|
handleDeleteFile: func.isRequired,
|
||||||
|
handlePauseFile: func.isRequired,
|
||||||
|
handleResumeFile: func.isRequired,
|
||||||
|
|
||||||
|
// All other props are passed down to UploadFilePreviews, including:
|
||||||
|
// * downloadable: enable files to be downloaded
|
||||||
|
// * pausable: enable pause / resume functionality
|
||||||
|
// * removable: enable files to be removed
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadFileDialogPreviewIterator = ({ files, thumbnailMapping, ...props }) => {
|
||||||
|
if (files.length) {
|
||||||
|
const multipleFiles = files.length > 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{files.map((file, i) => {
|
||||||
|
// Try to use an id from the file, but if we can't find one, just use its array
|
||||||
|
// index.
|
||||||
|
const key = file.uuid || file.id || i;
|
||||||
|
|
||||||
|
const thumbnailUrl = file.thumbnailUrl ||
|
||||||
|
(thumbnailMapping && 'id' in file ? thumbnailMapping[file.id] : null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UploadFilePreview
|
||||||
|
key={key}
|
||||||
|
{...props}
|
||||||
|
file={file}
|
||||||
|
showName={!multipleFiles}
|
||||||
|
showProgress={multipleFiles}
|
||||||
|
thumbnailUrl={thumbnailUrl} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
UploadFileDialogPreviewIterator.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UploadFileDialogPreviewIterator;
|
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getLangText } from '../../../../utils/lang';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper component to hide the given upload file dialog component from being printed, instead
|
||||||
|
* replacing it with a message saying that there's nothing uploaded yet. Useful for UI components
|
||||||
|
* that are used to show a pre-upload state of the upload file dialog.
|
||||||
|
*/
|
||||||
|
const UploadFileDialogPrintWrapper = (Component) => (
|
||||||
|
(props) => (
|
||||||
|
<div>
|
||||||
|
<div className="hidden-print">
|
||||||
|
<Component {...props} />
|
||||||
|
</div>
|
||||||
|
<p className="text-align-center visible-print">
|
||||||
|
{getLangText('No files uploaded')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UploadFileDialogPrintWrapper;
|
@ -0,0 +1,146 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { failedFilesFilter, validProgressFilesFilter } from 'react-utility-belt/es6/uploader/utils/file_filters';
|
||||||
|
|
||||||
|
import UploadFileDialogErrorHandler from './upload_file_dialog_error_handler';
|
||||||
|
import UploadFileDialogFileSelector from './upload_file_dialog_file_selector';
|
||||||
|
import UploadFileDialogHashProgress from './upload_file_dialog_hash_progress';
|
||||||
|
import UploadFileDialogMethodSelector from './upload_file_dialog_method_selector';
|
||||||
|
import UploadFileDialogPreviewIterator from './upload_file_dialog_preview_iterator';
|
||||||
|
|
||||||
|
import UploadProgressBar from '../upload_progress_bar';
|
||||||
|
|
||||||
|
import { UploadMethods } from '../../../../constants/uploader_constants';
|
||||||
|
|
||||||
|
|
||||||
|
const { arrayOf, bool, func, number, object, oneOf, shape, string } = React.PropTypes;
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
/**
|
||||||
|
* Upload method being used by the uploader.
|
||||||
|
*
|
||||||
|
* If USE_URL_PARAM is used, assume that the upload method is being determined by the url
|
||||||
|
* parameters and show a selector that will modify the current url's query parameters to be
|
||||||
|
* one of the available methods.
|
||||||
|
*/
|
||||||
|
uploadMethod: oneOf([
|
||||||
|
UploadMethods.HASH,
|
||||||
|
UploadMethods.NORMAL,
|
||||||
|
UploadMethods.USE_URL_PARAM
|
||||||
|
]).isRequired,
|
||||||
|
|
||||||
|
areAssetsDownloadable: bool,
|
||||||
|
areAssetsEditable: bool,
|
||||||
|
disabled: bool,
|
||||||
|
errorClass: object,
|
||||||
|
fileTypeNames: shape({
|
||||||
|
plural: string.isRequired,
|
||||||
|
singular: string.isRequired
|
||||||
|
}),
|
||||||
|
hashingFiles: arrayOf(shape({
|
||||||
|
progress: number.isRequired
|
||||||
|
})),
|
||||||
|
showError: bool,
|
||||||
|
thumbnailMapping: object,
|
||||||
|
|
||||||
|
// Provided by ReactS3FineUploader
|
||||||
|
// eslint-disable-next-line react/sort-prop-types
|
||||||
|
uploaderFiles: arrayOf(object).isRequired,
|
||||||
|
multiple: bool
|
||||||
|
|
||||||
|
// All other props are passed through to the child components
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextTypes = {
|
||||||
|
handleCancelFile: func.isRequired,
|
||||||
|
handleCancelHashing: func.isRequired,
|
||||||
|
handleDeleteFile: func.isRequired,
|
||||||
|
handlePauseFile: func.isRequired,
|
||||||
|
handleResumeFile: func.isRequired,
|
||||||
|
handleRetryFile: func.isRequired,
|
||||||
|
handleSelectFiles: func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadFileDialogUI = ({
|
||||||
|
areAssetsDownloadable,
|
||||||
|
areAssetsEditable,
|
||||||
|
disabled,
|
||||||
|
errorClass,
|
||||||
|
hashingFiles,
|
||||||
|
multiple,
|
||||||
|
showError,
|
||||||
|
thumbnailMapping,
|
||||||
|
uploaderFiles,
|
||||||
|
uploadMethod,
|
||||||
|
...props
|
||||||
|
}, {
|
||||||
|
handleCancelFile,
|
||||||
|
handleCancelHashing,
|
||||||
|
handleDeleteFile,
|
||||||
|
handlePauseFile,
|
||||||
|
handleResumeFile,
|
||||||
|
handleRetryFile,
|
||||||
|
handleSelectFiles
|
||||||
|
}) => {
|
||||||
|
let uploaderUI;
|
||||||
|
if (uploadMethod === UploadMethods.USE_URL_PARAMS) {
|
||||||
|
// Show upload method selector that will change the current url's query parameters
|
||||||
|
uploaderUI = (
|
||||||
|
<UploadFileDialogMethodSelector uploadMethod={UploadMethods.USE_URL_PARAMS} />
|
||||||
|
);
|
||||||
|
} else if (Array.isArray(hashingFiles) && hashingFiles.length) {
|
||||||
|
uploaderUI = (
|
||||||
|
<UploadFileDialogHashProgress
|
||||||
|
handleCancelHashing={handleCancelHashing}
|
||||||
|
hashingFiles={hashingFiles} />
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const failedFiles = uploaderFiles.filter(failedFilesFilter);
|
||||||
|
const validFiles = uploaderFiles.filter(validProgressFilesFilter);
|
||||||
|
|
||||||
|
if (errorClass && showError && failedFiles.length) {
|
||||||
|
uploaderUI = (
|
||||||
|
<UploadFileDialogErrorHandler
|
||||||
|
errorClass={errorClass}
|
||||||
|
failedFiles={failedFiles}
|
||||||
|
handleRetryFile={handleRetryFile} />
|
||||||
|
);
|
||||||
|
} else if (validFiles.length) {
|
||||||
|
uploaderUI = [(
|
||||||
|
<UploadFileDialogPreviewIterator
|
||||||
|
key="upload-file-preview-iterator"
|
||||||
|
downloadable={areAssetsDownloadable}
|
||||||
|
files={validFiles}
|
||||||
|
handleCancelFile={handleCancelFile}
|
||||||
|
handleDeleteFile={handleDeleteFile}
|
||||||
|
handlePauseFile={handlePauseFile}
|
||||||
|
handleResumeFile={handleResumeFile}
|
||||||
|
removable={areAssetsEditable}
|
||||||
|
thumbnailMapping={thumbnailMapping} />
|
||||||
|
), (
|
||||||
|
<UploadProgressBar
|
||||||
|
key="upload-progress"
|
||||||
|
className="file-drag-and-drop-overall-progress-bar"
|
||||||
|
files={uploaderFiles} />
|
||||||
|
)];
|
||||||
|
} else {
|
||||||
|
uploaderUI = (
|
||||||
|
<UploadFileDialogFileSelector
|
||||||
|
handleSelectFiles={handleSelectFiles}
|
||||||
|
uploadMethod={uploadMethod} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('file-drag-and-drop', { 'inactive-dropzone': disabled })}>
|
||||||
|
{uploaderUI}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UploadFileDialogUI.propTypes = propTypes;
|
||||||
|
UploadFileDialogUI.contextTypes = contextTypes;
|
||||||
|
|
||||||
|
export default UploadFileDialogUI;
|
@ -0,0 +1,3 @@
|
|||||||
|
// Make it easier for users to import this component by default exporting the container in an
|
||||||
|
// index.js
|
||||||
|
export { default } from './upload_file_preview';
|
@ -0,0 +1,140 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { pausedFilesFilter, uploadedFilesFilter, uploadingFilesFilter } from 'react-utility-belt/es6/uploader/utils/file_filters';
|
||||||
|
|
||||||
|
import UploadFilePreviewImage from './upload_file_preview_image';
|
||||||
|
import UploadFilePreviewOther from './upload_file_preview_other';
|
||||||
|
|
||||||
|
import UploadProgressBar from '../upload_progress_bar';
|
||||||
|
|
||||||
|
import { extractFileExtensionFromString } from '../../../../utils/file';
|
||||||
|
import { getLangText } from '../../../../utils/lang';
|
||||||
|
import { truncateText } from '../../../../utils/text';
|
||||||
|
|
||||||
|
|
||||||
|
const { bool, func, shape, string } = React.PropTypes;
|
||||||
|
|
||||||
|
const UploadFileName = ({ file: { name } }) => (
|
||||||
|
<span className="upload-file-preview--label">
|
||||||
|
{truncateText(name, 30, `(...).${extractFileExtensionFromString(name)}`)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
UploadFileName.propTypes = {
|
||||||
|
file: shape({
|
||||||
|
name: string.isRequired
|
||||||
|
}).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadRemoveButton = ({ handleRemoveFile }) => (
|
||||||
|
<button className="remove-file-btn text-center" onClick={handleRemoveFile}>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="glyphicon glyphicon-remove"
|
||||||
|
title={getLangText('Remove file')} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
UploadRemoveButton.propTypes = {
|
||||||
|
handleRemoveFile: func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const UploadFilePreview = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
file: shape({
|
||||||
|
name: string.isRequired,
|
||||||
|
type: string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
handleCancelFile: func.isRequired,
|
||||||
|
handleDeleteFile: func.isRequired,
|
||||||
|
handlePauseFile: func.isRequired,
|
||||||
|
handleResumeFile: func.isRequired,
|
||||||
|
|
||||||
|
className: string,
|
||||||
|
downloadable: bool,
|
||||||
|
pausable: bool,
|
||||||
|
removable: bool,
|
||||||
|
showName: bool,
|
||||||
|
showProgress: bool,
|
||||||
|
thumbnailUrl: string
|
||||||
|
},
|
||||||
|
|
||||||
|
handleRemoveFile() {
|
||||||
|
const { file, handleCancelFile, handleDeleteFile } = this.props;
|
||||||
|
|
||||||
|
// We only want to delete when we're sure that the file has been *completely* uploaded to S3
|
||||||
|
// and can now be properly deleted using an HTTP DELETE request.
|
||||||
|
if (uploadedFilesFilter(file) && file.s3UrlSafe) {
|
||||||
|
handleDeleteFile(file);
|
||||||
|
} else {
|
||||||
|
handleCancelFile(file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleUploadProcess() {
|
||||||
|
const { file, handlePauseFile, handleResumeFile } = this.props;
|
||||||
|
|
||||||
|
if (uploadingFilesFilter(file)) {
|
||||||
|
handlePauseFile(file.id);
|
||||||
|
} else if (pausedFilesFilter(file)) {
|
||||||
|
handleResumeFile(file.id);
|
||||||
|
} else {
|
||||||
|
console.logGlobal(
|
||||||
|
new Error('Tried to pause / resume upload of file that was not in a paused or ' +
|
||||||
|
'uploading state'),
|
||||||
|
{ file }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
downloadable,
|
||||||
|
file,
|
||||||
|
pausable,
|
||||||
|
removable,
|
||||||
|
showName,
|
||||||
|
showProgress,
|
||||||
|
thumbnailUrl
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const previewProps = {
|
||||||
|
downloadable,
|
||||||
|
file,
|
||||||
|
pausable,
|
||||||
|
toggleUploadProcess: this.toggleUploadProcess
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decide whether an image or a placeholder thumbnail should be displayed
|
||||||
|
// Even if a file is not an image, we'll display it as an image if it has has a thumbnail
|
||||||
|
const previewElement = (thumbnailUrl || file.type.split('/')[0] === 'image')
|
||||||
|
? (<UploadFilePreviewImage {...previewProps} thumbnailUrl={thumbnailUrl || file.url} />)
|
||||||
|
: (<UploadFilePreviewOther {...previewProps} />);
|
||||||
|
|
||||||
|
const fileProgressBar = showProgress ? (
|
||||||
|
<UploadProgressBar className="ascribe-progress-bar-xs" files={[file]} />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const fileRemoveButton = removable ? (
|
||||||
|
<UploadRemoveButton handleRemoveFile={this.handleRemoveFile} />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const fileName = showName ? (<UploadFileName file={file} />) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(className, 'upload-file-preview-container')}>
|
||||||
|
<div className="upload-file-preview">
|
||||||
|
{fileProgressBar}
|
||||||
|
{previewElement}
|
||||||
|
{fileRemoveButton}
|
||||||
|
</div>
|
||||||
|
{fileName}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UploadFilePreview;
|
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import UploadFilePreviewTypeWrapper from './upload_file_preview_type_wrapper';
|
||||||
|
|
||||||
|
|
||||||
|
const { node, string } = React.PropTypes;
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
thumbnailUrl: string.isRequired,
|
||||||
|
|
||||||
|
children: node
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadFilePreviewImage = ({ children, thumbnailUrl }) => {
|
||||||
|
const imageStyle = {
|
||||||
|
backgroundImage: `url("${thumbnailUrl}")`,
|
||||||
|
backgroundSize: 'cover'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-drag-and-drop-preview-image hidden-print" style={imageStyle}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UploadFilePreviewImage.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UploadFilePreviewTypeWrapper(UploadFilePreviewImage);
|
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import UploadFilePreviewTypeWrapper from './upload_file_preview_type_wrapper';
|
||||||
|
|
||||||
|
import { extractFileExtensionFromString } from '../../../../utils/file';
|
||||||
|
|
||||||
|
|
||||||
|
const { bool, node, shape, string } = React.PropTypes;
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
file: shape({
|
||||||
|
name: string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
children: node,
|
||||||
|
showType: bool
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadFilePreviewOther = ({ children, showType, file: { name } }) => (
|
||||||
|
<div className="file-drag-and-drop-preview-other">
|
||||||
|
{children}
|
||||||
|
{showType ? (
|
||||||
|
<p className="file-drag-and-drop-preview-other--label">
|
||||||
|
{`.${extractFileExtensionFromString(name) || 'file'}`}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
UploadFilePreviewOther.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UploadFilePreviewTypeWrapper(UploadFilePreviewOther);
|
@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { pausedFilesFilter, uploadedFilesFilter, uploadingFilesFilter } from 'react-utility-belt/es6/uploader/utils/file_filters';
|
||||||
|
|
||||||
|
import AscribeSpinner from '../../../ascribe_spinner';
|
||||||
|
|
||||||
|
import { getLangText } from '../../../../utils/lang';
|
||||||
|
|
||||||
|
|
||||||
|
const { bool, func, shape, string } = React.PropTypes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper component for preview types that handles upload states and pause / resume functionality
|
||||||
|
*/
|
||||||
|
const displayName = 'UploadFilePreviewTypeWrapper';
|
||||||
|
const propTypes = {
|
||||||
|
file: shape({
|
||||||
|
s3UrlSafe: string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
// Allow the file to be downloaded
|
||||||
|
downloadable: bool,
|
||||||
|
|
||||||
|
// Enable upload pause / resume functionality
|
||||||
|
pausable: bool,
|
||||||
|
|
||||||
|
// Controls pausing / resuming the file if pause / resume functionality is enabled
|
||||||
|
toggleUploadProcess: func
|
||||||
|
|
||||||
|
// All props are passed through to the PreviewComponent as well
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadFilePreviewTypeWrapper = (PreviewComponent) => {
|
||||||
|
const wrapperComponent = (props) => {
|
||||||
|
const { downloadable, file, pausable, toggleUploadProcess } = props;
|
||||||
|
const { s3UrlSafe } = file;
|
||||||
|
let uploadStateSymbol;
|
||||||
|
|
||||||
|
if (uploadedFilesFilter(file)) {
|
||||||
|
// Uploaded
|
||||||
|
uploadStateSymbol = downloadable ? (
|
||||||
|
<a
|
||||||
|
aria-hidden
|
||||||
|
className="glyphicon glyphicon-download action-file"
|
||||||
|
href={s3UrlSafe}
|
||||||
|
target="_blank"
|
||||||
|
title={getLangText('Download file')} />
|
||||||
|
) : (
|
||||||
|
<span className='ascribe-icon icon-ascribe-ok action-file' />
|
||||||
|
);
|
||||||
|
} else if (uploadingFilesFilter(file)) {
|
||||||
|
// Uploading
|
||||||
|
uploadStateSymbol = pausable ? (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="glyphicon glyphicon-pause action-file"
|
||||||
|
onClick={toggleUploadProcess}
|
||||||
|
title={getLangText('Pause upload')} />
|
||||||
|
) : (
|
||||||
|
<AscribeSpinner className="spinner-file" color='dark-blue' size='md' />
|
||||||
|
);
|
||||||
|
} else if (pausedFilesFilter(file)) {
|
||||||
|
// Paused
|
||||||
|
if (pausable) {
|
||||||
|
uploadStateSymbol = (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="glyphicon glyphicon-play action-file"
|
||||||
|
onClick={toggleUploadProcess}
|
||||||
|
title={getLangText('Resume uploading')} />
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.logGlobal(
|
||||||
|
new Error('UploadFilePreview encountered paused file but did not have resume ' +
|
||||||
|
'functionality enabled'),
|
||||||
|
{ file }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.logGlobal(
|
||||||
|
new Error('UploadFilePreview encountered file that was not uploading, uploaded, ' +
|
||||||
|
'or paused'),
|
||||||
|
{ file }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreviewComponent {...props} file={file}>
|
||||||
|
{uploadStateSymbol}
|
||||||
|
</PreviewComponent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapperComponent.displayName = displayName;
|
||||||
|
wrapperComponent.propTypes = propTypes;
|
||||||
|
|
||||||
|
return wrapperComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UploadFilePreviewTypeWrapper;
|
46
js/components/ascribe_uploader/ui/upload_progress_bar.js
Normal file
46
js/components/ascribe_uploader/ui/upload_progress_bar.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
|
||||||
|
|
||||||
|
|
||||||
|
const { arrayOf, number, shape, string } = React.PropTypes;
|
||||||
|
|
||||||
|
function calcOverallFileSize(files) {
|
||||||
|
// We just sum up all files' sizes
|
||||||
|
return files.reduce((overallSize, { size }) => overallSize + size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcOverallProgress(files) {
|
||||||
|
const overallFileSize = calcOverallFileSize(files);
|
||||||
|
|
||||||
|
// We calculate the overall progress by summing the individuals files' progresses in relation
|
||||||
|
// to the total size of all uploads
|
||||||
|
return files.reduce((overallProgress, { progress, size }) => (
|
||||||
|
(size / overallFileSize) * progress
|
||||||
|
), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
files: arrayOf(shape({
|
||||||
|
progress: number.isRequired,
|
||||||
|
size: number.isRequired
|
||||||
|
})).isRequired,
|
||||||
|
|
||||||
|
className: string
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadProgressBar = ({ className, files }) => {
|
||||||
|
const overallProgress = Math.ceil(calcOverallProgress(files));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProgressBar
|
||||||
|
className={classNames(className, 'ascribe-progress-bar', { 'hidden': !files.length })}
|
||||||
|
label={`${overallProgress}%`}
|
||||||
|
now={overallProgress} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UploadProgressBar.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UploadProgressBar;
|
341
js/components/ascribe_uploader/uploader.js
Normal file
341
js/components/ascribe_uploader/uploader.js
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { AscribeFileHashUploaderFactory, AscribeUploaderFactory } from 'ascribe-react-components/es6/uploader/ascribe_uploader_factory';
|
||||||
|
import { uploaderCreateBlobParamsShapeSpec } from 'ascribe-react-components/es6/prop_types/uploader_create_blob_params_shape';
|
||||||
|
import { uploaderRequestKeyParamsShapeSpec } from 'ascribe-react-components/es6/prop_types/uploader_request_key_params_shape';
|
||||||
|
import ValidationErrors from 'react-utility-belt/es6/uploader/constants/validation_errors';
|
||||||
|
import uploaderSpecExtender from 'react-utility-belt/es6/uploader/utils/uploader_spec_extender';
|
||||||
|
|
||||||
|
import { safeInvoke } from 'js-utility-belt/es6';
|
||||||
|
|
||||||
|
import S3Fetcher from '../../fetchers/s3_fetcher';
|
||||||
|
|
||||||
|
import GlobalNotificationModel from '../../models/global_notification_model';
|
||||||
|
import GlobalNotificationActions from '../../actions/global_notification_actions';
|
||||||
|
|
||||||
|
import { S3_ACCESS_KEY, S3_ACL, S3_BUCKET } from '../../constants/uploader_constants';
|
||||||
|
|
||||||
|
import { makeCsrfHeader as createCsrfHeader } from '../../utils/csrf';
|
||||||
|
import { kbToMb } from '../../utils/file';
|
||||||
|
import { getLangText } from '../../utils/lang';
|
||||||
|
import request from '../../utils/request';
|
||||||
|
import { resolveUrl } from '../../utils/url_resolver';
|
||||||
|
|
||||||
|
|
||||||
|
const { bool, func, number, shape, string } = React.PropTypes;
|
||||||
|
|
||||||
|
const Uploader = React.createClass(uploaderSpecExtender({
|
||||||
|
displayName: 'Uploader',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// Whether the selected files should be hashed locally before being uploaded
|
||||||
|
hashLocally: bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to control the notification (if any) that is shown when an upload fails.
|
||||||
|
*
|
||||||
|
* @param {File} file File that errored
|
||||||
|
* @param {string} errorReason Reason for the error
|
||||||
|
* @param {Xhr|Xdr} xhr The xhr used to make the request
|
||||||
|
*/
|
||||||
|
showErrorNotification: func,
|
||||||
|
|
||||||
|
// AscribeBlobUploader props
|
||||||
|
// Extend the base createBlobParamsShape with piece details
|
||||||
|
// eslint-disable-next-line react/sort-prop-types
|
||||||
|
createBlobParams: shape({
|
||||||
|
...uploaderCreateBlobParamsShapeSpec,
|
||||||
|
body: shape({
|
||||||
|
'piece_id': number
|
||||||
|
}).isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
onCreateBlobError: func,
|
||||||
|
onCreateBlobSuccess: func,
|
||||||
|
|
||||||
|
// AscribeRequestKeyUploader props
|
||||||
|
// Extend the base requestKeyParamsShape with category and piece details
|
||||||
|
// eslint-disable-next-line react/sort-prop-types
|
||||||
|
requestKeyParams: shape({
|
||||||
|
...uploaderRequestKeyParamsShapeSpec,
|
||||||
|
body: shape({
|
||||||
|
'category': string.isRequired,
|
||||||
|
'piece_id': number,
|
||||||
|
}).isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
onRequestKeyError: func,
|
||||||
|
onRequestKeySuccess: func,
|
||||||
|
|
||||||
|
// All other props are passed through to AscribeUploader
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps() {
|
||||||
|
return {
|
||||||
|
onCreateBlobError: (err, file) => this.onError(file, err && err.message),
|
||||||
|
onCreateBlobSuccess: this.defaultOnCreateBlobSuccess,
|
||||||
|
onDeleteOnlineFile: this.defaultOnDeleteOnlineFile,
|
||||||
|
onRequestKeyError: (err, file) => this.onError(file, err && err.message),
|
||||||
|
onRequestKeySuccess: this.defaultOnRequestKeySuccess,
|
||||||
|
onSessionRequestComplete: this.defaultOnSessionRequestComplete,
|
||||||
|
showErrorNotification: (file, errorReason) => {
|
||||||
|
const message = errorReason || getLangText('Oops, we had a problem uploading ' +
|
||||||
|
'your file. Please contact us if this ' +
|
||||||
|
'happens repeatedly.');
|
||||||
|
|
||||||
|
const notification = new GlobalNotificationModel(message, 'danger', 5000);
|
||||||
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
// Create uploaders based on current app settings
|
||||||
|
// Let's assume that these settings won't change without forcing a remounting of this
|
||||||
|
// component.
|
||||||
|
const [AscribeUploader, AscribeFileHashUploader] = [
|
||||||
|
AscribeUploaderFactory, AscribeFileHashUploaderFactory
|
||||||
|
].map((factory) => factory({
|
||||||
|
createCsrfHeader,
|
||||||
|
// Note that our request module already includes the CSRF token on every call, so we
|
||||||
|
// don't have to add it to the blob and request key headers ourselves
|
||||||
|
request,
|
||||||
|
S3_ACCESS_KEY,
|
||||||
|
S3_ACL,
|
||||||
|
S3_BUCKET,
|
||||||
|
Urls: {
|
||||||
|
S3_DELETE: resolveUrl('s3_delete_file'),
|
||||||
|
S3_SIGNATUURE: resolveUrl('s3_signature')
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Cache uploaders on this component
|
||||||
|
this.AscribeUploader = AscribeUploader;
|
||||||
|
this.AscribeFileHashUploader = AscribeFileHashUploader;
|
||||||
|
},
|
||||||
|
|
||||||
|
getXhrErrorComment(xhr) {
|
||||||
|
return xhr && {
|
||||||
|
response: xhr.response,
|
||||||
|
url: xhr.responseURL,
|
||||||
|
status: xhr.status,
|
||||||
|
statusText: xhr.statusText
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/** DEFAULT EVENT HANDLERS (CAN BE OVERRIDDEN COMPLETELY THROUGH PROPS) **/
|
||||||
|
defaultOnCreateBlobSuccess(res, file) {
|
||||||
|
// `res` should contain one of these file types as a property
|
||||||
|
const fileType = res.otherdata || res.digitalwork || res.contractblob || res.thumbnail;
|
||||||
|
|
||||||
|
if (!fileType) {
|
||||||
|
const errorMsg = getLangText(
|
||||||
|
'Could not find a s3 url as the download location of the file: %s',
|
||||||
|
file.name
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeSet = { $set: fileType.url_safe };
|
||||||
|
return {
|
||||||
|
s3Url: changeSet,
|
||||||
|
s3UrlSafe: changeSet
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultOnDeleteOnlineFile(file) {
|
||||||
|
return S3Fetcher.deleteFile(file.s3Key, file.s3Bucket);
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultOnRequestKeySuccess(res) {
|
||||||
|
return res.key;
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultOnSessionRequestComplete(response, success) {
|
||||||
|
if (!success) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.forEach((file) => {
|
||||||
|
file.url = file.s3UrlSafe;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** EXTENDED EVENT HANDLERS (ADDS ADDITIONAL BEHAVIOUR TO CALLBACK) **/
|
||||||
|
onCanceled(file, ...args) {
|
||||||
|
const notification = new GlobalNotificationModel(
|
||||||
|
getLangText('Upload of "%s" cancelled', file.name),
|
||||||
|
'success',
|
||||||
|
5000
|
||||||
|
);
|
||||||
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||||
|
|
||||||
|
safeInvoke(this.props.onCanceled, file, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
onDeleteComplete(file, xhr, isError, ...args) {
|
||||||
|
const notificationTemplate = isError ? 'There was an error deleting "${name}"'
|
||||||
|
: '"${name}" deleted';
|
||||||
|
|
||||||
|
const notification = new GlobalNotificationModel(
|
||||||
|
getLangText(notificationTemplate, file),
|
||||||
|
isError ? 'danger' : 'success',
|
||||||
|
5000
|
||||||
|
);
|
||||||
|
|
||||||
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||||
|
|
||||||
|
safeInvoke(this.props.onDeleteComplete, file, xhr, isError, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
onError(file, errorReason, xhr, ...args) {
|
||||||
|
const { onError, showErrorNotification } = this.props;
|
||||||
|
const { uploader } = this.refs;
|
||||||
|
|
||||||
|
console.logGlobal(errorReason, {
|
||||||
|
files: uploader.getFiles(),
|
||||||
|
chunks: uploader.getChunks(),
|
||||||
|
xhr: this.getXhrErrorComment(xhr)
|
||||||
|
});
|
||||||
|
|
||||||
|
safeInvoke(showErrorNotification, file, errorReason, xhr, ...args);
|
||||||
|
safeInvoke(onError, file, errorReason, xhr, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
onFileHashError(error, ...args) {
|
||||||
|
let notification;
|
||||||
|
|
||||||
|
if (error && error.message && error.message.toLowerCase().includes('cancel')) {
|
||||||
|
notification = new GlobalNotificationModel(error.message, 'success', 5000);
|
||||||
|
} else {
|
||||||
|
notification = new GlobalNotificationModel(
|
||||||
|
'Failed to hash files. Please contact us if this problem persists.',
|
||||||
|
'danger',
|
||||||
|
5000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||||
|
|
||||||
|
const {
|
||||||
|
invoked,
|
||||||
|
result: errorResult
|
||||||
|
} = safeInvoke(this.props.onFileHashError, error, ...args);
|
||||||
|
|
||||||
|
// If `onFileHashError` doesn't throw its own error or isn't invoked, rethrow the original
|
||||||
|
// error back to the uploader
|
||||||
|
if (invoked) {
|
||||||
|
return errorResult;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onValidationError(errors, validFiles, ...args) {
|
||||||
|
const numFileLimitErrors = errors
|
||||||
|
.filter((error) => error.validationError.type === ValidationErrors.FILE_LIMIT)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
if (numFileLimitErrors === errors.length) {
|
||||||
|
// Validation failed because number of files submitted was over the limit
|
||||||
|
const { limit, remaining } = errors[0].validationError.description;
|
||||||
|
let notification;
|
||||||
|
|
||||||
|
// If we are currently under the limit, just select as many files from the files as
|
||||||
|
// possible
|
||||||
|
if (remaining) {
|
||||||
|
const firstSubmittedFiles = errors
|
||||||
|
.slice(0, remaining)
|
||||||
|
.map((error) => error.file);
|
||||||
|
|
||||||
|
validFiles.push(...firstSubmittedFiles);
|
||||||
|
|
||||||
|
notification = new GlobalNotificationModel(
|
||||||
|
getLangText(
|
||||||
|
'Only %s were allowed (took first %s)',
|
||||||
|
`${remaining} ${getLangText(remaining === 1 ? 'file' : 'files')}`,
|
||||||
|
remaining
|
||||||
|
),
|
||||||
|
'danger',
|
||||||
|
10000
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notification = new GlobalNotificationModel(
|
||||||
|
getLangText(
|
||||||
|
"Oops, you've already uploaded the maximum number of items (%s)!",
|
||||||
|
limit
|
||||||
|
),
|
||||||
|
'danger',
|
||||||
|
10000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||||
|
} else {
|
||||||
|
// Validation failed because of only some items; ignore those and notify the user
|
||||||
|
// If a lot of submitted files fail validation, this might be spammy
|
||||||
|
errors
|
||||||
|
.map(({ file, validationError: { description, type } }) => {
|
||||||
|
switch (type) {
|
||||||
|
case ValidationErrors.SIZE:
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
return getLangText('Cancelled upload of "${name}" as it was bigger than ${size}MB', {
|
||||||
|
name: file.name,
|
||||||
|
size: kbToMb(description.limit)
|
||||||
|
});
|
||||||
|
case ValidationErrors.EXTENSION:
|
||||||
|
return getLangText(
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
'Cancelled upload of "${name}" as it has an invalid file format (valid formats: ${allowedExt})', {
|
||||||
|
allowedExt: description.allowedExtensions.join(', '),
|
||||||
|
name: file.name
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return getLangText(
|
||||||
|
'Cancelled upload of "${name}" as it failed file validation',
|
||||||
|
file
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forEach((errorMsg) => {
|
||||||
|
const notification = new GlobalNotificationModel(errorMsg, 'danger', 5000);
|
||||||
|
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
invoked,
|
||||||
|
result
|
||||||
|
} = safeInvoke(this.props.onValidationError, errors, validFiles, ...args);
|
||||||
|
|
||||||
|
return invoked ? result : validFiles;
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { AscribeUploader, AscribeFileHashUploader } = this;
|
||||||
|
const { hashLocally, ...props } = this.props;
|
||||||
|
|
||||||
|
const uploaderProps = {
|
||||||
|
...props,
|
||||||
|
onCanceled: this.onCanceled,
|
||||||
|
onDeleteComplete: this.onDeleteComplete,
|
||||||
|
onError: this.onError,
|
||||||
|
onValidationError: this.onValidationError,
|
||||||
|
|
||||||
|
// Mandatory for uploaderSpecExtender
|
||||||
|
ref: 'uploader'
|
||||||
|
};
|
||||||
|
|
||||||
|
return hashLocally ? (
|
||||||
|
<AscribeFileHashUploader
|
||||||
|
{...uploaderProps}
|
||||||
|
onFileHashError={this.onFileHashError} />
|
||||||
|
) : (<AscribeUploader {...uploaderProps} />);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default Uploader;
|
@ -4,11 +4,11 @@ import React from 'react';
|
|||||||
const { number, object, shape, string } = React.PropTypes;
|
const { number, object, shape, string } = React.PropTypes;
|
||||||
|
|
||||||
const currentUserShapeSpec = {
|
const currentUserShapeSpec = {
|
||||||
acl: object,
|
acl: object.isRequired,
|
||||||
email: string,
|
email: string.isRequired,
|
||||||
id: number,
|
id: number.isRequired,
|
||||||
profile: object,
|
profile: object.isRequired,
|
||||||
username: string
|
username: string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default shape(currentUserShapeSpec);
|
export default shape(currentUserShapeSpec);
|
||||||
|
@ -4,10 +4,10 @@ import React from 'react';
|
|||||||
const { shape, string } = React.PropTypes;
|
const { shape, string } = React.PropTypes;
|
||||||
|
|
||||||
const whitelabelShapeSpec = {
|
const whitelabelShapeSpec = {
|
||||||
name: string,
|
name: string.isRequired,
|
||||||
subdomain: string,
|
subdomain: string.isRequired,
|
||||||
title: string,
|
title: string.isRequired,
|
||||||
user: string
|
user: string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default shape(whitelabelShapeSpec);
|
export default shape(whitelabelShapeSpec);
|
||||||
|
@ -1,3 +1,13 @@
|
|||||||
|
// Upload types:
|
||||||
|
// * HASH: Hash the file before uploading
|
||||||
|
// * NORMAL: Upload the file as-is
|
||||||
|
// * USE_URL_PARAM: Determine the type to use through the current url's `upload_method` parameter
|
||||||
|
export const UploadMethods = {
|
||||||
|
HASH: 'hash',
|
||||||
|
NORMAL: 'normal',
|
||||||
|
USE_URL_PARAM: 'use_url_param'
|
||||||
|
};
|
||||||
|
|
||||||
// Validation types
|
// Validation types
|
||||||
export const ValidationParts = {
|
export const ValidationParts = {
|
||||||
allowedExtensions: {
|
allowedExtensions: {
|
||||||
@ -39,6 +49,7 @@ export const S3_ACL = process.env.S3_ACL;
|
|||||||
export const S3_BUCKET = process.env.S3_BUCKET;
|
export const S3_BUCKET = process.env.S3_BUCKET;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
UploadMethods,
|
||||||
ValidationParts,
|
ValidationParts,
|
||||||
ValidationTypes,
|
ValidationTypes,
|
||||||
RETRY_ATTEMPT_TO_SHOW_CONTACT_US,
|
RETRY_ATTEMPT_TO_SHOW_CONTACT_US,
|
||||||
|
@ -5,3 +5,14 @@ export {
|
|||||||
extractFileExtensionFromString,
|
extractFileExtensionFromString,
|
||||||
extractFileExtensionFromUrl
|
extractFileExtensionFromUrl
|
||||||
} from 'js-utility-belt/es6/file';
|
} from 'js-utility-belt/es6/file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms kb size to mb size, using an arbitrary rounding function at the end.
|
||||||
|
*
|
||||||
|
* @param {number} size Size in kb
|
||||||
|
* @param {function} roundFn Function to round out the precise size in mb, defaulting to Math.ceil.
|
||||||
|
* @return {number} Size in mb
|
||||||
|
*/
|
||||||
|
export function kbToMb(size, roundFn = Math.ceil) {
|
||||||
|
return roundFn(size / 1024);
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ export {
|
|||||||
deepMatchObject,
|
deepMatchObject,
|
||||||
intersectLists,
|
intersectLists,
|
||||||
omitFromObject,
|
omitFromObject,
|
||||||
|
safeInvoke,
|
||||||
safeMerge,
|
safeMerge,
|
||||||
sanitize,
|
sanitize,
|
||||||
sanitizeList,
|
sanitizeList,
|
||||||
|
@ -9,21 +9,18 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding-top: 1.5em;
|
padding-top: 1.5em;
|
||||||
|
|
||||||
@media screen and (max-width: 625px) {
|
|
||||||
.file-name {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inactive-dropzone {
|
.inactive-dropzone {
|
||||||
background-color: rgba(0, 0, 0, 0) !important;
|
background-color: transparent !important;
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.present-options {
|
.file-drag-and-drop-dialog {
|
||||||
|
margin: 0 0 1.5em 0;
|
||||||
|
|
||||||
|
.present-options {
|
||||||
> p {
|
> p {
|
||||||
margin-bottom: .75em !important;
|
margin-bottom: .75em !important;
|
||||||
}
|
}
|
||||||
@ -31,10 +28,7 @@
|
|||||||
.btn {
|
.btn {
|
||||||
margin: 0 1em;
|
margin: 0 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-drag-and-drop-dialog {
|
|
||||||
margin: 0 0 1.5em 0;
|
|
||||||
|
|
||||||
.file-drag-and-drop-dialog-title {
|
.file-drag-and-drop-dialog-title {
|
||||||
font-size: 1.5em !important;
|
font-size: 1.5em !important;
|
||||||
@ -46,6 +40,10 @@
|
|||||||
> .btn {
|
> .btn {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-drag-and-drop-overall-progress-bar {
|
||||||
|
margin-top: 1.3em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-drag-and-drop-hashing-dialog {
|
.file-drag-and-drop-hashing-dialog {
|
||||||
@ -53,44 +51,67 @@
|
|||||||
margin: 1.5em 0 0 0;
|
margin: 1.5em 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-drag-and-drop-position {
|
.remove-file-btn {
|
||||||
|
background-color: black;
|
||||||
|
border-width: 0;
|
||||||
|
border-radius: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0;
|
||||||
|
width: 20px;
|
||||||
|
|
||||||
|
.glyphicon-remove {
|
||||||
|
color: white;
|
||||||
|
font-size: .8em;
|
||||||
|
left: 0;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .glyphicon-remove {
|
||||||
|
color: $brand-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-preview-container {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: .7em;
|
margin-left: .7em;
|
||||||
margin-right: .7em;
|
margin-right: .7em;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.delete-file {
|
&:not(:only-of-type) {
|
||||||
background-color: black;
|
.upload-file-preview {
|
||||||
border-radius: 1em;
|
vertical-align: middle;
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
|
||||||
height: 20px;
|
|
||||||
position: absolute;
|
|
||||||
right: -7px;
|
|
||||||
text-align: center;
|
|
||||||
top: -7px;
|
|
||||||
width: 20px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: white;
|
|
||||||
font-size: .8em;
|
|
||||||
left: 0;
|
|
||||||
top: 1px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $brand-danger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-file-preview--label {
|
||||||
|
padding-top: 5px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-drag-and-drop-preview {
|
.upload-file-preview {
|
||||||
background-color: #eeeeee;
|
background-color: #eeeeee;
|
||||||
border: 1px solid #616161;
|
border: 1px solid #616161;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.remove-file-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: -7px;
|
||||||
|
top: -7px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-file-preview--label {
|
||||||
|
@media screen and (max-width: 625px) {
|
||||||
|
padding-top: 5px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.action-file {
|
.action-file {
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -146,7 +167,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
width: 104px;
|
width: 104px;
|
||||||
|
|
||||||
p {
|
.file-drag-and-drop-preview-other--label {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -319,8 +340,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-button .upload-button--label {
|
||||||
|
margin-left: 1em;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-button--label-icon {
|
||||||
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user