From 79780cfb3ad819afa385a0062aa19a3953509002 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 8 Dec 2015 18:22:11 +0100 Subject: [PATCH] Add FileDragAndDropError --- .../further_details_fileuploader.js | 7 +- .../ascribe_forms/form_register_piece.js | 6 +- .../ascribe_forms/input_fineuploader.js | 3 + .../file_drag_and_drop.js | 20 ++ .../file_drag_and_drop_error_dialog.js | 86 ++++++ .../react_s3_fine_uploader.js | 250 ++++++++++++------ js/constants/error_constants.js | 2 +- js/utils/general_utils.js | 43 ++- sass/ascribe_uploader.scss | 154 ++++++++++- 9 files changed, 471 insertions(+), 100 deletions(-) create mode 100644 js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_error_dialog.js diff --git a/js/components/ascribe_detail/further_details_fileuploader.js b/js/components/ascribe_detail/further_details_fileuploader.js index b660c80e..f5593ab9 100644 --- a/js/components/ascribe_detail/further_details_fileuploader.js +++ b/js/components/ascribe_detail/further_details_fileuploader.js @@ -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} /> ); } diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index 9deed676..d711cd61 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -173,7 +173,8 @@ let RegisterPieceForm = React.createClass({ disabled={!isFineUploaderEditable} enableLocalHashing={hashLocally} uploadMethod={location.query.method} - handleChangedFile={this.handleChangedDigitalWork}/> + handleChangedFile={this.handleChangedDigitalWork} + showErrorPrompt /> + }} /> + ); + }, + 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 diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_error_dialog.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_error_dialog.js new file mode 100644 index 00000000..326f727f --- /dev/null +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_error_dialog.js @@ -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 ( + + ); + }, + + getContactUsDetail() { + return ( +
+

Let us help you

+

{getLangText('Still having problems? Give us a call!')}

+ {this.getRetryButton('Contact us', true)} +
+ ); + }, + + getErrorDetail(multipleFiles) { + const { errorClass: { prettifiedText }, files } = this.props; + + return ( +
+
+

{getLangText(multipleFiles ? 'Some files did not upload correctly' + : 'Error uploading the file!')} +

+

{prettifiedText}

+ {this.getRetryButton('Retry')} +
+ + + +
+
    + {files.map((file) => (
  • {file.originalName}
  • ))} +
+
+
+ ); + }, + + 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; diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index f75555a3..172940e6 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -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); - isFileValid(file) { - if(file.size > this.props.validation.sizeLimit) { + // 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 + }) + } + }); - 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, diff --git a/js/constants/error_constants.js b/js/constants/error_constants.js index 6386892c..19e06c18 100644 --- a/js/constants/error_constants.js +++ b/js/constants/error_constants.js @@ -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; }); }); diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index f9e8cdc9..b15a0525 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -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 }); } /** diff --git a/sass/ascribe_uploader.scss b/sass/ascribe_uploader.scss index 38f8400b..fa353ecd 100644 --- a/sass/ascribe_uploader.scss +++ b/sass/ascribe_uploader.scss @@ -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 { @@ -197,4 +349,4 @@ span + .btn { margin-left: 1em; } -} \ No newline at end of file +}