1
0
mirror of https://github.com/ascribe/onion.git synced 2024-12-22 17:33:14 +01:00

Add FileDragAndDropError

This commit is contained in:
Brett Sun 2015-12-08 18:22:11 +01:00
parent 04d7e6951a
commit 79780cfb3a
9 changed files with 471 additions and 100 deletions

View File

@ -22,6 +22,7 @@ let FurtherDetailsFileuploader = React.createClass({
// Props for ReactS3FineUploader
multiple: bool,
showErrorPrompt: bool,
submitFile: func, // TODO: rename to onSubmitFile
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
@ -42,6 +43,7 @@ let FurtherDetailsFileuploader = React.createClass({
otherData,
pieceId,
setIsUploadReady,
showErrorPrompt,
submitFile } = this.props;
// Essentially there a three cases important to the fileuploader
@ -101,8 +103,9 @@ let FurtherDetailsFileuploader = React.createClass({
}
}}
areAssetsDownloadable={true}
areAssetsEditable={this.props.editable}
multiple={this.props.multiple} />
areAssetsEditable={editable}
multiple={multiple}
showErrorPrompt={showErrorPrompt} />
</Property>
);
}

View File

@ -173,7 +173,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"
@ -196,7 +197,6 @@ let RegisterPieceForm = React.createClass({
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif']
}}
setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')}
location={location}
fileClassToUpload={{
singular: getLangText('Select representative image'),
plural: getLangText('Select representative images')

View File

@ -25,6 +25,7 @@ const InputFineUploader = React.createClass({
// Props for ReactS3FineUploader
areAssetsDownloadable: bool,
showErrorPrompt: bool,
handleChangedFile: func, // TODO: rename to onChangedFile
submitFile: func, // TODO: rename to onSubmitFile
@ -121,6 +122,7 @@ const InputFineUploader = React.createClass({
fileClassToUpload,
uploadMethod,
handleChangedFile,
showErrorPrompt,
disabled } = this.props;
let editable = isFineUploaderActive;
@ -142,6 +144,7 @@ const InputFineUploader = React.createClass({
isReadyForFormSubmission={isReadyForFormSubmission}
areAssetsDownloadable={areAssetsDownloadable}
areAssetsEditable={editable}
showErrorPrompt={showErrorPrompt}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/',
customHeaders: {

View File

@ -39,6 +39,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({
@ -144,6 +150,17 @@ let FileDragAndDrop = React.createClass({
this.refs.fileSelector.getDOMNode().dispatchEvent(evt);
},
getErrorDialog(failedFiles) {
const { errorClass } = this.props;
return (
<FileDragAndDropErrorDialog
errorClass={errorClass}
files={failedFiles}
handleRetryFiles={this.props.handleRetryFiles} />
);
},
getPreviewIterator() {
const { areAssetsDownloadable, areAssetsEditable, filesToUpload } = this.props;
@ -179,6 +196,8 @@ let FileDragAndDrop = React.createClass({
hashingProgress,
handleCancelHashing,
multiple,
showError,
errorClass,
fileClassToUpload,
allowedExtensions } = this.props;
@ -216,6 +235,7 @@ let FileDragAndDrop = React.createClass({
onDrag={this.handleDrop}
onDragOver={this.handleDragOver}
onDrop={this.handleDrop}>
{hasError ? this.getErrorDialog(failedFiles) : this.getPreviewIterator()}
{!hasFiles && !hasError ? this.getUploadDialog() : null}
{/*
Opera doesn't trigger simulated click events

View File

@ -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 with uploading my file: "));
}
this.retryAllFiles()
}}>
{getLangText(text)}
</button>
);
},
getContactUsDetail() {
return (
<div className='file-drag-and-drop-error'>
<h4>Let us help you</h4>
<p>{getLangText('Still having problems? Give us a call!')}</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='file-drag-and-drop-error-icon'></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;

View File

@ -35,6 +35,8 @@ const ReactS3FineUploader = React.createClass({
propTypes: {
areAssetsDownloadable: bool,
areAssetsEditable: bool,
errorNotificationMessage: string,
showErrorPrompt: bool,
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
@ -152,8 +154,18 @@ const ReactS3FineUploader = React.createClass({
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'
@ -195,22 +207,20 @@ const ReactS3FineUploader = React.createClass({
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: undefined
},
uploadInProgress: false,
// -1: aborted
// -2: uninitialized
@ -228,7 +238,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
});
}
@ -241,8 +251,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 {
@ -263,6 +277,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,
@ -408,6 +423,65 @@ const ReactS3FineUploader = React.createClass({
}
},
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');
}
},
isFileValid(file) {
const { validation } = this.props;
if (validation && file.size > validation.sizeLimit) {
const fileSizeInMegaBytes = validation.sizeLimit / 1000000;
const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
return false;
} else {
return true;
}
},
getUploadErrorClass({ type = 'upload', reason, xhr }) {
const { manualRetryAttempt } = this.state.errorState;
let matchedErrorClass;
// Use the contact us error class if they've retried a number of times
// and are still unsuccessful
if (manualRetryAttempt === RETRY_ATTEMPT_TO_SHOW_CONTACT_US) {
matchedErrorClass = ErrorClasses.upload.contactUs;
} else {
matchedErrorClass = testErrorAgainstAll({ type, reason, xhr });
// If none found, show the next error message
if (!matchedErrorClass) {
matchedErrorClass = ErrorQueueStore.getNextError('upload');
}
}
return matchedErrorClass;
},
getXhrErrorComment(xhr) {
if (xhr) {
return {
response: xhr.response,
url: xhr.responseURL,
status: xhr.status,
statusText: xhr.statusText
};
}
},
/* FineUploader specific callback function handlers */
onUploadChunk(id, name, chunkData) {
@ -438,7 +512,14 @@ const ReactS3FineUploader = React.createClass({
this.setState({ startedChunks });
}
},
onAllComplete(succeed, failed) {
if (this.state.uploadInProgress) {
this.setState({
uploadInProgress: false
});
}
},
onComplete(id, name, res, xhr) {
@ -464,29 +545,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();
});
}
},
@ -502,42 +568,43 @@ const ReactS3FineUploader = React.createClass({
},
onError(id, name, errorReason, xhr) {
const { errorNotificationMessage, showErrorPrompt } = this.props;
const { chunks, filesToUpload } = this.state;
console.logGlobal(errorReason, false, {
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);
GlobalNotificationActions.appendGlobalNotification(notification);
},
if (showErrorPrompt) {
notificationMessage = errorNotificationMessage;
getXhrErrorComment(xhr) {
if (xhr) {
return {
response: xhr.response,
url: xhr.responseURL,
status: xhr.status,
statusText: xhr.statusText
};
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) {
const errorState = React.addons.update(this.state.errorState, {
errorClass: {
$set: this.getUploadErrorClass({
reason: errorReason,
xhr
})
}
},
});
isFileValid(file) {
if(file.size > this.props.validation.sizeLimit) {
let fileSizeInMegaBytes = this.props.validation.sizeLimit / 1000000;
let notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
return false;
this.setState({ errorState });
}
} else {
return true;
notificationMessage = errorReason || errorNotificationMessage;
this.cancelUploads();
}
const notification = new GlobalNotificationModel(notificationMessage, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
onCancel(id) {
@ -552,17 +619,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;
@ -619,20 +687,7 @@ 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) {
@ -692,6 +747,27 @@ const ReactS3FineUploader = React.createClass({
}
},
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
});
},
handleUploadFile(files) {
// While files are being uploaded, the form cannot be ready
// for submission
@ -819,6 +895,9 @@ const ReactS3FineUploader = React.createClass({
if(files.length > 0) {
this.state.uploader.addFiles(files);
this.synchronizeFileLists(files);
this.setState({
uploadInProgress: true
});
}
}
},
@ -920,7 +999,7 @@ const ReactS3FineUploader = React.createClass({
},
isDropzoneInactive() {
const { areAssetsEditable, enableLocalHashing, multiple, showErrorStates, uploadMethod } = this.props;
const { areAssetsEditable, enableLocalHashing, multiple, showErrorPrompt, uploadMethod } = this.props;
const { errorState, filesToUpload } = this.state;
const filesToDisplay = filesToUpload.filter((file) => {
@ -931,7 +1010,7 @@ const ReactS3FineUploader = React.createClass({
});
if ((enableLocalHashing && !uploadMethod) || !areAssetsEditable ||
(showErrorStates && errorState.errorClass) ||
(showErrorPrompt && errorState.errorClass) ||
(!multiple && filesToDisplay.length > 0)) {
return true;
} else {
@ -940,7 +1019,7 @@ const ReactS3FineUploader = React.createClass({
},
getAllowedExtensions() {
let { validation } = this.props;
const { validation } = this.props;
if(validation && validation.allowedExtensions && validation.allowedExtensions.length > 0) {
return transformAllowedExtensionsToInputAcceptProp(validation.allowedExtensions);
@ -950,6 +1029,7 @@ const ReactS3FineUploader = React.createClass({
},
render() {
const { errorState: { errorClass }, filesToUpload, uploadInProgress } = this.state;
const {
multiple,
areAssetsDownloadable,
@ -973,11 +1053,15 @@ const ReactS3FineUploader = React.createClass({
uploadMethod,
fileClassToUpload,
filesToUpload,
uploadInProgress,
errorClass,
showError,
onDrop: this.handleUploadFile,
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,

View File

@ -135,7 +135,7 @@ Object.keys(ErrorClasses).forEach((errorGroupKey) => {
const errorGroup = ErrorClasses[errorGroupKey];
Object.keys(errorGroup).forEach((errorClassKey) => {
const errorClass = errorGroup[errorClassKey];
errorClass.name = errorClassKey;
errorClass.name = errorGroupKey + '-' + errorClassKey;
errorClass.group = errorGroupKey;
});
});

View File

@ -145,7 +145,7 @@ export function escapeHTML(s) {
* Returns a copy of the given object's own and inherited enumerable
* properties, omitting any keys that pass the given filter function.
*/
function filterObjOnFn(obj, filterFn) {
function applyFilterOnObject(obj, filterFn) {
const filteredObj = {};
for (let key in obj) {
@ -158,6 +158,37 @@ function filterObjOnFn(obj, filterFn) {
return filteredObj;
}
/**
* Abstraction for selectFromObject and omitFromObject
* for DRYness
* @param {boolean} isInclusion True if the filter should be for including the filtered items
* (ie. selecting only them vs omitting only them)
*/
function filterFromObject(obj, filter, { isInclusion = true } = {}) {
if (filter && filter.constructor === Array) {
return applyFilterOnObject(obj, isInclusion ? ((_, key) => filter.indexOf(key) < 0)
: ((_, key) => filter.indexOf(key) >= 0));
} else if (filter && typeof filter === 'function') {
// Flip the filter fn's return if it's for inclusion
return applyFilterOnObject(obj, isInclusion ? (...args) => !filter(...args)
: filter);
} else {
throw new Error('The given filter is not an array or function. Exclude aborted');
}
}
/**
* Similar to lodash's _.pick(), this returns a copy of the given object's
* own and inherited enumerable properties, selecting only the keys in
* the given array or whose value pass the given filter function.
* @param {object} obj Source object
* @param {array|function} filter Array of key names to select or function to invoke per iteration
* @return {object} The new object
*/
export function selectFromObject(obj, filter) {
return filterFromObject(obj, filter);
}
/**
* Similar to lodash's _.omit(), this returns a copy of the given object's
* own and inherited enumerable properties, omitting any keys that are
@ -167,15 +198,7 @@ function filterObjOnFn(obj, filterFn) {
* @return {object} The new object
*/
export function omitFromObject(obj, filter) {
if (filter && filter.constructor === Array) {
return filterObjOnFn(obj, (_, key) => {
return filter.indexOf(key) >= 0;
});
} else if (filter && typeof filter === 'function') {
return filterObjOnFn(obj, filter);
} else {
throw new Error('The given filter is not an array or function. Exclude aborted');
}
return filterFromObject(obj, filter, { isInclusion: false });
}
/**

View File

@ -169,6 +169,158 @@
}
}
.file-drag-and-drop-error {
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: inline-block;
float: left;
height: 104px;
position: relative;
width: 104px;
vertical-align: top;
.file-drag-and-drop-error-icon {
position: absolute;
}
&.file-drag-and-drop-error-icon-container-multiple-files {
background-color: #d7d7d7;
left: -15px;
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;
}
.file-drag-and-drop-error-icon {
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 {