From 3e28d6f4210aa508b36044ee40983cc0b0fed4d1 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 23 Nov 2015 17:12:47 +0100 Subject: [PATCH 01/22] Add functionality to match known errors Implemented as part of AD-1360 and AD-1378 as a way to match an error situation to a known class of errors so that we can modify behaviour or show pretty text to the user. --- js/constants/error_constants.js | 205 ++++++++++++++++++++++++++++++++ js/utils/error_utils.js | 5 +- js/utils/general_utils.js | 41 +++++++ 3 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 js/constants/error_constants.js diff --git a/js/constants/error_constants.js b/js/constants/error_constants.js new file mode 100644 index 00000000..81e04f04 --- /dev/null +++ b/js/constants/error_constants.js @@ -0,0 +1,205 @@ +'use strict' + +import { deepMatchObject } from '../utils/general_utils'; +import { getLangText } from '../utils/lang_utils'; + +/** + * ErrorClasses + * ============ + * Known error classes based on groupings (ie. where they happened, which component, etc). + * + * Error classes have a test object that can be used to test whether or not an error + * object matches that specific class. Properties in the test object will be recursively + * checked in the error object, and the error object is only matched to the class if all + * tests succeed. See testErrorAgainstClass() below for the implementation of the matching. + * + * ErrorClasses.default.default is the generic error for errors not identified under any + * grouping and class. + * + * Format: + * ErrorClasses = { + * 'errorGrouping': { + * 'errorClassName': ErrorClass + * ... + * }, + * ... + * 'default': { + * ... + * 'default': generic error for errors that don't fall under any grouping and class + * } + * } + * + * Each class is of the format: + * ErrorClass = { + * 'name': name of the class + * 'group': grouping of the error, + * 'prettifiedText': prettified text for the class + * 'test': { + * prop1: property in the error object to recursively match against using + * either === or, if the property is a string, substring match + * (ie. indexOf() >= 0) + * ... + * }, + * } + * + * Test object examples + * ==================== + * A class like this: + * + * 'errorClass': { + * 'test': { + * 'reason': 'Invalid server response', + * 'xhr': { + * 'response': 'Internal error', + * 'status': 500 + * } + * } + * } + * + * will match this error object: + * + * error = { + * 'reason': 'Invalid server response', + * 'xhr': { // Simplified version of the XMLHttpRequest object responsible for the failure + * 'response': 'Internal error', + * 'status': 500 + * } + * } + * + * but will *NOT* match this error object: + * + * error = { + * 'reason': 'Invalid server response', + * 'xhr': { + * 'response': 'Unauthorized', + * 'status': 401 + * } + * } + * + * A common use case is for the test to just be against the error.reason string. + * In these cases, setting the test object to be just a string will enforce this test, + * so something like this: + * + * 'errorClass': { + * 'test': { + * 'reason': 'Invalid server response' + * } + * } + * + * is the same as: + * + * 'errorClass': { + * 'test': 'Invalid server response' + * } + */ +const ErrorClasses = { + 'upload': { + 'requestTimeTooSkewed': { + 'prettifiedText': getLangText('It appears that the time set on your computer is too ' + + 'inaccurate compared to your current local time. As a security ' + + 'measure, we check to make sure that our users are not falsifying ' + + "their registration times. Please synchronize your computer's " + + 'clock and try again.'), + 'test': { + 'xhr': { + 'response': 'RequestTimeTooSkewed' + } + } + }, + 'chunkSignatureError': { + 'prettifiedText': getLangText('We are experiencing some problems with uploads at the moment and ' + + 'are working to resolve them. Please try again in a few hours.'), + 'test': 'Problem signing the chunk' + }, + }, + 'default': { + 'default': { + 'prettifiedText': getLangText("It looks like there's been a problem on our end. If you keep experiencing this error, please contact us.") + } + } +}; + +// Dynamically inject the name and group properties into the classes +Object.keys(ErrorClasses).forEach((errorGroup) => { + Object.keys(ErrorClasses[errorGroup]).forEach((errorClass) => { + errorClass.name = errorClass; + errorClass.group = errorGroup; + }); +}); + +/** + * Returns prettified text for a given error by trying to match it to + * a known error in ErrorClasses or the given class. + * + * One should provide a class (eg. ErrorClasses.upload.requestTimeTooSkewed) + * if they already have an error in mind that they want to match against rather + * than all the available error classes. + * + * @param {object} error An error with the following: + * @param {string} error.type Type of error + * @param {string} error.reason Reason of error + * @param {(XMLHttpRequest)} error.xhr XHR associated with the error + * @param {(any)} error.* Any other property as necessary + * + * @param {(object)} errorClass ErrorClass to match against the given error. + * Signature should be similar to ErrorClasses' classes (see above). + * @param {object|string} errorClass.test Test object to recursively match against the given error + * @param {string} errorClass.prettifiedText Prettified text to return if the test matches + * + * @return {string} Prettified error string. Returns the default error string if no + * error class was matched to the given error. + */ +function getPrettifiedError(error, errorClass) { + const matchedClass = errorClass ? testErrorAgainstClass(error, errorClass) : testErrorAgainstAll(error); + return (matchedClass && matchedClass.prettifiedText) || ErrorClasses.default.default.prettifiedText; +} + +/** + * Tests the given error against all items in ErrorClasses and returns + * the matching class if available. + * See getPrettifiedError() for the signature of @param error. + * @return {(object)} Matched error class + */ +function testErrorAgainstAll(error) { + const type = error.type != null ? error.type : 'default'; + const errorGroup = ErrorClasses[type]; + + return Object + .keys(errorGroup) + .reduce((result, key) => { + return result || testErrorAgainstClass(error, errorGroup[key]); + }, null); +} + +/** + * Tests the error against the class by recursively testing the + * class's test object against the error. + * Implements the test matching behaviour described in ErrorClasses. + * + * See getPrettifiedError() for the signatures of @param error and @param errorClass. + * @return {(object)} Returns the given class if the test succeeds. + */ +function testErrorAgainstClass(error, errorClass) { + // Automatically fail classes if no tests present + if (!errorClass.test) { + return; + } + + if (typeof errorClass.test === 'string') { + errorClass.test = { + reason: errorClass.test + }; + } + + return deepMatchObject(error, errorClass.test, (objProp, matchProp) => { + return (objProp === matchProp || (typeof objProp === 'string' && objProp.indexOf(matchProp) >= 0)); + }) ? errorClass : null; +} + +// Need to export with the clause syntax as we change ErrorClasses after its declaration. +export { + ErrorClasses, + getPrettifiedError, + testErrorAgainstAll, + testErrorAgainstClass +}; diff --git a/js/utils/error_utils.js b/js/utils/error_utils.js index 4e9de6e2..e80819dc 100644 --- a/js/utils/error_utils.js +++ b/js/utils/error_utils.js @@ -4,7 +4,6 @@ import Raven from 'raven-js'; import AppConstants from '../constants/application_constants'; - /** * Logs an error in to the console but also sends it to * Sentry. @@ -14,7 +13,6 @@ import AppConstants from '../constants/application_constants'; * @param {string} comment Will also be submitted to Sentry, but will not be logged */ function logGlobal(error, ignoreSentry, comment) { - console.error(error); if(!ignoreSentry) { @@ -24,7 +22,6 @@ function logGlobal(error, ignoreSentry, comment) { Raven.captureException(error); } } - } export function initLogging() { @@ -36,4 +33,4 @@ export function initLogging() { window.onerror = Raven.process; console.logGlobal = logGlobal; -} \ No newline at end of file +} diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index e717fa75..d690929e 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -178,6 +178,47 @@ export function omitFromObject(obj, filter) { } } +/** + * Recursively tests an object against a "match" object to see if the + * object is similar to the "match" object. In other words, this will + * deeply traverse the "match" object's properties and check them + * against the object by using the testFn. + * + * The object is considered a match if all primitive properties in the + * "match" object are found and accepted in the object by the testFn. + * + * @param {object} obj Object to test + * @param {object} match "Match" object to test against + * @param {(function)} testFn Function to use on each property test. + * Return true to accept the match. + * By default, applies strict equality using === + * @return {boolean} True if obj matches the "match" object + */ +export function deepMatchObject(obj, match, testFn) { + if (typeof match !== 'object') { + throw new Error('Your specified match argument was not an object'); + } + + if (typeof testFn !== 'function') { + testFn = (objProp, matchProp) => { + return objProp === matchProp; + }; + } + + return Object + .keys(match) + .reduce((result, matchKey) => { + if (!result) { return false; } + + const objProp = obj[matchKey]; + const matchProp = match[matchKey]; + + return (typeof matchProp === 'object') ? testObjAgainstMatch(objProp, matchProp, testFn) + : testFn(objProp, matchProp); + + }, true); +} + /** * Takes a string and breaks it at the supplied index and replaces it * with a (potentially) short string that also has been provided From dff180ff8758ca90b9f9a8e21f489ea332c8121b Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 25 Nov 2015 16:27:32 +0100 Subject: [PATCH 02/22] Use FineUploader status constants as opposed to strings --- .../file_drag_and_drop.js | 13 +++++- .../file_drag_and_drop_preview.js | 7 +-- .../react_s3_fine_uploader.js | 44 ++++++++++++------- .../react_s3_fine_uploader_utils.js | 9 +++- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js index cfca4d56..1efe301c 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js @@ -6,6 +6,7 @@ import ProgressBar from 'react-bootstrap/lib/ProgressBar'; import FileDragAndDropDialog from './file_drag_and_drop_dialog'; import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator'; +import { FileStatus } from '../react_s3_fine_uploader_utils'; import { getLangText } from '../../../utils/lang_utils'; @@ -155,8 +156,16 @@ let FileDragAndDrop = React.createClass({ areAssetsEditable, allowedExtensions } = this.props; - // has files only is true if there are files that do not have the status deleted or canceled - let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0; + // has files only is true if there are files that do not have the status deleted, canceled, or failed + let hasFiles = filesToUpload + .filter((file) => { + return file.status !== FileStatus.DELETED && + file.status !== FileStatus.CANCELED && + file.status !== FileStatus.UPLOAD_FAILED && + file.size !== -1; + }) + .length > 0; + let updatedClassName = hasFiles ? 'has-files ' : ''; updatedClassName += dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone'; updatedClassName += ' file-drag-and-drop'; diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview.js index ca1be2d2..f5986786 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview.js @@ -5,6 +5,7 @@ import React from 'react'; import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image'; import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other'; +import { FileStatus } from '../react_s3_fine_uploader_utils'; import { getLangText } from '../../../utils/lang_utils'; import { truncateTextAtCharIndex } from '../../../utils/general_utils'; import { extractFileExtensionFromString } from '../../../utils/file_utils'; @@ -33,9 +34,9 @@ const FileDragAndDropPreview = React.createClass({ }, toggleUploadProcess() { - if(this.props.file.status === 'uploading') { + if(this.props.file.status === FileStatus.UPLOADING) { this.props.handlePauseFile(this.props.file.id); - } else if(this.props.file.status === 'paused') { + } else if(this.props.file.status === FileStatus.PAUSED) { this.props.handleResumeFile(this.props.file.id); } }, @@ -51,7 +52,7 @@ const FileDragAndDropPreview = React.createClass({ // deleted using an HTTP DELETE request. if (handleDeleteFile && file.progress === 100 && - (file.status === 'upload successful' || file.status === 'online') && + (file.status === FileStatus.UPLOAD_SUCCESSFUL || file.status === FileStatus.ONLINE) && file.s3UrlSafe) { handleDeleteFile(file.id); } else if(handleCancelFile) { diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index c5d2cb1c..562f9449 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -13,8 +13,8 @@ import GlobalNotificationActions from '../../actions/global_notification_actions import AppConstants from '../../constants/application_constants'; +import { displayValidFilesFilter, FileStatus, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils'; import { computeHashOfFile } from '../../utils/file_utils'; -import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils'; import { getCookie } from '../../utils/fetch_api_utils'; import { getLangText } from '../../utils/lang_utils'; @@ -449,7 +449,7 @@ const ReactS3FineUploader = React.createClass({ // Set the state of the completed file to 'upload successful' in order to // remove it from the GUI - files[id].status = 'upload successful'; + files[id].status = FileStatus.UPLOAD_SUCCESSFUL; files[id].key = this.state.uploader.getKey(id); let filesToUpload = React.addons.update(this.state.filesToUpload, { $set: files }); @@ -536,7 +536,7 @@ const ReactS3FineUploader = React.createClass({ onCancel(id) { // when a upload is canceled, we need to update this components file array - this.setStatusOfFile(id, 'canceled') + this.setStatusOfFile(id, FileStatus.CANCELED) .then(() => { if(typeof this.props.handleChangedFile === 'function') { this.props.handleChangedFile(this.state.filesToUpload[id]); @@ -576,7 +576,7 @@ const ReactS3FineUploader = React.createClass({ // fetch blobs for images response = response.map((file) => { file.url = file.s3UrlSafe; - file.status = 'online'; + file.status = FileStatus.ONLINE; file.progress = 100; return file; }); @@ -604,7 +604,7 @@ const ReactS3FineUploader = React.createClass({ onDeleteComplete(id, xhr, isError) { if(isError) { - this.setStatusOfFile(id, 'online'); + this.setStatusOfFile(id, FileStatus.ONLINE); let notification = new GlobalNotificationModel(getLangText('There was an error deleting your file.'), 'danger', 10000); GlobalNotificationActions.appendGlobalNotification(notification); @@ -633,9 +633,9 @@ const ReactS3FineUploader = React.createClass({ // We set the files state to 'deleted' immediately, so that the user is not confused with // the unresponsiveness of the UI // - // If there is an error during the deletion, we will just change the status back to 'online' + // If there is an error during the deletion, we will just change the status back to FileStatus.ONLINE // and display an error message - this.setStatusOfFile(fileId, 'deleted') + this.setStatusOfFile(fileId, FileStatus.DELETED) .then(() => { if(typeof this.props.handleChangedFile === 'function') { this.props.handleChangedFile(this.state.filesToUpload[fileId]); @@ -651,7 +651,7 @@ const ReactS3FineUploader = React.createClass({ // To check which files are already uploaded from previous sessions we check their status. // If they are, it is "online" - if(this.state.filesToUpload[fileId].status !== 'online') { + if(this.state.filesToUpload[fileId].status !== FileStatus.ONLINE) { // delete file from server this.state.uploader.deleteFile(fileId); // this is being continued in onDeleteFile, as @@ -672,7 +672,7 @@ const ReactS3FineUploader = React.createClass({ handlePauseFile(fileId) { if(this.state.uploader.pauseUpload(fileId)) { - this.setStatusOfFile(fileId, 'paused'); + this.setStatusOfFile(fileId, FileStatus.PAUSED); } else { throw new Error(getLangText('File upload could not be paused.')); } @@ -680,7 +680,7 @@ const ReactS3FineUploader = React.createClass({ handleResumeFile(fileId) { if(this.state.uploader.continueUpload(fileId)) { - this.setStatusOfFile(fileId, 'uploading'); + this.setStatusOfFile(fileId, FileStatus.UPLOADING); } else { throw new Error(getLangText('File upload could not be resumed.')); } @@ -860,12 +860,12 @@ const ReactS3FineUploader = React.createClass({ // // If the user deletes one of those files, then fineuploader will still keep it in his // files array but with key, progress undefined and size === -1 but - // status === 'upload successful'. + // status === FileStatus.UPLOAD_SUCCESSFUL. // This poses a problem as we depend on the amount of files that have - // status === 'upload successful', therefore once the file is synced, - // we need to tag its status as 'deleted' (which basically happens here) + // status === FileStatus.UPLOAD_SUCCESSFUL, therefore once the file is synced, + // we need to tag its status as FileStatus.DELETED (which basically happens here) if(oldAndNewFiles[i].size === -1 && (!oldAndNewFiles[i].progress || oldAndNewFiles[i].progress === 0)) { - oldAndNewFiles[i].status = 'deleted'; + oldAndNewFiles[i].status = FileStatus.DELETED; } if(oldAndNewFiles[i].originalName === oldFiles[j].name) { @@ -901,7 +901,7 @@ const ReactS3FineUploader = React.createClass({ return Q.Promise((resolve) => { let changeSet = {}; - if(status === 'deleted' || status === 'canceled') { + if (status === FileStatus.DELETED || status === FileStatus.CANCELED || status === FileStatus.UPLOAD_FAILED) { changeSet.progress = { $set: 0 }; } @@ -914,9 +914,19 @@ const ReactS3FineUploader = React.createClass({ }, isDropzoneInactive() { - const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1); + const { areAssetsEditable, enableLocalHashing, multiple, showErrorStates, uploadMethod } = this.props; + const { errorState, filesToUpload } = this.state; - if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) { + const filesToDisplay = filesToUpload.filter((file) => { + return file.status !== FileStatus.DELETED && + file.status !== FileStatus.CANCELED && + file.status !== FileStatus.UPLOAD_FAILED && + file.size !== -1; + }); + + if ((enableLocalHashing && !uploadMethod) || !areAssetsEditable || + (showErrorStates && errorState.errorClass) || + (!multiple && filesToDisplay.length > 0)) { return true; } else { return false; diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js b/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js index ed76c5e8..2e0a046b 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js @@ -1,11 +1,18 @@ 'use strict'; +import fineUploader from 'fineUploader'; +// Re-export qq.status from FineUploader with an additional online +// state that we use to keep track of files from S3. +export const FileStatus = Object.assign(fineUploader.status, { + ONLINE: 'online' +}); + export const formSubmissionValidation = { /** * Returns a boolean if there has been at least one file uploaded * successfully without it being deleted or canceled. * @param {array of files} files provided by react fine uploader - * @return {boolean} + * @return {boolean} */ atLeastOneUploadedFile(files) { files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled'); From 01e3fd5fcdb96d3b7c1f19a0048664f01c309837 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 25 Nov 2015 16:30:17 +0100 Subject: [PATCH 03/22] Add error queue store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allows errors to be queued for showing to the user, such as in the uploader’s error states. --- js/actions/error_queue_actions.js | 13 ++++++++ js/constants/error_constants.js | 21 ++++++++++--- js/stores/error_queue_store.js | 52 +++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 js/actions/error_queue_actions.js create mode 100644 js/stores/error_queue_store.js diff --git a/js/actions/error_queue_actions.js b/js/actions/error_queue_actions.js new file mode 100644 index 00000000..92f77247 --- /dev/null +++ b/js/actions/error_queue_actions.js @@ -0,0 +1,13 @@ +'use strict'; + +import { alt } from '../alt'; + +class ErrorQueueActions { + constructor() { + this.generateActions( + 'shiftErrorQueue' + ); + } +} + +export default alt.createActions(ErrorQueueActions); diff --git a/js/constants/error_constants.js b/js/constants/error_constants.js index 81e04f04..6386892c 100644 --- a/js/constants/error_constants.js +++ b/js/constants/error_constants.js @@ -111,6 +111,17 @@ const ErrorClasses = { 'are working to resolve them. Please try again in a few hours.'), 'test': 'Problem signing the chunk' }, + + // Fallback error tips + 'slowConnection': { + 'prettifiedText': getLangText('Are you on a slow or unstable network? Uploading large files requires a fast Internet connection.') + }, + 'tryDifferentBrowser': { + 'prettifiedText': getLangText("We still can't seem to upload your file. Maybe try another browser?") + }, + 'contactUs': { + 'prettifiedText': getLangText("We're having a really hard time with your upload. Please contact us for more help.") + } }, 'default': { 'default': { @@ -120,10 +131,12 @@ const ErrorClasses = { }; // Dynamically inject the name and group properties into the classes -Object.keys(ErrorClasses).forEach((errorGroup) => { - Object.keys(ErrorClasses[errorGroup]).forEach((errorClass) => { - errorClass.name = errorClass; - errorClass.group = errorGroup; +Object.keys(ErrorClasses).forEach((errorGroupKey) => { + const errorGroup = ErrorClasses[errorGroupKey]; + Object.keys(errorGroup).forEach((errorClassKey) => { + const errorClass = errorGroup[errorClassKey]; + errorClass.name = errorClassKey; + errorClass.group = errorGroupKey; }); }); diff --git a/js/stores/error_queue_store.js b/js/stores/error_queue_store.js new file mode 100644 index 00000000..8521ff61 --- /dev/null +++ b/js/stores/error_queue_store.js @@ -0,0 +1,52 @@ +'use strict'; + +import { alt } from '../alt'; + +import ErrorQueueActions from '../actions/error_queue_actions.js'; + +import { ErrorClasses } from '../constants/error_constants.js'; + +class ErrorQueueStore { + constructor() { + const { upload: { slowConnection, tryDifferentBrowser } } = ErrorClasses; + + this.errorQueue = { + 'upload': { + queue: [slowConnection, tryDifferentBrowser], + loop: true + } + }; + + // Add intial index to each error queue + Object + .keys(this.errorQueue) + .forEach((type) => { + this.errorQueue[type].index = 0; + }); + + this.exportPublicMethods({ + getNextError: this.getNextError + }); + } + + getNextError(type) { + const errorQueue = this.errorQueues[type]; + const { queue, index } = errorQueue; + + ErrorQueueActions.shiftErrorQueue(type); + return queue[index]; + } + + onShiftQueue(type) { + const errorQueue = this.errorQueues[type]; + const { queue, loop } = errorQueue; + + ++errorQueue.index; + if (loop) { + // Loop back to the beginning if all errors have been exhausted + errorQueue.index %= queue.length; + } + } +} + +export default alt.createStore(ErrorQueueStore, 'ErrorQueueStore'); From 4821e36189530546edb77ce3e6250f5037bdb848 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 25 Nov 2015 18:58:49 +0100 Subject: [PATCH 04/22] Update FineUploader utils to use FileStatus --- .../ascribe_forms/form_register_piece.js | 4 +-- .../react_s3_fine_uploader_utils.js | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index 232ef444..9deed676 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -17,9 +17,7 @@ import AscribeSpinner from '../ascribe_spinner'; import { getLangText } from '../../utils/lang_utils'; import { mergeOptions } from '../../utils/general_utils'; -import { formSubmissionValidation, - displayValidFilesFilter, - displayRemovedFilesFilter } from '../ascribe_uploader/react_s3_fine_uploader_utils'; +import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; let RegisterPieceForm = React.createClass({ diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js b/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js index 2e0a046b..05fb565e 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js @@ -15,8 +15,13 @@ export const formSubmissionValidation = { * @return {boolean} */ atLeastOneUploadedFile(files) { - files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled'); - if (files.length > 0 && files[0].status === 'upload successful') { + files = files.filter((file) => { + return file.status !== FileStatus.DELETED && + file.status !== FileStatus.CANCELED && + file.status != FileStatus.UPLOADED_FAILED + }); + + if (files.length > 0 && files[0].status === FileStatus.UPLOAD_SUCCESSFUL) { return true; } else { return false; @@ -30,7 +35,7 @@ export const formSubmissionValidation = { * @return {boolean} [description] */ fileOptional(files) { - let uploadingFiles = files.filter((file) => file.status === 'submitting'); + let uploadingFiles = files.filter((file) => file.status === FileStatus.SUBMITTING); if (uploadingFiles.length === 0) { return true; @@ -41,21 +46,25 @@ export const formSubmissionValidation = { }; /** - * Filter function for filtering all deleted and canceled files + * Filter function for filtering all deleted, canceled, and failed files * @param {object} file A file from filesToUpload that has status as a prop. * @return {boolean} */ export function displayValidFilesFilter(file) { - return file.status !== 'deleted' && file.status !== 'canceled'; + return file.status !== FileStatus.DELETED && + file.status !== FileStatus.CANCELED && + file.status !== FileStatus.UPLOAD_FAILED; } /** - * Filter function for filtering all files except for deleted and canceled files + * Filter function for filtering all files except for deleted, canceled, and failed files * @param {object} file A file from filesToUpload that has status as a prop. * @return {boolean} */ export function displayRemovedFilesFilter(file) { - return file.status === 'deleted' || file.status === 'canceled'; + return file.status === FileStatus.DELETED || + file.status === FileStatus.CANCELED || + file.status === FileStatus.UPLOAD_FAILED; } @@ -65,7 +74,10 @@ export function displayRemovedFilesFilter(file) { * @return {boolean} */ export function displayValidProgressFilesFilter(file) { - return file.status !== 'deleted' && file.status !== 'canceled' && file.status !== 'online'; + return file.status !== FileStatus.DELETED && + file.status !== FileStatus.CANCELED && + file.status !== FileStatus.UPLOAD_FAILED && + file.status !== FileStatus.ONLINE; } From 0d6b3710f78a4272e117714a7d5dfec2764dd45d Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 25 Nov 2015 18:58:56 +0100 Subject: [PATCH 05/22] Fix bugs in ErrorQueueStore and deepMatchObject() --- js/stores/error_queue_store.js | 9 ++++++--- js/utils/general_utils.js | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/js/stores/error_queue_store.js b/js/stores/error_queue_store.js index 8521ff61..d08ed4c3 100644 --- a/js/stores/error_queue_store.js +++ b/js/stores/error_queue_store.js @@ -10,7 +10,7 @@ class ErrorQueueStore { constructor() { const { upload: { slowConnection, tryDifferentBrowser } } = ErrorClasses; - this.errorQueue = { + this.errorQueues = { 'upload': { queue: [slowConnection, tryDifferentBrowser], loop: true @@ -19,11 +19,14 @@ class ErrorQueueStore { // Add intial index to each error queue Object - .keys(this.errorQueue) + .keys(this.errorQueues) .forEach((type) => { - this.errorQueue[type].index = 0; + this.errorQueues[type].index = 0; }); + // Bind the exported functions to this instance + this.getNextError = this.getNextError.bind(this); + this.exportPublicMethods({ getNextError: this.getNextError }); diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index d690929e..f9e8cdc9 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -213,9 +213,12 @@ export function deepMatchObject(obj, match, testFn) { const objProp = obj[matchKey]; const matchProp = match[matchKey]; - return (typeof matchProp === 'object') ? testObjAgainstMatch(objProp, matchProp, testFn) - : testFn(objProp, matchProp); - + if (typeof matchProp === 'object') { + return (typeof objProp === 'object') ? deepMatchObject(objProp, matchProp, testFn) + : false; + } else { + return testFn(objProp, matchProp); + } }, true); } From 9b54a75e279b7034cf76b4f438057c9549cace49 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Sun, 29 Nov 2015 16:57:29 +0100 Subject: [PATCH 06/22] Bind ErrorQueueStore to ErrorQueueActions --- js/stores/error_queue_store.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/stores/error_queue_store.js b/js/stores/error_queue_store.js index d08ed4c3..802b767d 100644 --- a/js/stores/error_queue_store.js +++ b/js/stores/error_queue_store.js @@ -30,6 +30,7 @@ class ErrorQueueStore { this.exportPublicMethods({ getNextError: this.getNextError }); + this.bindActions(ErrorQueueActions); } getNextError(type) { @@ -40,7 +41,7 @@ class ErrorQueueStore { return queue[index]; } - onShiftQueue(type) { + onShiftErrorQueue(type) { const errorQueue = this.errorQueues[type]; const { queue, loop } = errorQueue; From a0db6d3037547750914bd518d5301baad6fb80a8 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 8 Dec 2015 18:09:24 +0100 Subject: [PATCH 07/22] Shuffle uploader props to be more readable --- .../further_details_fileuploader.js | 43 ++++--- .../ascribe_forms/input_fineuploader.js | 79 +++++++------ .../file_drag_and_drop.js | 11 +- .../react_s3_fine_uploader.js | 106 ++++++++++-------- 4 files changed, 140 insertions(+), 99 deletions(-) diff --git a/js/components/ascribe_detail/further_details_fileuploader.js b/js/components/ascribe_detail/further_details_fileuploader.js index 5c9a70d0..b660c80e 100644 --- a/js/components/ascribe_detail/further_details_fileuploader.js +++ b/js/components/ascribe_detail/further_details_fileuploader.js @@ -11,15 +11,21 @@ import AppConstants from '../../constants/application_constants'; import { getCookie } from '../../utils/fetch_api_utils'; + +const { func, bool, number, object, arrayOf } = React.PropTypes; + let FurtherDetailsFileuploader = React.createClass({ propTypes: { - pieceId: React.PropTypes.number, - otherData: React.PropTypes.arrayOf(React.PropTypes.object), - setIsUploadReady: React.PropTypes.func, - submitFile: React.PropTypes.func, - isReadyForFormSubmission: React.PropTypes.func, - editable: React.PropTypes.bool, - multiple: React.PropTypes.bool + pieceId: number, + otherData: arrayOf(object), + editable: bool, + + // Props for ReactS3FineUploader + multiple: bool, + submitFile: func, // TODO: rename to onSubmitFile + + setIsUploadReady: func, //TODO: rename to setIsUploaderValidated + isReadyForFormSubmission: func }, getDefaultProps() { @@ -29,16 +35,25 @@ let FurtherDetailsFileuploader = React.createClass({ }, render() { + const { + editable, + isReadyForFormSubmission, + multiple, + otherData, + pieceId, + setIsUploadReady, + submitFile } = this.props; + // Essentially there a three cases important to the fileuploader // // 1. there is no other_data => do not show the fileuploader at all (where otherData is now an array) // 2. there is other_data, but user has no edit rights => show fileuploader but without action buttons // 3. both other_data and editable are defined or true => show fileuploader with all action buttons - if (!this.props.editable && (!this.props.otherData || this.props.otherData.length === 0)) { + if (!editable && (!otherData || otherData.length === 0)) { return null; } - let otherDataIds = this.props.otherData ? this.props.otherData.map((data) => data.id).join() : null; + let otherDataIds = otherData ? otherData.map((data) => data.id).join() : null; return ( + handleChangedFile={handleChangedFile} /> ); } }); diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js index 1efe301c..cf3ac775 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js @@ -13,7 +13,12 @@ import { getLangText } from '../../../utils/lang_utils'; // Taken from: https://github.com/fedosejev/react-file-drag-and-drop let FileDragAndDrop = React.createClass({ propTypes: { - className: React.PropTypes.string, + areAssetsDownloadable: React.PropTypes.bool, + areAssetsEditable: React.PropTypes.bool, + multiple: React.PropTypes.bool, + dropzoneInactive: React.PropTypes.bool, + filesToUpload: React.PropTypes.array, + onDrop: React.PropTypes.func.isRequired, onDragOver: React.PropTypes.func, onInactive: React.PropTypes.func, @@ -22,10 +27,6 @@ let FileDragAndDrop = React.createClass({ handleCancelFile: React.PropTypes.func, handlePauseFile: React.PropTypes.func, handleResumeFile: React.PropTypes.func, - multiple: React.PropTypes.bool, - dropzoneInactive: React.PropTypes.bool, - areAssetsDownloadable: React.PropTypes.bool, - areAssetsEditable: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool, uploadMethod: React.PropTypes.string, diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index 562f9449..f75555a3 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -33,6 +33,47 @@ const { shape, const ReactS3FineUploader = React.createClass({ propTypes: { + areAssetsDownloadable: bool, + areAssetsEditable: 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 + onInactive: func, // for when the user does something while the uploader's inactive + + // Handle form validation + setIsUploadReady: func, //TODO: rename to setIsUploaderValidated + isReadyForFormSubmission: func, + + // We encountered some cases where people had difficulties to upload their + // works to ascribe due to a slow internet connection. + // One solution we found in the process of tackling this problem was to hash + // the file in the browser using md5 and then uploading the resulting text document instead + // of the actual file. + // + // This boolean and string essentially enable that behavior. + // Right now, we determine which upload method to use by appending a query parameter, + // which should be passed into 'uploadMethod': + // 'hash': upload using the hash + // 'upload': upload full file (default if not specified) + enableLocalHashing: bool, + uploadMethod: oneOf(['hash', 'upload']), + + // A class of a file the user has to upload + // Needs to be defined both in singular as well as in plural + fileClassToUpload: shape({ + singular: string, + plural: string + }), + + // Uploading functionality of react fineuploader is disconnected from its UI + // layer, which means that literally every (properly adjusted) react element + // can handle the UI handling. + fileInputElement: oneOfType([ + func, + element + ]), + + // S3 helpers keyRoutine: shape({ url: string, fileClass: string, @@ -48,10 +89,11 @@ const ReactS3FineUploader = React.createClass({ number ]) }), - handleChangedFile: func, // is for when a file is dropped or selected - submitFile: func, // is for when a file has been successfully uploaded, TODO: rename to handleSubmitFile + + // FineUploader options autoUpload: bool, debug: bool, + multiple: bool, objectProperties: shape({ acl: string }), @@ -103,45 +145,9 @@ const ReactS3FineUploader = React.createClass({ unsupportedBrowser: string }), formatFileName: func, - multiple: bool, retry: shape({ enableAuto: bool - }), - setIsUploadReady: func, - isReadyForFormSubmission: func, - areAssetsDownloadable: bool, - areAssetsEditable: bool, - defaultErrorMessage: string, - onInactive: func, - - // We encountered some cases where people had difficulties to upload their - // works to ascribe due to a slow internet connection. - // One solution we found in the process of tackling this problem was to hash - // the file in the browser using md5 and then uploading the resulting text document instead - // of the actual file. - // - // This boolean and string essentially enable that behavior. - // Right now, we determine which upload method to use by appending a query parameter, - // which should be passed into 'uploadMethod': - // 'hash': upload using the hash - // 'upload': upload full file (default if not specified) - enableLocalHashing: bool, - uploadMethod: oneOf(['hash', 'upload']), - - // A class of a file the user has to upload - // Needs to be defined both in singular as well as in plural - fileClassToUpload: shape({ - singular: string, - plural: string - }), - - // Uploading functionality of react fineuploader is disconnected from its UI - // layer, which means that literally every (properly adjusted) react element - // can handle the UI handling. - fileInputElement: oneOfType([ - func, - element - ]) + }) }, getDefaultProps() { @@ -945,14 +951,18 @@ const ReactS3FineUploader = React.createClass({ render() { const { - multiple, - areAssetsDownloadable, - areAssetsEditable, - onInactive, - enableLocalHashing, - fileClassToUpload, - fileInputElement: FileInputElement, - uploadMethod } = this.props; + multiple, + areAssetsDownloadable, + areAssetsEditable, + onInactive, + enableLocalHashing, + fileClassToUpload, + fileInputElement: FileInputElement, + showErrorPrompt, + uploadMethod } = this.props; + + // Only show the error state once all files are finished + const showError = !uploadInProgress && showErrorPrompt && errorClass; const props = { multiple, @@ -962,8 +972,8 @@ const ReactS3FineUploader = React.createClass({ enableLocalHashing, uploadMethod, fileClassToUpload, + filesToUpload, onDrop: this.handleUploadFile, - filesToUpload: this.state.filesToUpload, handleDeleteFile: this.handleDeleteFile, handleCancelFile: this.handleCancelFile, handlePauseFile: this.handlePauseFile, From 04d7e6951a8feea5c294b846852521e265736870 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 8 Dec 2015 18:16:11 +0100 Subject: [PATCH 08/22] Don't render fileDragAndDropDialog if it's not needed --- .../file_drag_and_drop.js | 96 +++++++------- .../file_drag_and_drop_dialog.js | 120 +++++++++--------- 2 files changed, 109 insertions(+), 107 deletions(-) diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js index cf3ac775..983c0655 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js @@ -1,9 +1,11 @@ 'use strict'; import React from 'react'; +import classNames from 'classnames'; import ProgressBar from 'react-bootstrap/lib/ProgressBar'; import FileDragAndDropDialog from './file_drag_and_drop_dialog'; +import FileDragAndDropErrorDialog from './file_drag_and_drop_error_dialog'; import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator'; import { FileStatus } from '../react_s3_fine_uploader_utils'; @@ -22,11 +24,11 @@ let FileDragAndDrop = React.createClass({ onDrop: React.PropTypes.func.isRequired, onDragOver: React.PropTypes.func, onInactive: React.PropTypes.func, - filesToUpload: React.PropTypes.array, handleDeleteFile: React.PropTypes.func, handleCancelFile: React.PropTypes.func, handlePauseFile: React.PropTypes.func, handleResumeFile: React.PropTypes.func, + handleRetryFiles: React.PropTypes.func, enableLocalHashing: React.PropTypes.bool, uploadMethod: React.PropTypes.string, @@ -142,73 +144,79 @@ let FileDragAndDrop = React.createClass({ this.refs.fileSelector.getDOMNode().dispatchEvent(evt); }, + getPreviewIterator() { + const { areAssetsDownloadable, areAssetsEditable, filesToUpload } = this.props; + + return ( + + ); + }, + + getUploadDialog() { + const { enableLocalHashing, fileClassToUpload, multiple, uploadMethod } = this.props; + + return ( + + ); + }, + render: function () { const { filesToUpload, dropzoneInactive, - className, hashingProgress, handleCancelHashing, multiple, - enableLocalHashing, - uploadMethod, fileClassToUpload, - areAssetsDownloadable, - areAssetsEditable, allowedExtensions } = this.props; // has files only is true if there are files that do not have the status deleted, canceled, or failed - let hasFiles = filesToUpload - .filter((file) => { - return file.status !== FileStatus.DELETED && - file.status !== FileStatus.CANCELED && - file.status !== FileStatus.UPLOAD_FAILED && - file.size !== -1; - }) - .length > 0; + const hasFiles = filesToUpload + .filter((file) => { + return file.status !== FileStatus.DELETED && + file.status !== FileStatus.CANCELED && + file.status !== FileStatus.UPLOAD_FAILED && + file.size !== -1; + }) + .length > 0; - let updatedClassName = hasFiles ? 'has-files ' : ''; - updatedClassName += dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone'; - updatedClassName += ' file-drag-and-drop'; + const failedFiles = filesToUpload.filter((file) => file.status === FileStatus.UPLOAD_FAILED); + let hasError = showError && errorClass && failedFiles.length > 0; // if !== -2: triggers a FileDragAndDrop-global spinner if(hashingProgress !== -2) { return ( -
-
-

{getLangText('Computing hash(es)... This may take a few minutes.')}

-

- {getLangText('Cancel hashing')} -

- -
+
+

{getLangText('Computing hash(es)... This may take a few minutes.')}

+

+ {getLangText('Cancel hashing')} +

+
); } else { return (
- - + {!hasFiles && !hasError ? this.getUploadDialog() : null} {/* Opera doesn't trigger simulated click events if the targeted input has `display:none` set. diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js index 25552819..738fabcf 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js @@ -9,7 +9,6 @@ import { getCurrentQueryParams } from '../../../utils/url_utils'; let FileDragAndDropDialog = React.createClass({ propTypes: { - hasFiles: React.PropTypes.bool, multipleFiles: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool, uploadMethod: React.PropTypes.string, @@ -36,80 +35,75 @@ let FileDragAndDropDialog = React.createClass({ render() { const { - hasFiles, multipleFiles, enableLocalHashing, uploadMethod, fileClassToUpload, onClick } = this.props; - if (hasFiles) { - return null; + if (enableLocalHashing && !uploadMethod) { + const currentQueryParams = getCurrentQueryParams(); + + const queryParamsHash = Object.assign({}, currentQueryParams); + queryParamsHash.method = 'hash'; + + const queryParamsUpload = Object.assign({}, currentQueryParams); + queryParamsUpload.method = 'upload'; + + return ( +
+

{getLangText('Would you rather')}

+ {/* + The frontend in live is hosted under /app, + Since `Link` is appending that base url, if its defined + by itself, we need to make sure to not set it at this point. + Otherwise it will be appended twice. + */} + + + {getLangText('Hash your work')} + + + + or + + + + {getLangText('Upload and hash your work')} + + +
+ ); } else { - if (enableLocalHashing && !uploadMethod) { - const currentQueryParams = getCurrentQueryParams(); - - const queryParamsHash = Object.assign({}, currentQueryParams); - queryParamsHash.method = 'hash'; - - const queryParamsUpload = Object.assign({}, currentQueryParams); - queryParamsUpload.method = 'upload'; - + if (multipleFiles) { return ( -
-

{getLangText('Would you rather')}

- {/* - The frontend in live is hosted under /app, - Since `Link` is appending that base url, if its defined - by itself, we need to make sure to not set it at this point. - Otherwise it will be appended twice. - */} - - - {getLangText('Hash your work')} - - - - or - - - - {getLangText('Upload and hash your work')} - - -
+ + {this.getDragDialog(fileClassToUpload.plural)} + + {getLangText('choose %s to upload', fileClassToUpload.plural)} + + ); } else { - if (multipleFiles) { - return ( - - {this.getDragDialog(fileClassToUpload.plural)} - - {getLangText('choose %s to upload', fileClassToUpload.plural)} - - - ); - } else { - const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular) - : getLangText('choose a %s to upload', fileClassToUpload.singular); + const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular) + : getLangText('choose a %s to upload', fileClassToUpload.singular); - return ( - - {this.getDragDialog(fileClassToUpload.singular)} - - {dialog} - + return ( + + {this.getDragDialog(fileClassToUpload.singular)} + + {dialog} - ); - } + + ); } } } From 79780cfb3ad819afa385a0062aa19a3953509002 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 8 Dec 2015 18:22:11 +0100 Subject: [PATCH 09/22] 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 +} From 5bc447ecc8898e8c15613beaeefbc15818883f5c Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 8 Dec 2015 18:23:02 +0100 Subject: [PATCH 10/22] Allow warnings to be set from inside properties to change appearance --- .../ascribe_forms/input_fineuploader.js | 3 +++ js/components/ascribe_forms/property.js | 21 ++++++++++++------- .../react_s3_fine_uploader.js | 19 +++++++++++++++++ sass/ascribe_property.scss | 7 +++++-- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/js/components/ascribe_forms/input_fineuploader.js b/js/components/ascribe_forms/input_fineuploader.js index a6028cf9..b25056a3 100644 --- a/js/components/ascribe_forms/input_fineuploader.js +++ b/js/components/ascribe_forms/input_fineuploader.js @@ -25,6 +25,7 @@ const InputFineUploader = React.createClass({ // Props for ReactS3FineUploader areAssetsDownloadable: bool, + setWarning: func, showErrorPrompt: bool, handleChangedFile: func, // TODO: rename to onChangedFile @@ -122,6 +123,7 @@ const InputFineUploader = React.createClass({ fileClassToUpload, uploadMethod, handleChangedFile, + setWarning, showErrorPrompt, disabled } = this.props; let editable = isFineUploaderActive; @@ -144,6 +146,7 @@ const InputFineUploader = React.createClass({ isReadyForFormSubmission={isReadyForFormSubmission} areAssetsDownloadable={areAssetsDownloadable} areAssetsEditable={editable} + setWarning={setWarning} showErrorPrompt={showErrorPrompt} signature={{ endpoint: AppConstants.serverUrl + 's3/signature/', diff --git a/js/components/ascribe_forms/property.js b/js/components/ascribe_forms/property.js index 063d27dd..7f2a34e2 100644 --- a/js/components/ascribe_forms/property.js +++ b/js/components/ascribe_forms/property.js @@ -71,7 +71,8 @@ const Property = React.createClass({ initialValue: null, value: null, isFocused: false, - errors: null + errors: null, + hasWarning: false }; }, @@ -209,17 +210,20 @@ const Property = React.createClass({ this.setState({errors: null}); }, + setWarning(hasWarning) { + this.setState({ hasWarning }); + }, + getClassName() { - if(!this.state.expanded && !this.props.checkboxLabel){ + if (!this.state.expanded && !this.props.checkboxLabel) { return 'is-hidden'; - } - if(!this.props.editable){ + } else if (!this.props.editable) { return 'is-fixed'; - } - if (this.state.errors){ + } else if (this.state.errors) { return 'is-error'; - } - if(this.state.isFocused) { + } else if (this.state.hasWarning) { + return 'is-warning'; + } else if (this.state.isFocused) { return 'is-focused'; } else { return ''; @@ -245,6 +249,7 @@ const Property = React.createClass({ onChange: this.handleChange, onFocus: this.handleFocus, onBlur: this.handleBlur, + setWarning: this.setWarning, disabled: !this.props.editable, ref: 'input', name: this.props.name diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index 172940e6..b022fe27 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -8,10 +8,13 @@ import S3Fetcher from '../../fetchers/s3_fetcher'; import FileDragAndDrop from './ascribe_file_drag_and_drop/file_drag_and_drop'; +import ErrorQueueStore from '../../stores/error_queue_store'; + import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; import AppConstants from '../../constants/application_constants'; +import { ErrorClasses, testErrorAgainstAll } from '../../constants/error_constants'; import { displayValidFilesFilter, FileStatus, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils'; import { computeHashOfFile } from '../../utils/file_utils'; @@ -31,12 +34,16 @@ const { shape, element, arrayOf } = React.PropTypes; +// After 5 manual retries, show the contact us prompt. +const RETRY_ATTEMPT_TO_SHOW_CONTACT_US = 5; + const ReactS3FineUploader = React.createClass({ propTypes: { areAssetsDownloadable: bool, areAssetsEditable: bool, errorNotificationMessage: string, showErrorPrompt: bool, + setWarning: func, // for when the parent component wants to be notified of uploader warnings (ie. upload failed) 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 @@ -301,6 +308,9 @@ const ReactS3FineUploader = React.createClass({ // proclaim that upload is not ready this.props.setIsUploadReady(false); + // reset any warnings propagated to parent + this.setWarning(false); + // reset internal data structures of component this.setState(this.getInitialState()); }, @@ -423,6 +433,12 @@ const ReactS3FineUploader = React.createClass({ } }, + setWarning(hasWarning) { + if (typeof this.props.setWarning === 'function') { + this.props.setWarning(hasWarning); + } + }, + checkFormSubmissionReady() { const { isReadyForFormSubmission, setIsUploadReady } = this.props; @@ -597,6 +613,7 @@ const ReactS3FineUploader = React.createClass({ }); this.setState({ errorState }); + this.setWarning(true); } } else { notificationMessage = errorReason || errorNotificationMessage; @@ -766,6 +783,8 @@ const ReactS3FineUploader = React.createClass({ }, filesToUpload }); + + this.setWarning(false); }, handleUploadFile(files) { diff --git a/sass/ascribe_property.scss b/sass/ascribe_property.scss index d214c57e..e3a4c914 100644 --- a/sass/ascribe_property.scss +++ b/sass/ascribe_property.scss @@ -37,7 +37,6 @@ $ascribe-red-error: rgb(169, 68, 66); margin-right: 1em; } } - > input, > textarea { @@ -50,6 +49,10 @@ $ascribe-red-error: rgb(169, 68, 66); } } +.is-warning { + border-left: 3px solid $ascribe-pink +} + .is-fixed { cursor: default; @@ -233,4 +236,4 @@ $ascribe-red-error: rgb(169, 68, 66); > span > span { margin-top: 0; } -} \ No newline at end of file +} From 5250427ce06cef258bef8bf99b0699e087b28987 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 8 Dec 2015 18:29:26 +0100 Subject: [PATCH 11/22] Use FileStatus in UploadButton, register form --- js/components/ascribe_forms/form_register_piece.js | 7 +++++-- js/components/ascribe_forms/input_fineuploader.js | 2 +- .../ascribe_upload_button/upload_button.js | 8 ++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index d711cd61..8e8b015c 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -11,9 +11,12 @@ import InputFineUploader from './input_fineuploader'; import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button'; import FormSubmitButton from '../ascribe_buttons/form_submit_button'; +import { FileStatus } from '../ascribe_uploader/react_s3_fine_uploader_utils'; + +import AscribeSpinner from '../ascribe_spinner'; + import ApiUrls from '../../constants/api_urls'; import AppConstants from '../../constants/application_constants'; -import AscribeSpinner from '../ascribe_spinner'; import { getLangText } from '../../utils/lang_utils'; import { mergeOptions } from '../../utils/general_utils'; @@ -84,7 +87,7 @@ let RegisterPieceForm = React.createClass({ handleChangedDigitalWork(digitalWorkFile) { if (digitalWorkFile && - (digitalWorkFile.status === 'deleted' || digitalWorkFile.status === 'canceled')) { + (digitalWorkFile.status === FileStatus.DELETED || digitalWorkFile.status === FileStatus.CANCELED)) { this.refs.form.refs.thumbnail_file.reset(); this.setState({ digitalWorkFile: null }); } else { diff --git a/js/components/ascribe_forms/input_fineuploader.js b/js/components/ascribe_forms/input_fineuploader.js index b25056a3..6ee44113 100644 --- a/js/components/ascribe_forms/input_fineuploader.js +++ b/js/components/ascribe_forms/input_fineuploader.js @@ -10,7 +10,7 @@ import AppConstants from '../../constants/application_constants'; import { getCookie } from '../../utils/fetch_api_utils'; -const { func, bool, shape, string, number, element, oneOf, arrayOf } = React.PropTypes; +const { func, bool, shape, string, number, element, oneOf, oneOfType, arrayOf } = React.PropTypes; const InputFineUploader = React.createClass({ propTypes: { diff --git a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js index 83aef402..aabf19d9 100644 --- a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js +++ b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js @@ -2,7 +2,7 @@ import React from 'react'; -import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils'; +import { displayValidProgressFilesFilter, FileStatus } from '../react_s3_fine_uploader_utils'; import { getLangText } from '../../../utils/lang_utils'; import { truncateTextAtCharIndex } from '../../../utils/general_utils'; @@ -43,11 +43,11 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = }, getUploadingFiles() { - return this.props.filesToUpload.filter((file) => file.status === 'uploading'); + return this.props.filesToUpload.filter((file) => file.status === FileStatus.UPLOADING); }, getUploadedFile() { - return this.props.filesToUpload.filter((file) => file.status === 'upload successful')[0]; + return this.props.filesToUpload.filter((file) => file.status === FileStatus.UPLOAD_SUCESSFUL)[0]; }, handleOnClick() { @@ -144,4 +144,4 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = ); } }); -} \ No newline at end of file +} From edeec39548e3f5b581e6243f9ecdfe5026872ec9 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 8 Dec 2015 20:25:18 +0100 Subject: [PATCH 12/22] Small fixes for upload error behaviour * Change error dialog text slightly * Only show global notification on first error * Only create the S3 blob if the response is successful --- .../file_drag_and_drop_error_dialog.js | 4 ++-- .../ascribe_uploader/react_s3_fine_uploader.js | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) 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 index 326f727f..8696cc7f 100644 --- 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 @@ -24,7 +24,7 @@ let FileDragAndDropErrorDialog = React.createClass({ className='btn btn-default' onClick={() => { if (openIntercom) { - window.Intercom('showNewMessage', getLangText("I'm having trouble with uploading my file: ")); + window.Intercom('showNewMessage', getLangText("I'm having trouble uploading my file.")); } this.retryAllFiles() @@ -38,7 +38,7 @@ let FileDragAndDropErrorDialog = React.createClass({ return (

Let us help you

-

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

+

{getLangText('Still having problems? Send us a message!')}

{this.getRetryButton('Contact us', true)}
); diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index b022fe27..a9dd1039 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -547,7 +547,7 @@ const ReactS3FineUploader = React.createClass({ xhr: this.getXhrErrorComment(xhr) }); // onError will catch any errors, so we can ignore them here - } else if (!res.error || res.success) { + } else if (!res.error && res.success) { let files = this.state.filesToUpload; // Set the state of the completed file to 'upload successful' in order to @@ -596,13 +596,13 @@ const ReactS3FineUploader = React.createClass({ let notificationMessage; if (showErrorPrompt) { - notificationMessage = errorNotificationMessage; - this.setStatusOfFile(id, FileStatus.UPLOAD_FAILED); // If we've already found an error on this upload, just ignore other errors // that pop up. They'll likely pop up again when the user retries. if (!this.state.errorState.errorClass) { + notificationMessage = errorNotificationMessage; + const errorState = React.addons.update(this.state.errorState, { errorClass: { $set: this.getUploadErrorClass({ @@ -620,8 +620,10 @@ const ReactS3FineUploader = React.createClass({ this.cancelUploads(); } - const notification = new GlobalNotificationModel(notificationMessage, 'danger', 5000); - GlobalNotificationActions.appendGlobalNotification(notification); + if (notificationMessage) { + const notification = new GlobalNotificationModel(notificationMessage, 'danger', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + } }, onCancel(id) { @@ -1061,7 +1063,7 @@ const ReactS3FineUploader = React.createClass({ uploadMethod } = this.props; // Only show the error state once all files are finished - const showError = !uploadInProgress && showErrorPrompt && errorClass; + const showError = !uploadInProgress && showErrorPrompt && errorClass != null; const props = { multiple, From e0b35e0a24d9fcb05448c6144acdece7b74bd27c Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 8 Dec 2015 20:55:13 +0100 Subject: [PATCH 13/22] Merge with master --- README.md | 1 + fonts/ascribe-font.eot | Bin 0 -> 3020 bytes fonts/{ascribe-logo.svg => ascribe-font.svg} | 7 +- fonts/ascribe-font.ttf | Bin 0 -> 2836 bytes fonts/ascribe-font.woff | Bin 0 -> 2912 bytes fonts/ascribe-logo.eot | Bin 2856 -> 0 bytes fonts/ascribe-logo.ttf | Bin 2692 -> 0 bytes fonts/ascribe-logo.woff | Bin 2768 -> 0 bytes js/actions/contract_agreement_list_actions.js | 40 +- js/actions/edition_actions.js | 4 +- js/actions/global_notification_actions.js | 6 +- js/actions/piece_actions.js | 4 +- js/actions/webhook_actions.js | 19 + .../accordion_list_item_edition_widget.js | 3 +- .../accordion_list_item_piece.js | 28 +- ...cordion_list_item_thumbnail_placeholder.js | 15 + .../accordion_list_item_wallet.js | 23 +- .../ascribe_buttons/acl_button_list.js | 2 +- .../ascribe_buttons/acls/acl_button.js | 2 +- .../collapsible_paragraph.js | 4 +- .../ascribe_detail/detail_property.js | 1 + js/components/ascribe_detail/edition.js | 34 +- .../ascribe_detail/edition_action_panel.js | 26 +- .../ascribe_detail/edition_container.js | 33 +- .../ascribe_detail/further_details.js | 6 +- .../further_details_fileuploader.js | 6 +- js/components/ascribe_detail/piece.js | 5 +- .../ascribe_detail/piece_container.js | 55 ++- .../ascribe_forms/acl_form_factory.js | 10 +- js/components/ascribe_forms/form.js | 15 +- js/components/ascribe_forms/form_consign.js | 72 +++- js/components/ascribe_forms/form_loan.js | 248 ++++-------- .../ascribe_forms/form_loan_request_answer.js | 6 +- .../ascribe_forms/form_register_piece.js | 27 +- ...ent.js => form_send_contract_agreement.js} | 6 +- .../ascribe_forms/form_unconsign_request.js | 4 +- .../input_contract_agreement_checkbox.js | 206 ++++++++++ .../ascribe_forms/input_fineuploader.js | 35 +- .../ascribe_forms/input_textarea_toggable.js | 6 + .../list_form_request_actions.js | 4 +- js/components/ascribe_forms/property.js | 44 +- .../piece_list_bulk_modal.js | 102 +---- .../piece_list_toolbar_filter_widget.js | 10 +- .../proxy_routes/auth_proxy_handler.js | 6 +- .../ascribe_settings/settings_container.js | 2 + .../ascribe_settings/webhook_settings.js | 165 ++++++++ .../slides_container.js | 14 +- .../facebook_share_button.js | 16 +- js/components/ascribe_spinner.js | 18 +- .../file_drag_and_drop.js | 68 ++-- .../ascribe_upload_button/upload_button.js | 101 +++-- .../react_s3_fine_uploader.js | 13 +- js/components/contract_notification.js | 36 -- js/components/error_not_found_page.js | 12 +- js/components/footer.js | 1 + js/components/global_notification.js | 95 ++--- js/components/header.js | 22 +- js/components/piece_list.js | 205 ++++++++-- js/components/piece_list_filter_display.js | 2 +- js/components/register_piece.js | 16 +- .../components/pr_login_container.js | 0 .../components/pr_register_piece.js | 5 + .../ascribe_detail/prize_piece_container.js | 17 +- ...cordion_list_item_thumbnail_placeholder.js | 15 + .../components/23vivi/23vivi_landing.js | 78 ++++ .../components/23vivi/23vivi_piece_list.js | 24 ++ .../ascribe_detail/wallet_piece_container.js | 2 +- .../cyland_detail/cyland_piece_container.js | 2 +- .../cyland_additional_data_form.js | 16 +- .../components/cyland/cyland_landing.js | 4 +- .../cyland/cyland_register_piece.js | 21 +- .../ikonotv_detail/ikonotv_piece_container.js | 2 +- .../ikonotv_artist_details_form.js | 8 +- .../ikonotv_artwork_details_form.js | 12 +- .../ikonotv/ikonotv_register_piece.js | 60 ++- .../components/lumenus/lumenus_landing.js | 84 ++++ .../market_buttons/market_acl_button_list.js | 74 ++++ .../market_buttons/market_submit_button.js | 160 ++++++++ .../market_detail/market_edition_container.js | 24 ++ .../market_detail/market_further_details.js | 23 ++ .../market_detail/market_piece_container.js | 21 + .../market_additional_data_form.js | 235 +++++++++++ .../components/market/market_piece_list.js | 90 +++++ .../market/market_register_piece.js | 174 ++++++++ .../wallet/constants/wallet_api_urls.js | 16 +- js/components/whitelabel/wallet/wallet_app.js | 2 +- .../whitelabel/wallet/wallet_routes.js | 89 ++++- js/constants/api_urls.js | 3 + js/constants/application_constants.js | 21 +- js/mixins/react_error.js | 16 + js/models/errors.js | 31 ++ js/sources/webhook_source.js | 46 +++ js/stores/edition_list_store.js | 2 +- js/stores/edition_store.js | 6 + js/stores/global_notification_store.js | 57 ++- js/stores/piece_store.js | 6 + js/stores/webhook_store.js | 88 ++++ js/utils/acl_utils.js | 22 +- js/utils/error_utils.js | 3 +- js/utils/file_utils.js | 7 +- js/utils/form_utils.js | 6 + js/utils/general_utils.js | 8 +- js/utils/inject_utils.js | 23 +- js/utils/regex_utils.js | 7 + js/utils/requests.js | 19 +- package.json | 5 +- sass/ascribe-fonts/ascribe-fonts.scss | 57 +-- sass/ascribe_accordion_list.scss | 32 +- sass/ascribe_acl_information.scss | 2 +- sass/ascribe_custom_style.scss | 64 +-- sass/ascribe_notification_list.scss | 7 +- sass/ascribe_notification_page.scss | 11 +- sass/ascribe_panel.scss | 2 +- sass/ascribe_piece_list_toolbar.scss | 4 + sass/ascribe_spinner.scss | 18 +- sass/ascribe_uploader.scss | 9 +- sass/main.scss | 2 +- .../wallet/23vivi/23vivi_custom_style.scss | 377 ++++++++++++++++++ .../whitelabel/wallet/cc/cc_custom_style.scss | 16 + .../wallet/cyland/cyland_custom_style.scss | 108 +++-- .../wallet/ikonotv/ikonotv_custom_style.scss | 36 ++ sass/whitelabel/wallet/index.scss | 1 + 122 files changed, 3342 insertions(+), 949 deletions(-) create mode 100755 fonts/ascribe-font.eot rename fonts/{ascribe-logo.svg => ascribe-font.svg} (90%) mode change 100644 => 100755 create mode 100755 fonts/ascribe-font.ttf create mode 100755 fonts/ascribe-font.woff delete mode 100644 fonts/ascribe-logo.eot delete mode 100644 fonts/ascribe-logo.ttf delete mode 100644 fonts/ascribe-logo.woff create mode 100644 js/actions/webhook_actions.js create mode 100644 js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js rename js/components/ascribe_forms/{form_contract_agreement.js => form_send_contract_agreement.js} (97%) create mode 100644 js/components/ascribe_forms/input_contract_agreement_checkbox.js create mode 100644 js/components/ascribe_settings/webhook_settings.js delete mode 100644 js/components/contract_notification.js create mode 100644 js/components/whitelabel/prize/portfolioreview/components/pr_login_container.js create mode 100644 js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js create mode 100644 js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js create mode 100644 js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js create mode 100644 js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js create mode 100644 js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js create mode 100644 js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js create mode 100644 js/components/whitelabel/wallet/components/market/market_detail/market_edition_container.js create mode 100644 js/components/whitelabel/wallet/components/market/market_detail/market_further_details.js create mode 100644 js/components/whitelabel/wallet/components/market/market_detail/market_piece_container.js create mode 100644 js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js create mode 100644 js/components/whitelabel/wallet/components/market/market_piece_list.js create mode 100644 js/components/whitelabel/wallet/components/market/market_register_piece.js create mode 100644 js/mixins/react_error.js create mode 100644 js/models/errors.js create mode 100644 js/sources/webhook_source.js create mode 100644 js/stores/webhook_store.js create mode 100644 js/utils/regex_utils.js create mode 100644 sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss diff --git a/README.md b/README.md index 36f47954..e07eca0d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Additionally, to work on the white labeling functionality, you need to edit your 127.0.0.1 sluice.localhost.com 127.0.0.1 lumenus.localhost.com 127.0.0.1 portfolioreview.localhost.com +127.0.0.1 23vivi.localhost.com ``` diff --git a/fonts/ascribe-font.eot b/fonts/ascribe-font.eot new file mode 100755 index 0000000000000000000000000000000000000000..860c534b024e95a4fed9250ff21ac9b15619115e GIT binary patch literal 3020 zcma(TZA@F&^_=(o0fri!*w{40Ha5nAfU&Xh3*dmsh%vzsAb|pdB#vXi1TZmVgJr3b zR8c3$luVmIRXathqG`XVCau#{st9%K{;W#1s?t`qkMVq>z%iX0QA%mIN03~ZXu&Y`ghZrY_>i3>4V7QJOC3!VG&}Gf&`p}IMl-= zBw-$wNK^}hM45t%FiTP?qLjffQBtIlkff~O0T;N5^S|s6n*~t>SAYli@e>V=C3{p} zqP$N1Ol&T?uoKz&EeTZ+K0Xy)T%fs>@MW3{re;?rBYVn!Mfeo})t{#0(R2D2zV|$_ zMu_j9CW78xO7;qF5neStx3t{;bKD^OmxKpqld-7!2O1UOe<$2J7hPTe3)TV@k`J)X zN9W>i)_!n~a5wpQVIq%ex4oo}7_LXs`Xqtd|M*;W8W&rV*Y&aSI8RO-CCN(G4akZ8yI zA-seR0+2#|ZQvf>Ba;l92sTfgB>I}in8*AlJ+gaSAq5Q@gU%S3;oPKze ze?5^No94G3b|#GL>tqKqzv0tl$3#aHl-Pz%J{M=VX*q-5%y}FA0tybBi)-?=5oRW; zmb3F|@t(J(Be^j&w7$^Mv9LZga8tlWQ~Gw_>SQ1=x!TvaI@Q`bwc6*vM&af_PHJc) z+0o(^@0s3aynNYgP9U#?3ibl*#l3C z@b|m?D!%qx0L5T~LuAIzdxc7)m*@AP-LdN+(8_r1Z1Z!A#um}TRrh=`){g4-(_jAh z{OljOx&GCy^XIo#`*U+!*QGn-<99A~yGyb0ox_i<69%t6Qv(in59BsUG2P@I8_6lC3 z*Qpka_TA3xQvBJ(3?BT&Q^%2YAvF4VsfYllpYiS-kY~eBoBQtHmg%h5G#}`7Y&)Sw|{X+p8$0-!)uW+CL zWT5qguUz~V_vXG4YuQAZZNyhy9eFh!(>*YJo<&6yiJ zYihc@`v(o9xcqKaf1l4AQ5OAFGuEd!_&kk1M^#;I!^z>{9<5e5+}0}8*LaSVyF>Ms ziJEY%wKdx52$|g4Gw$J)W8Etwo>4;sexm8<_-^_-4i63$6}eQ((1#|wUCQ>)>FeAj z@r|yPXBrQO@s)@D%b}ombR{H2!v_zBqr#zRxVAPNJ!B2e9k0)sJkvN>b!5ERTKbf& z??|1iv)96Pzt`OS-t*C@E}LBH>m!C8oE5-mgl`7c!(kvZ8dGC9^$mF@ zI3vpN$807WpzH)QgQcdS_uNdc);PQj_t1<%9K%iC$A|e@$y*5?U~dPtTvQSw4_eri zV?uiUq!p7S)$l`^&J+Ep3@ZqJS%#JL##YI&is%U$*1*&7J60hm3W;JOP8Q~<(EEuD z^F$wzVFkgPGOVQ1uR(@YM87D*8t}n`=wd9DI2*5@OwKR;w{mhgo?1*K=dB)>`Z!$UWf{IaX1H7Dw|hm?4}|*3BA-=`Rt;-l4?kKH@{)}H*B4I5C8xG literal 0 HcmV?d00001 diff --git a/fonts/ascribe-logo.svg b/fonts/ascribe-font.svg old mode 100644 new mode 100755 similarity index 90% rename from fonts/ascribe-logo.svg rename to fonts/ascribe-font.svg index 2a9bb79b..2628dfee --- a/fonts/ascribe-logo.svg +++ b/fonts/ascribe-font.svg @@ -3,7 +3,7 @@ Generated by IcoMoon - + @@ -12,9 +12,10 @@ - - + + + \ No newline at end of file diff --git a/fonts/ascribe-font.ttf b/fonts/ascribe-font.ttf new file mode 100755 index 0000000000000000000000000000000000000000..a66d8f04d7235861814ad5bb234306b9c4145e44 GIT binary patch literal 2836 zcma(TZA@F&^_=(o0fri!UZ-MkpLg!_b7MMb z%eC%1-=FW^^9%?8MQ{yxaGyTY)Le2v<0Z};v@b*##pS)o-fsY~is;E%ab=lcDbcG0 z3uYJAW+DeFe@XN;0L`D~V&cWJ7r*x`iN;9opCd+@y_EbFJV$iR++u3A^Jln4^e>1G zEF_|$_J=wR(SIk}x+tzLg9YmW3MmFym&C=`TlF7YB-%|e-ds+sq$p2Xy3d##c-0s9 z9H;@f7rEaP=XMt2U%`uoOfPRCbGdH?0rpDy-OMfodDO>i00y#hYE1%}m{3dR zjKJ+%8Id@jDN@juWhSnkv-4@`zPGI_u{AQXx!l#Yyg4#-TfpXu^qqnA znLuD>ePCdHw!M9JeZYau!tJ4)*2q?(tIaFjuXvlu@^!N{fzk&J90WLs2L%IGqaKY0 zE$^#!5)OF1@K<>6A#g`js_$rdUivAYk}mVR>Ay&(SFw#Vr8BBxjr48;Usmf>_>oji zZ$2Ys?(q_T56Yk({M4;n9l5$%zAm?e3Z?Qr`L6+`==P1iz8l-o==SwUaFB1x zT=Bbd>|726nU7E4E?>oc@Xa&CS3OD%f448L>g%`#Pz*LWMsDo9SEx35dHxXE9s3Rf zb;e_7FF)5b)LO%1!B-jB{Vl!L2FFzBO%MJXqsA@s#`Al(A8le5P|{%;!0q3|>nfsKd8wE#4_ZIP54W5L{1MinZ_9 zDk^QZ$_ksQr-yH997=^wUUl{K9Jv|}r-mCNmg5txZ4&{jH89cEI&s{>WsXMXJA%t+ zJV#G2ht{95r55}n0UO7uROzqqkpFC;{fw_t`WE-rp)qUuRJm=;S6dr-KOTw1*)BiV z)pd?-3=NN3EJqs3d_~t8^>|=-I~DxpEsf|`iX90pv>s=H2WMi z4fRcDM@Regdf|9SyU6_N+bCd_0V=J{(*P1-;{IAwdiuJsK8;V`8|z zJ}e%y1{Y5^=3HKA9o$I1|ZqDqg z&HM)YUaCz!LK(f2+J*>sg}C;b6n-xGO}jr>i9 z(r8AaKO0fzW#1~vddl*c8KY@$DUk4tq~M=0NIpQ_4Q7VROGoqEOjBzTUV-~)#vo4M zHt*xZ{DK^<1dp)UK`WP)OehaMY%4h-O+UF~vZfY(r0{v-pHy%a;jbvTnr3W`f@_E$ zS8yFX3BP6k5LAW4sUS%f=RglXR&bv9Lkg}Ud|ScQP!3HBt|9)@3a$emd?>C&lkw?T z<4j^H1tP3K6p|Em8e;U#o`D1`L25LXT!|-^tR9yeMoEw)wYV&@f``VY`*EB+j?Qpw z_UQ#N3Bwd*mhxPnjFOLI7^3X0v$3UEQcT4zTBp~nz0pK}BC+IxP__oJlE)ZT!_pTa dMeri=reTeqy;OAq`f0OzUG%QlKe_+0{sYPEW)lDa literal 0 HcmV?d00001 diff --git a/fonts/ascribe-font.woff b/fonts/ascribe-font.woff new file mode 100755 index 0000000000000000000000000000000000000000..7d18d089b2623098bedccc32e670483ffa0aad7e GIT binary patch literal 2912 zcma)8Z%kX)6+h=a{{ce{PHYSfv5k#!AYg2s;RP^YQesRn1W2I3Ac=7dm;ffmUBl8; zO{S<5q)MwzpsJlBRcP8Ls(t7*l`2C0voD)ct*W$DO{8}12d1f-Hf`0~#?L$V`8k13 z+H$RP@9+FS=lZ_;-m|BIK|r8Ua2^We)tKcO-ak&Au5SRqJd&PgRCGWiL>D4UB%39E zg3)ex_x+j3GLyYeJhNp~JTt#CO|stuX#T_~e4y;tv$4npQg4#JiIMTuAH6tBLXzDh zzLJsAQ7p_Zq%H&G6AOMu9lykl`9zfL@)>Vqr2UCbvk0QV|KRl=?8ihl(G<NcWRF7HDUL9LZ8v|nIFrs~WIx+3#g;kDd{L@0 zONyVYlqUov+aftyR>}d0&2kBk>iEC1%#I4EdO8JIVQ?5KOce&fP@w|J5tJNwKZy77 z3%svzEDr}8nH@e(K6zjuFPGLVl8YI&bd5=$1oKmO*P-8nb`rMquuIEff3nw%*i6}blZm!YSPMn3Ldd|V8 zrMp5)XJTV`czvm}b7_5e@Ro=TrRm%KtJ8k}^lE?q>P&0v%xb?A8^l|KIjiA~L}!a2 z-7Wo)>Gs-Ydje%2G;k2$ARZJASb=&p8MM5w%0(FPTK;eF&I90%s8m1H^1Sp5J|$h^ zchY~6%x_`~XHI8Sg&OJO1ir4;sqk~Dg3eb)%G}{4{tg(ShMjlJ^^xnV@nO5|)F`#@ z-TOD76y3Vs+k1U08r`}U4qw}f&b_@hIJoxq+}!rsz`)w}99pEm*`@DLZASzD`d&-s zX=f&TU8E<_pg6F;>6|Ei8;SXTDIDx|Wuf>TId`rGT3L)QVUsWC?tT9Z@#T+7!{6=8 zt9qC>4+_B!$0&@07sLv)!1IUD;oNr+XlJ|*_UGp|jcuZrtLpt`yaTlz=f3^v#rZ#S z3j?d$7cXwF4&>JM?kk%U6Ps7Mb1Sb;zjpgJ7n82zLFp^cUR&m~*Oa-?{uHq~i=%W# z?cjz{xB{=hZPjug8%o1?%(t#uPdA0RpFsP&URjY1}o5^6-2=-xU{0# zu|JtZu0PwD$&3HAmmmH?sC&^`uA4d8JF^uy*I~6C>vvDa4Lv<2(|vvY;pki3)ir<1 zXjfBXkFC}@89edajq2(b#{I36R>5*5|WPERXuU zXOe*zk_W2s!z!yVX$Xa!d3mDyNo%3@BYSC?-CkB|H+OgQ^>u@(;EAj5?(QR3L!s1A zUD(<--rO?ox7qyTEzRR?RxWciJl7srI_*7rYALw-ygfDFH0-x?oJy7c4i7b*@wcA# zl}S%=9~>IBl}wh{M}1XQ;ZNh?aGX)e+0M?hj4(JfVznNrHTv@0BcpaLXKCoFuI?5N z51GdB^2e0}{XQYAF8H}_yx(Z@c^iDr%G#RxGb1CtdcD}z-YV8rdry{mf_2u(>QJ<` zHPYn_mU{H(JtHe8dsarhW2SoiO4r%>Z2AU{3=J0)xHanF=cNvZ-0h#!H@GX(yWJ~K zH?)QD%?AUQg8^Y|B`8KhM~{Xg;;~4mrY00QW(zEws>_8u-!N2pe4^P_{Di&#c&)pu z&&u_D(%kΞ;*S+g&`IZSHI*J?ECpj@rVnvG=7K#e6j5TT=WIR`C!I;H|jyDfjO< zyAN`AN+B_#D<8FFnvWv#-)eLc{dD-XJmJ^G@V`>iEW<|{eS}0=lzpQp8!0PeZj2||P*%bjNx@&T zm12Op3oH!FLr3?yg>J10cmwXD1p_#aTfC1C@$+)FB0R$G4jQ?tWJa0vu%*<5bo$8*mp3LO#suR42`;dZ#Bqo*0ZhQzU}>r( zTDA!)CDSBOw@i^LRP8VNSEZ>`5o)!+HlcfRZE#$?g< zN#Fg>Ip6QRzSlhsB>>+y03dL%zaiX3CJB%1mosdxmGldrJUa(qge1fv1q;+uut)$S zjKK^f;3~{R43W-1*UFzrK z3$dlj=x^^5sG9iGiP*{#jb+5Mu_!UWc1ioa-$#gF1<>7{orzs6Pik6es)*+Ovm_|D zmwAW}5MMjHkY4Tl1#S_4k@&XxR6M5tk?yC&-y`0-5L;aWD>f0&{0ob*g_+k+6h9{Z zGxG6+rPNBA;-qF^`U3FUQoIXV0Pc0}Hzc`}8}TpT#bSmlXh>o2ZvlX>iut|l9t3#m zG?`y~h7;IF@y0w>(_C)k6;w57ppE1PD5A~fNG5z$oXupjOo?bvXv}_5sUf{Wy9h|A z(gGPB4yj#%JjMNAS$2;M@6pZy)|l+38gq@wYpT(JY!AwId=$h-_ys;vITjNKTiHE6 zNjgPfBCUG8MIsdo>Pek3xbxaZRZP5qyY#7gZG&&x+_cO-InKYE%uLPlyHEO)md#DF z1KIca4B4rK762{wV5`r`*+nB~F1K=Ck6%K`Av(EMUk`Csk{UTXpOGJWJNi;vkYKQ3~&A4I2DhuAyDe%Gn3J%s+r~XaYa2Rz8n>UajAx+e3*``tHJQKsmm1YiQ`! zPCUMIGaSCT6Q6r$V{~-mow>Q)jggU!-8r<%e--7&s1?z~-z!8iO_4zgRpb}Yq$;qy z>71x}mlN}BC>#{3GE@AX;yWJ$jm*ZEu+3L+kW z@H_Y$$f(B>tipPVo(_o#~ilW>Gt{jlH%7QNj@eegLHvf-Fh~O z%4+J^{q(lxjsN$8?q77jyFTZ>zQ#p{96h{p2W!x-_ex%iw_YzeaX{jrx_XQ$0AhUHg#S0;S!)60Py%P%(K2X!{@ zv?&yF6ctI%muv_0ABvS#qF7ZaS_TIAqb;N9;PLCufq{nWp-_6PC2Z@SYVVlp5`?a) zj`pc;8<#y2p6dxLopK*JxfEP~RZP$O$Gb$1(`Yh}@sR&?SLZ2TmHaL4SBEBqis=e* z!dF)p{xlg5Ct0sJ+t+uNbqtP0ZMKHya$k`%Iw9>Ea!Qalm_c%sh#!AJvXTeBQ9O zrEw<^#P`tA<*6#>bx{T*t(X|tU zYZLBC^HKcT(AW3f%x#R0jhB=-b=u(Pm3F(5?VmEYxoh%!18XmPxYvwd{Wt=T=TmEU0hNi9mnXu-GTRmpGo!}DD4xWzvNtL*FT_y zE<3dk(|>%FAK9y@+$#DZv;Tn7NKg5PLRma3Q*jW_TTL zU$T*GfVLm3iiUySU@N^eXW$KZh*k{X6z=doK18!DW+OJR`%Wz%k;14SBkZX0Am~Y! z%aSAdyh`&#`&C*)^i`GC(z|F@X&uRjRN4S9!8-e=Yl=x?q?sJe5uH$Jp6G6s))4)Q zN^7Y&6I5D9@-dY*fDg8l@zg>pwYa~+i_R>sBvXrm+v$3KS@&G6=L*MW5?AMA%lqra z=OTlt#k7!^S)5sprDrY*7uJNqcxsqpb3#xlDbtiif{M&VrO=E)n#PMHy8vtS9aKux QVYNhc(yFt{>3_8U0NQ3t`v3p{ diff --git a/fonts/ascribe-logo.ttf b/fonts/ascribe-logo.ttf deleted file mode 100644 index 61cdce038efc5e33c752040e0e88b55930f8ae6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2692 zcmai0eN0=|6+h=ae}G91ZZL+1_~RKvAmI25KfW;;u}v@zkU+r;NgT&9CV&Yz8!Szg zM9MZnrDU1}YMUw2gr@yP|Ee^VDngs~*QRuH&pY?|xiM{| zUF*K{JKyKrckg`$1b_;-2|Tz?oj&HNd|7jxB=6I_kXVQ>)lUBI9st%8ePox^ zY5?v{?zbelTcG$i@M1Z$D;Y>-?;inxC*}NJeh&gXO;WP8C=RO(ST;86Nmrn}Y*h~` z=peZc%7CLEg7ryxK9|qSUe+fdW6XbEt0KSBxClsAl_fF`KZK@6nqB zv>EIMo6%_N$nkAwIaKgY)k$BN=$E5FC5$)^kqm$xgQaxwqbJ8Pk*Fa`#Vq$Y?U|?x;V(hMnp4!}l==$ZJp3Cde z=z6NVJGCBlphvtrR6^Y#1NzA>sF*dgG-rU^o z#^~tA?i`w>zYEe+R10X}@0VIKPl1_~x=1gfL2+Px(>_u9t|sQ|rEpN{%0lsba_(Xc zL|Ke4V4JV!KK=S0;_IK6hQHaDSM^oiGB^MNI{|3ty`s(J<@rNsckJ5;v@&ixd-=sh zV~gnK8i)TA>ql+>`Tzd-%KRU>h0*ohD_3^cM~k#Cbbb5m+3oA0BIWhDw;w#HBs#cgc#8nk*+Hn#ix{b+Q^wuH&fB+6dK>Kc<7jnx`5YSNl10_$1rBEGo) z71QU3qX}J&EMdEyPM^;&%6Uzq=wng}NC&9htz}A-S5w2zr?9qV;)v*|1T z=|Dr%@mF2vW{hpR<=6XHrhM)*%YmEAFE`@{jTY~WAslv;m5I(*EC;k73bl2DP**FM zLLvTG+gLVu;)XL6I&vc%&W^W5EWNSLu2_%N+7s*QjP+W${L#os-M$%S>}P(cK*%bOdW%`U|efwUa|@Q|@WwG5k_DF!0^n9h@AWsHkvi z)WOec?RL4_zvS+4*QH;E)?W4WhVh+eqpQJycX}--#=}RChT~#KJlxU}j(1oC3#ZzO zAuo8w8;+mtv{t<&M31*R2S+U2(8ryfAO9d8Hx#-{M+(iI8=!M;&hM$s{093@s!=RQ z6TT~@uVNz)=>XnMOP_H6PP6+!9)Ge9R-CWw`4?2sVW;t7_Ro)tl--K*siF@u`wqy1 z{N!&aR3-8f{W(Y!BPn_SeIZhTXKswAxusCTc}c-vFiJ5%JqTvmLq~V8nQof1@Kbn% zW(;5qcX%HkCM~O3k4M;fr%|j(W|W5>c9eP$v?MEZ$q~MwV4kpF!79Sn6s)Fu(Wqbz z$%hrJgI8dkebZItB+-+mfH}e`1@nY^6|5rsnu66(16BoVNItG$9r$27oyaU?GK-Lg z1Y}@=W(F1~lglgV%%aupbipJfVHsA)fD{zRO?R8?gL%m0V0N$Fs>x){AS_p+shcYI8zR&Yh*~DGIYFR}_aV(Mu$|2y65lk{cK) PG(a`d1}S|d-^Bj|`x-nj diff --git a/fonts/ascribe-logo.woff b/fonts/ascribe-logo.woff deleted file mode 100644 index c563df56f6aa31dfe5d9ae9f0acce0d0273ecef8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2768 zcmai0du&tJ8UMa}{m6qlak1m11V66hyhx1Sm%MEj9EUh23B*k@0vKZ)ha?V(S;;6> zL1UW|B$y^8>edNqil+UQ?w?9iQ=w>&zc$gXsy1~Un!5H6rm31VZPGUI^}cg`T}+F# zV}0lRzVCd`bI-%CoCt>jfm+r3P%V$&V*T^liIayt09ZkD=NQ$zs5zQkOe~XZj`&lI z>f`U|NrSE~DD?!rDcW{R%*Hmy!CLzmLtO5*KKi*##IGGAjQZ2}yRF_$EdM zd#z_~F?W?@KO_DKqn@AQ)R48vp;w3VY;uk9~c#>Fs)((Wdw6Z7PuLVabjU!}t(C#fJ*V;&8B4 z*yA%~QvrIiYSEfRGBKf+%o&3_w>KIR!g<`KuHp6u-@dtNntO7Le>an#nd5h#3}#H5 zo8$)yzvT1erye>1)Yy-HpOdo-I?iY?b6!tCMA0EQIlr%;I5SCgoSn~054_!>?AGMu z=5i>syg503M?_D3{$6bTVsG!o^;m2@-P4m^k2%mI-We}xO>SjF-CpTI{d;V#T(?*g zDC?ks{Q&!Mzod>gy^1kL4!hkm_e~$N`0N1KgeM8Ih(oguDbeZ4F|5Y-*h25Mf zUr_DSNFQeLO?8zDKap&-uL@G(J}>e2!2s=SpJSfO^G% z)lK_EsoOxzms8=O+?2WE_vF~69B5=dK7(z(k^9q^_YmLsG&TIqGjUa4#I1mRAg~jF zcHS%6OkSQpfOf|-8-Z5FZD$|9v}kM*-CXm?e`W?yJ8<^D-@ml*2X1j}efQF(-Sx53 z*gte_`}FDUYeS`x*X4hB?;e+uZsLCFb62@8vnf|)CbT{^EYI2`?NI@oFa_7(dvFi7 zVHbV}{{RWKSd9(XLfO+MaiU$&Xifd-_0%0it&T5kW20BE)zM_L-xnA_qeIpuOnN3! zwlW&(OlCA1G^Es`HIoNcv)V;`Y5yy#PY*{6x|&%)znxB>FCfZsO`_;yQgTQK$nVy& zNt9Pp!_KGIUvmD{1$}nW1MZe$_!gUs969=V`3&07uJwvulea}Hn(WU8v&;Dx3p2X$ zpF-oouSbTKER9v^<0I*v;MoC-^+?P)o6!#s*IgVPjYX4hbJsU|yQc;N{$Xo}V>W#3 z)mv?Cug~=M%v!wW%hMg>eGYCeb15(#Y-&0Bvg^#Ov9oIBwSm-9(^H_;ij^v!g2&-7ZY zy))fiGkq4Wa40(8A6!1^K6GL^y#AVyTL?_{3LK|Wx3y^b940LOf;HdRCgv6I>QLV6LE{BwZq`6aK@(uEob%&wzUm;4^9}T@#=?7V=S~)lAG_^tnAgM^i`p;ZyPWh)yT=_4kOKZSLa@u5hPiwk?wE=}8Pa!u2lQIahq` z`0(14d)jyyKd%afUdi9W_{3y&wNs-Ge^PI^%hmoRe~Y^&{bFeCWlvuO-+D53H5~L# zuZ6`#x24QwY~~>o#5)=3WA5J>b|1*?kJiEJvo*c{ zf@(VK)IP}l`C(~fx1xNi=n-e{fZWJVenX))S&-;YL$c&anFr7nJr#Lo#(1JFWh7jX z6#N+*$p@$h<^Kruia)i$*m?s=iu!`^%1*_>^G%8p_@(~4B!Ar2t-gH$ZNpv(*#2n$Yf_cJy3RV$* zRl#bg1FM2HB%e@l75HE~lgut=vrCYHBxGTcC<{yR)XHimyJU4cT@Z&9tiUQM&; 0) { this.actions.updateContractAgreementList(contractAgreementList.results); resolve(contractAgreementList.results); - } - else{ + } else { resolve(null); } }) @@ -35,13 +34,13 @@ class ContractAgreementListActions { ); } - fetchAvailableContractAgreementList(issuer, createContractAgreement) { + fetchAvailableContractAgreementList(issuer, createPublicContractAgreement) { return Q.Promise((resolve, reject) => { OwnershipFetcher.fetchContractAgreementList(issuer, true, null) .then((acceptedContractAgreementList) => { // if there is at least an accepted contract agreement, we're going to // use it - if(acceptedContractAgreementList.count > 0) { + if (acceptedContractAgreementList.count > 0) { this.actions.updateContractAgreementList(acceptedContractAgreementList.results); } else { // otherwise, we're looking for contract agreements that are still pending @@ -50,15 +49,13 @@ class ContractAgreementListActions { // overcomplicate the method OwnershipFetcher.fetchContractAgreementList(issuer, null, true) .then((pendingContractAgreementList) => { - if(pendingContractAgreementList.count > 0) { + if (pendingContractAgreementList.count > 0) { this.actions.updateContractAgreementList(pendingContractAgreementList.results); - } else { + } else if (createPublicContractAgreement) { // if there was neither a pending nor an active contractAgreement - // found and createContractAgreement is set to true, we create a - // new contract agreement - if(createContractAgreement) { - this.actions.createContractAgreementFromPublicContract(issuer); - } + // found and createPublicContractAgreement is set to true, we create a + // new public contract agreement + this.actions.createContractAgreementFromPublicContract(issuer); } }) .catch((err) => { @@ -81,8 +78,7 @@ class ContractAgreementListActions { // create an agreement with the public contract if there is one if (publicContract && publicContract.length > 0) { return this.actions.createContractAgreement(null, publicContract[0]); - } - else { + } else { /* contractAgreementList in the store is already set to null; */ @@ -91,21 +87,17 @@ class ContractAgreementListActions { if (publicContracAgreement) { this.actions.updateContractAgreementList([publicContracAgreement]); } - }).catch((err) => { - console.logGlobal(err); - }); + }).catch(console.logGlobal); } createContractAgreement(issuer, contract){ return Q.Promise((resolve, reject) => { - OwnershipFetcher.createContractAgreement(issuer, contract).then( - (contractAgreement) => { - resolve(contractAgreement); - } - ).catch((err) => { - console.logGlobal(err); - reject(err); - }); + OwnershipFetcher + .createContractAgreement(issuer, contract).then(resolve) + .catch((err) => { + console.logGlobal(err); + reject(err); + }); }); } } diff --git a/js/actions/edition_actions.js b/js/actions/edition_actions.js index 4bdf093a..3f659524 100644 --- a/js/actions/edition_actions.js +++ b/js/actions/edition_actions.js @@ -7,7 +7,8 @@ import EditionFetcher from '../fetchers/edition_fetcher'; class EditionActions { constructor() { this.generateActions( - 'updateEdition' + 'updateEdition', + 'editionFailed' ); } @@ -18,6 +19,7 @@ class EditionActions { }) .catch((err) => { console.logGlobal(err); + this.actions.editionFailed(err.json); }); } } diff --git a/js/actions/global_notification_actions.js b/js/actions/global_notification_actions.js index 2bb8d6e6..73aa9815 100644 --- a/js/actions/global_notification_actions.js +++ b/js/actions/global_notification_actions.js @@ -2,13 +2,15 @@ import { alt } from '../alt'; - class GlobalNotificationActions { constructor() { this.generateActions( 'appendGlobalNotification', + 'showNextGlobalNotification', 'shiftGlobalNotification', - 'emulateEmptyStore' + 'cooldownGlobalNotifications', + 'pauseGlobalNotifications', + 'resumeGlobalNotifications' ); } } diff --git a/js/actions/piece_actions.js b/js/actions/piece_actions.js index 7aed13fc..9002e8c5 100644 --- a/js/actions/piece_actions.js +++ b/js/actions/piece_actions.js @@ -8,7 +8,8 @@ class PieceActions { constructor() { this.generateActions( 'updatePiece', - 'updateProperty' + 'updateProperty', + 'pieceFailed' ); } @@ -19,6 +20,7 @@ class PieceActions { }) .catch((err) => { console.logGlobal(err); + this.actions.pieceFailed(err.json); }); } } diff --git a/js/actions/webhook_actions.js b/js/actions/webhook_actions.js new file mode 100644 index 00000000..f9555ce7 --- /dev/null +++ b/js/actions/webhook_actions.js @@ -0,0 +1,19 @@ +'use strict'; + +import { alt } from '../alt'; + + +class WebhookActions { + constructor() { + this.generateActions( + 'fetchWebhooks', + 'successFetchWebhooks', + 'fetchWebhookEvents', + 'successFetchWebhookEvents', + 'removeWebhook', + 'successRemoveWebhook' + ); + } +} + +export default alt.createActions(WebhookActions); diff --git a/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js b/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js index 5d3e033f..8033f239 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js @@ -78,7 +78,6 @@ let AccordionListItemEditionWidget = React.createClass({ return ( ); } else { @@ -137,4 +136,4 @@ let AccordionListItemEditionWidget = React.createClass({ } }); -export default AccordionListItemEditionWidget; \ No newline at end of file +export default AccordionListItemEditionWidget; diff --git a/js/components/ascribe_accordion_list/accordion_list_item_piece.js b/js/components/ascribe_accordion_list/accordion_list_item_piece.js index 4547ce3b..006479c5 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_piece.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_piece.js @@ -4,6 +4,7 @@ import React from 'react'; import { Link } from 'react-router'; import AccordionListItem from './accordion_list_item'; +import AccordionListItemThumbnailPlacholder from './accordion_list_item_thumbnail_placeholder'; import { getLangText } from '../../utils/lang_utils'; @@ -19,7 +20,14 @@ let AccordionListItemPiece = React.createClass({ ]), subsubheading: React.PropTypes.object, buttons: React.PropTypes.object, - badge: React.PropTypes.object + badge: React.PropTypes.object, + thumbnailPlaceholder: React.PropTypes.func + }, + + getDefaultProps() { + return { + thumbnailPlaceholder: AccordionListItemThumbnailPlacholder + }; }, getLinkData() { @@ -34,19 +42,23 @@ let AccordionListItemPiece = React.createClass({ }, render() { - const { className, piece, artistName, buttons, badge, children, subsubheading } = this.props; + const { + artistName, + badge, + buttons, + children, + className, + piece, + subsubheading, + thumbnailPlaceholder: ThumbnailPlaceholder } = this.props; const { url, url_safe } = piece.thumbnail; let thumbnail; // Since we're going to refactor the thumbnail generation anyway at one point, // for not use the annoying ascribe_spiral.png, we're matching the url against // this name and replace it with a CSS version of the new logo. - if(url.match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/)) { - thumbnail = ( - - A - - ); + if (url.match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/)) { + thumbnail = (); } else { thumbnail = (
diff --git a/js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js b/js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js new file mode 100644 index 00000000..37c98371 --- /dev/null +++ b/js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js @@ -0,0 +1,15 @@ +'use strict' + +import React from 'react'; + +let AccordionListItemThumbnailPlaceholder = React.createClass({ + render() { + return ( + + A + + ); + } +}); + +export default AccordionListItemThumbnailPlaceholder; diff --git a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js index da45d1e8..a8cab166 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -31,6 +31,7 @@ let AccordionListItemWallet = React.createClass({ propTypes: { className: React.PropTypes.string, content: React.PropTypes.object, + thumbnailPlaceholder: React.PropTypes.func, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -123,32 +124,36 @@ let AccordionListItemWallet = React.createClass({ }, render() { + const { children, className, content, thumbnailPlaceholder } = this.props; return ( - {Moment(this.props.content.date_created, 'YYYY-MM-DD').year()} + {Moment(content.date_created, 'YYYY-MM-DD').year()} {this.getLicences()} -
} +
+ } buttons={
-
} - badge={this.getGlyphicon()}> +
+ } + badge={this.getGlyphicon()} + thumbnailPlaceholder={thumbnailPlaceholder}> {this.getCreateEditionsDialog()} {/* this.props.children is AccordionListItemTableEditions */} - {this.props.children} + {children} ); } diff --git a/js/components/ascribe_buttons/acl_button_list.js b/js/components/ascribe_buttons/acl_button_list.js index 42f86320..35e42c20 100644 --- a/js/components/ascribe_buttons/acl_button_list.js +++ b/js/components/ascribe_buttons/acl_button_list.js @@ -41,7 +41,7 @@ let AclButtonList = React.createClass({ componentDidMount() { UserStore.listen(this.onChange); - UserActions.fetchCurrentUser(); + UserActions.fetchCurrentUser.defer(); window.addEventListener('resize', this.handleResize); window.dispatchEvent(new Event('resize')); diff --git a/js/components/ascribe_buttons/acls/acl_button.js b/js/components/ascribe_buttons/acls/acl_button.js index 6a3df7b2..97f2e173 100644 --- a/js/components/ascribe_buttons/acls/acl_button.js +++ b/js/components/ascribe_buttons/acls/acl_button.js @@ -26,7 +26,7 @@ export default function ({ action, displayName, title, tooltip }) { availableAcls: React.PropTypes.object.isRequired, buttonAcceptName: React.PropTypes.string, buttonAcceptClassName: React.PropTypes.string, - currentUser: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object, email: React.PropTypes.string, pieceOrEditions: React.PropTypes.oneOfType([ React.PropTypes.object, diff --git a/js/components/ascribe_collapsible/collapsible_paragraph.js b/js/components/ascribe_collapsible/collapsible_paragraph.js index e146b42b..7ad8d0af 100644 --- a/js/components/ascribe_collapsible/collapsible_paragraph.js +++ b/js/components/ascribe_collapsible/collapsible_paragraph.js @@ -12,7 +12,9 @@ const CollapsibleParagraph = React.createClass({ React.PropTypes.object, React.PropTypes.array ]), - iconName: React.PropTypes.string + iconName: React.PropTypes.string, + show: React.PropTypes.bool, + defaultExpanded: React.PropTypes.bool }, getDefaultProps() { diff --git a/js/components/ascribe_detail/detail_property.js b/js/components/ascribe_detail/detail_property.js index 9ea37285..8b0f50b5 100644 --- a/js/components/ascribe_detail/detail_property.js +++ b/js/components/ascribe_detail/detail_property.js @@ -7,6 +7,7 @@ let DetailProperty = React.createClass({ propTypes: { label: React.PropTypes.string, value: React.PropTypes.oneOfType([ + React.PropTypes.number, React.PropTypes.string, React.PropTypes.element ]), diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 6b38ddf8..bc2f0cfa 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -41,13 +41,20 @@ import { getLangText } from '../../utils/lang_utils'; */ let Edition = React.createClass({ propTypes: { + actionPanelButtonListType: React.PropTypes.func, + furtherDetailsType: React.PropTypes.func, edition: React.PropTypes.object, - loadEdition: React.PropTypes.func, - location: React.PropTypes.object + loadEdition: React.PropTypes.func }, mixins: [History], + getDefaultProps() { + return { + furtherDetailsType: FurtherDetails + }; + }, + getInitialState() { return UserStore.getState(); }, @@ -75,6 +82,8 @@ let Edition = React.createClass({ }, render() { + let FurtherDetailsType = this.props.furtherDetailsType; + return ( @@ -90,6 +99,7 @@ let Edition = React.createClass({
@@ -137,7 +147,7 @@ let Edition = React.createClass({ currentUser={this.state.currentUser}/> {return {'bitcoin_id': this.props.edition.bitcoin_id}; }} - label={getLangText('Edition note (public)')} + label={getLangText('Personal note (public)')} defaultValue={this.props.edition.public_note ? this.props.edition.public_note : null} placeholder={getLangText('Enter your comments ...')} editable={!!this.props.edition.acl.acl_edit} @@ -151,13 +161,12 @@ let Edition = React.createClass({ show={this.props.edition.acl.acl_edit || Object.keys(this.props.edition.extra_data).length > 0 || this.props.edition.other_data.length > 0}> - + handleSuccess={this.props.loadEdition} /> @@ -173,6 +182,7 @@ let Edition = React.createClass({ let EditionSummary = React.createClass({ propTypes: { + actionPanelButtonListType: React.PropTypes.func, edition: React.PropTypes.object, currentUser: React.PropTypes.object, handleSuccess: React.PropTypes.func @@ -185,7 +195,7 @@ let EditionSummary = React.createClass({ getStatus(){ let status = null; if (this.props.edition.status.length > 0){ - let statusStr = this.props.edition.status.join().replace(/_/, ' '); + let statusStr = this.props.edition.status.join(', ').replace(/_/g, ' '); status = ; if (this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer){ status = ( @@ -197,7 +207,7 @@ let EditionSummary = React.createClass({ }, render() { - let { edition, currentUser } = this.props; + let { actionPanelButtonListType, edition, currentUser } = this.props; return (
{this.getStatus()} - + {/* + `acl_view` is always available in `edition.acl`, therefore if it has + no more than 1 key, we're hiding the `DetailProperty` actions as otherwise + `AclInformation` would show up + */} + 1}> diff --git a/js/components/ascribe_detail/edition_action_panel.js b/js/components/ascribe_detail/edition_action_panel.js index 162427d5..36a79e7c 100644 --- a/js/components/ascribe_detail/edition_action_panel.js +++ b/js/components/ascribe_detail/edition_action_panel.js @@ -36,6 +36,7 @@ import { getLangText } from '../../utils/lang_utils'; */ let EditionActionPanel = React.createClass({ propTypes: { + actionPanelButtonListType: React.PropTypes.func, edition: React.PropTypes.object, currentUser: React.PropTypes.object, handleSuccess: React.PropTypes.func @@ -43,6 +44,12 @@ let EditionActionPanel = React.createClass({ mixins: [History], + getDefaultProps() { + return { + actionPanelButtonListType: AclButtonList + }; + }, + getInitialState() { return PieceListStore.getState(); }, @@ -87,7 +94,10 @@ let EditionActionPanel = React.createClass({ }, render(){ - let {edition, currentUser} = this.props; + const { + actionPanelButtonListType: ActionPanelButtonListType, + edition, + currentUser } = this.props; if (edition && edition.notifications && @@ -104,7 +114,7 @@ let EditionActionPanel = React.createClass({ return ( - + type="text" + value={edition.bitcoin_id} + readOnly />
}> + label={labels.email || getLangText('Email')} + editable={!defaultEmail} + onChange={this.handleEmailOnChange} + overrideForm={!!defaultEmail}> + label={labels.message || getLangText('Personal Message')} + editable + overrideForm> + + + diff --git a/js/components/ascribe_forms/form_loan.js b/js/components/ascribe_forms/form_loan.js index d6398a20..a204fb87 100644 --- a/js/components/ascribe_forms/form_loan.js +++ b/js/components/ascribe_forms/form_loan.js @@ -1,33 +1,34 @@ 'use strict'; import React from 'react'; - import classnames from 'classnames'; import Button from 'react-bootstrap/lib/Button'; +import ContractAgreementListStore from '../../stores/contract_agreement_list_store'; + import Form from './form'; import Property from './property'; -import InputTextAreaToggable from './input_textarea_toggable'; -import InputDate from './input_date'; -import InputCheckbox from './input_checkbox'; -import ContractAgreementListStore from '../../stores/contract_agreement_list_store'; -import ContractAgreementListActions from '../../actions/contract_agreement_list_actions'; +import InputDate from './input_date'; +import InputTextAreaToggable from './input_textarea_toggable'; +import InputContractAgreementCheckbox from './input_contract_agreement_checkbox'; import AscribeSpinner from '../ascribe_spinner'; -import { mergeOptions } from '../../utils/general_utils'; -import { getLangText } from '../../utils/lang_utils'; import AclInformation from '../ascribe_buttons/acl_information'; +import { getLangText } from '../../utils/lang_utils'; +import { mergeOptions } from '../../utils/general_utils'; + + let LoanForm = React.createClass({ propTypes: { loanHeading: React.PropTypes.string, email: React.PropTypes.string, gallery: React.PropTypes.string, - startdate: React.PropTypes.object, - enddate: React.PropTypes.object, + startDate: React.PropTypes.object, + endDate: React.PropTypes.object, showPersonalMessage: React.PropTypes.bool, showEndDate: React.PropTypes.bool, showStartDate: React.PropTypes.bool, @@ -36,7 +37,11 @@ let LoanForm = React.createClass({ id: React.PropTypes.object, message: React.PropTypes.string, createPublicContractAgreement: React.PropTypes.bool, - handleSuccess: React.PropTypes.func + handleSuccess: React.PropTypes.func, + children: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]) }, getDefaultProps() { @@ -45,148 +50,33 @@ let LoanForm = React.createClass({ showPersonalMessage: true, showEndDate: true, showStartDate: true, - showPassword: true, - createPublicContractAgreement: true + showPassword: true }; }, getInitialState() { - return ContractAgreementListStore.getState(); - }, - - componentDidMount() { - ContractAgreementListStore.listen(this.onChange); - this.getContractAgreementsOrCreatePublic(this.props.email); - }, - - /** - * This method needs to be in form_loan as some whitelabel pages (Cyland) load - * the loanee's email async! - * - * SO LEAVE IT IN! - */ - componentWillReceiveProps(nextProps) { - if(nextProps && nextProps.email && this.props.email !== nextProps.email) { - this.getContractAgreementsOrCreatePublic(nextProps.email); - } - }, - - componentWillUnmount() { - ContractAgreementListStore.unlisten(this.onChange); + return { + email: this.props.email || '' + }; }, onChange(state) { this.setState(state); }, - getContractAgreementsOrCreatePublic(email){ - ContractAgreementListActions.flushContractAgreementList.defer(); - if (email) { - // fetch the available contractagreements (pending/accepted) - ContractAgreementListActions.fetchAvailableContractAgreementList(email, true); - } - }, - - getFormData(){ - return mergeOptions( - this.props.id, - this.getContractAgreementId() - ); - }, - - handleOnChange(event) { + handleEmailOnChange(event) { // event.target.value is the submitted email of the loanee - if(event && event.target && event.target.value && event.target.value.match(/.*@.*\..*/)) { - this.getContractAgreementsOrCreatePublic(event.target.value); - } else { - ContractAgreementListActions.flushContractAgreementList(); - } + this.setState({ + email: event && event.target && event.target.value || '' + }); }, - getContractAgreementId() { - if (this.state.contractAgreementList && this.state.contractAgreementList.length > 0) { - return {'contract_agreement_id': this.state.contractAgreementList[0].id}; - } - return {}; + handleReset() { + this.handleEmailOnChange(); }, - getContractCheckbox() { - if(this.state.contractAgreementList && this.state.contractAgreementList.length > 0) { - // we need to define a key on the InputCheckboxes as otherwise - // react is not rerendering them on a store switch and is keeping - // the default value of the component (which is in that case true) - let contractAgreement = this.state.contractAgreementList[0]; - let contract = contractAgreement.contract; - - if(contractAgreement.datetime_accepted) { - return ( - - - - {getLangText('Download contract')} - - {/* We still need to send the server information that we're accepting */} - - - ); - } else { - return ( - - - - {getLangText('I agree to the')}  - - {getLangText('terms of ')} {contract.issuer} - - - - - ); - } - } else { - return ( - - - - ); - } - }, - - getAppendix() { - if(this.state.contractAgreementList && this.state.contractAgreementList.length > 0) { - let appendix = this.state.contractAgreementList[0].appendix; - if (appendix && appendix.default) { - return ( - -
{appendix.default}
-
- ); - } - } - return null; + getFormData() { + return this.props.id; }, getButtons() { @@ -214,14 +104,31 @@ let LoanForm = React.createClass({ }, render() { + const { email } = this.state; + const { + children, + createPublicContractAgreement, + email: defaultEmail, + handleSuccess, + gallery, + loanHeading, + message, + showPersonalMessage, + endDate, + startDate, + showEndDate, + showStartDate, + showPassword, + url } = this.props; + return ( @@ -229,18 +136,18 @@ let LoanForm = React.createClass({

}> -
-

{this.props.loanHeading}

+
+

{loanHeading}

+ editable={!defaultEmail} + onChange={this.handleEmailOnChange} + overrideForm={!!defaultEmail}> @@ -248,31 +155,31 @@ let LoanForm = React.createClass({ + editable={!gallery} + overrideForm={!!gallery}> + editable={!startDate} + overrideForm={!!startDate} + expanded={showStartDate}> + expanded={showEndDate}> + expanded={showPersonalMessage}> + required={showPersonalMessage}/> + + + - {this.getContractCheckbox()} - {this.getAppendix()} + expanded={showPassword}> + required={showPassword}/> - {this.props.children} + {children} ); } diff --git a/js/components/ascribe_forms/form_loan_request_answer.js b/js/components/ascribe_forms/form_loan_request_answer.js index 1bfe90db..349b4efc 100644 --- a/js/components/ascribe_forms/form_loan_request_answer.js +++ b/js/components/ascribe_forms/form_loan_request_answer.js @@ -65,8 +65,8 @@ let LoanRequestAnswerForm = React.createClass({ url={this.props.url} email={this.state.loanRequest ? this.state.loanRequest.new_owner : null} gallery={this.state.loanRequest ? this.state.loanRequest.gallery : null} - startdate={startDate} - enddate={endDate} + startDate={startDate} + endDate={endDate} showPassword={true} showPersonalMessage={false} handleSuccess={this.props.handleSuccess}/> @@ -76,4 +76,4 @@ let LoanRequestAnswerForm = React.createClass({ } }); -export default LoanRequestAnswerForm; \ No newline at end of file +export default LoanRequestAnswerForm; diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index 8e8b015c..f1c49191 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -31,7 +31,7 @@ let RegisterPieceForm = React.createClass({ isFineUploaderActive: React.PropTypes.bool, isFineUploaderEditable: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool, - onLoggedOut: React.PropTypes.func, + enableSeparateThumbnail: React.PropTypes.bool, // For this form to work with SlideContainer, we sometimes have to disable it disabled: React.PropTypes.bool, @@ -46,7 +46,8 @@ let RegisterPieceForm = React.createClass({ return { headerMessage: getLangText('Register your work'), submitMessage: getLangText('Register work'), - enableLocalHashing: true + enableLocalHashing: true, + enableSeparateThumbnail: true }; }, @@ -89,6 +90,11 @@ let RegisterPieceForm = React.createClass({ if (digitalWorkFile && (digitalWorkFile.status === FileStatus.DELETED || digitalWorkFile.status === FileStatus.CANCELED)) { this.refs.form.refs.thumbnail_file.reset(); + + // Manually we need to set the ready state for `thumbnailKeyReady` back + // to `true` as `ReactS3Fineuploader`'s `reset` method triggers + // `setIsUploadReady` with `false` + this.refs.submitButton.setReadyStateForKey('thumbnailKeyReady', true); this.setState({ digitalWorkFile: null }); } else { this.setState({ digitalWorkFile }); @@ -99,13 +105,18 @@ let RegisterPieceForm = React.createClass({ const { digitalWorkFile } = this.state; const { fineuploader } = this.refs.digitalWorkFineUploader.refs; - fineuploader.setThumbnailForFileId(digitalWorkFile.id, thumbnailFile.url); + fineuploader.setThumbnailForFileId( + digitalWorkFile.id, + // if thumbnail was deleted, we delete it from the display as well + thumbnailFile.status !== FileStatus.DELETED ? thumbnailFile.url : null + ); }, isThumbnailDialogExpanded() { + const { enableSeparateThumbnail } = this.props; const { digitalWorkFile } = this.state; - if(digitalWorkFile) { + if(digitalWorkFile && enableSeparateThumbnail) { const { type: mimeType } = digitalWorkFile; const mimeSubType = mimeType && mimeType.split('/').length ? mimeType.split('/')[1] : 'unknown'; @@ -121,7 +132,6 @@ let RegisterPieceForm = React.createClass({ submitMessage, headerMessage, isFineUploaderActive, - onLoggedOut, isFineUploaderEditable, location, children, @@ -172,7 +182,6 @@ let RegisterPieceForm = React.createClass({ setIsUploadReady={this.setIsUploadReady('digitalWorkKeyReady')} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} isFineUploaderActive={isFineUploaderActive} - onLoggedOut={onLoggedOut} disabled={!isFineUploaderEditable} enableLocalHashing={hashLocally} uploadMethod={location.query.method} @@ -189,7 +198,7 @@ let RegisterPieceForm = React.createClass({ url: ApiUrls.blob_thumbnails }} handleChangedFile={this.handleChangedThumbnail} - isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} + isReadyForFormSubmission={formSubmissionValidation.fileOptional} keyRoutine={{ url: AppConstants.serverUrl + 's3/key/', fileClass: 'thumbnail' @@ -203,7 +212,9 @@ let RegisterPieceForm = React.createClass({ fileClassToUpload={{ singular: getLangText('Select representative image'), plural: getLangText('Select representative images') - }} /> + }} + isFineUploaderActive={isFineUploaderActive} + disabled={!isFineUploaderEditable} /> + required />
@@ -65,4 +65,4 @@ let UnConsignRequestForm = React.createClass({ } }); -export default UnConsignRequestForm; \ No newline at end of file +export default UnConsignRequestForm; diff --git a/js/components/ascribe_forms/input_contract_agreement_checkbox.js b/js/components/ascribe_forms/input_contract_agreement_checkbox.js new file mode 100644 index 00000000..61235631 --- /dev/null +++ b/js/components/ascribe_forms/input_contract_agreement_checkbox.js @@ -0,0 +1,206 @@ +'use strict'; + +import React from 'react/addons'; + +import InputCheckbox from './input_checkbox'; + +import ContractAgreementListStore from '../../stores/contract_agreement_list_store'; +import ContractAgreementListActions from '../../actions/contract_agreement_list_actions'; + +import { getLangText } from '../../utils/lang_utils'; +import { mergeOptions } from '../../utils/general_utils'; +import { isEmail } from '../../utils/regex_utils'; + + +const InputContractAgreementCheckbox = React.createClass({ + propTypes: { + createPublicContractAgreement: React.PropTypes.bool, + email: React.PropTypes.string, + + required: React.PropTypes.bool, + + // provided by Property + disabled: React.PropTypes.bool, + onChange: React.PropTypes.func, + name: React.PropTypes.string, + setExpanded: React.PropTypes.func, + + // can be used to style the component from the outside + style: React.PropTypes.object + }, + + getDefaultProps() { + return { + createPublicContractAgreement: true + }; + }, + + getInitialState() { + return mergeOptions( + ContractAgreementListStore.getState(), + { + value: { + terms: null, + contract_agreement_id: null + } + } + ); + }, + + componentDidMount() { + ContractAgreementListStore.listen(this.onStoreChange); + this.getContractAgreementsOrCreatePublic(this.props.email); + }, + + componentWillReceiveProps({ email: nextEmail }) { + if (this.props.email !== nextEmail) { + if (isEmail(nextEmail)) { + this.getContractAgreementsOrCreatePublic(nextEmail); + } else if (this.getContractAgreement()) { + ContractAgreementListActions.flushContractAgreementList(); + } + } + }, + + componentWillUnmount() { + ContractAgreementListStore.unlisten(this.onStoreChange); + }, + + onStoreChange(state) { + const contractAgreement = this.getContractAgreement(state.contractAgreementList); + + // If there is no contract available, hide this `Property` from the user + this.props.setExpanded(!!contractAgreement); + + state = mergeOptions(state, { + value: { + // If `email` is defined in this component, `getContractAgreementsOrCreatePublic` + // is either: + // + // - fetching a already existing contract agreement; or + // - trying to create a contract agreement + // + // If both attempts result in `contractAgreement` being not defined, + // it means that the receiver hasn't defined a contract, which means + // a contract agreement cannot be created, which means we don't have to + // specify `contract_agreement_id` when sending a request to the server. + contract_agreement_id: contractAgreement ? contractAgreement.id : null, + // If the receiver hasn't set a contract or the contract was + // previously accepted, we set the terms to `true` + // as we always need to at least give a boolean value for `terms` + // to the API endpoint + terms: !contractAgreement || !!contractAgreement.datetime_accepted + } + }); + + this.setState(state); + }, + + onChange(event) { + // Sync the value between our `InputCheckbox` and this component's `terms` + // so the parent `Property` is able to get the correct value of this component + // when the `Form` queries it. + this.setState({ + value: React.addons.update(this.state.value, { + terms: { $set: event.target.value } + }) + }); + + // Propagate change events from the checkbox up to the parent `Property` + this.props.onChange(event); + }, + + getContractAgreement(contractAgreementList = this.state.contractAgreementList) { + if (contractAgreementList && contractAgreementList.length) { + return contractAgreementList[0]; + } + }, + + getContractAgreementsOrCreatePublic(email) { + ContractAgreementListActions.flushContractAgreementList.defer(); + + if (email) { + // fetch the available contractagreements (pending/accepted) + ContractAgreementListActions.fetchAvailableContractAgreementList(email, this.props.createPublicContractAgreement); + } + }, + + getAppendix() { + const contractAgreement = this.getContractAgreement(); + + if (contractAgreement && + contractAgreement.appendix && + contractAgreement.appendix.default) { + return ( +
+

{getLangText('Appendix')}

+
{contractAgreement.appendix.default}
+
+ ); + } + }, + + getContractCheckbox() { + const contractAgreement = this.getContractAgreement(); + + if(contractAgreement) { + const { + datetime_accepted: datetimeAccepted, + contract: { + issuer: contractIssuer, + blob: { url_safe: contractUrl } + } + } = contractAgreement; + + if(datetimeAccepted) { + return ( + + ); + } else { + const { + name, + disabled, + style } = this.props; + + return ( + + + {getLangText('I agree to the')}  + + {getLangText('terms of ')} {contractIssuer} + + + + ); + } + } + }, + + render() { + return ( +
+ {this.getContractCheckbox()} + {this.getAppendix()} +
+ ); + } +}); + +export default InputContractAgreementCheckbox; diff --git a/js/components/ascribe_forms/input_fineuploader.js b/js/components/ascribe_forms/input_fineuploader.js index 6ee44113..625ac2ff 100644 --- a/js/components/ascribe_forms/input_fineuploader.js +++ b/js/components/ascribe_forms/input_fineuploader.js @@ -18,10 +18,10 @@ const InputFineUploader = React.createClass({ // a user is actually not logged in already to prevent him from droping files // before login in isFineUploaderActive: bool, - onLoggedOut: func, // provided by Property disabled: bool, + onChange: func, // Props for ReactS3FineUploader areAssetsDownloadable: bool, @@ -110,22 +110,22 @@ const InputFineUploader = React.createClass({ }, render() { - const { fileInputElement, - keyRoutine, - createBlobRoutine, - validation, - setIsUploadReady, - isReadyForFormSubmission, - isFineUploaderActive, - areAssetsDownloadable, - onLoggedOut, - enableLocalHashing, - fileClassToUpload, - uploadMethod, - handleChangedFile, - setWarning, - showErrorPrompt, - disabled } = this.props; + const { + areAssetsDownloadable, + createBlobRoutine, + enableLocalHashing, + disabled, + fileClassToUpload, + fileInputElement, + handleChangedFile, + isFineUploaderActive, + isReadyForFormSubmission, + keyRoutine, + setIsUploadReady, + setWarning, + showErrorPrompt, + uploadMethod, + validation } = this.props; let editable = isFineUploaderActive; // if disabled is actually set by property, we want to override @@ -162,7 +162,6 @@ const InputFineUploader = React.createClass({ 'X-CSRFToken': getCookie(AppConstants.csrftoken) } }} - onInactive={onLoggedOut} enableLocalHashing={enableLocalHashing} uploadMethod={uploadMethod} fileClassToUpload={fileClassToUpload} diff --git a/js/components/ascribe_forms/input_textarea_toggable.js b/js/components/ascribe_forms/input_textarea_toggable.js index c17a0e5a..0be8b87a 100644 --- a/js/components/ascribe_forms/input_textarea_toggable.js +++ b/js/components/ascribe_forms/input_textarea_toggable.js @@ -7,6 +7,7 @@ import TextareaAutosize from 'react-textarea-autosize'; let InputTextAreaToggable = React.createClass({ propTypes: { + autoFocus: React.PropTypes.bool, disabled: React.PropTypes.bool, rows: React.PropTypes.number.isRequired, required: React.PropTypes.bool, @@ -23,6 +24,10 @@ let InputTextAreaToggable = React.createClass({ }, componentDidMount() { + if (this.props.autoFocus) { + this.refs.textarea.focus(); + } + this.setState({ value: this.props.defaultValue }); @@ -51,6 +56,7 @@ let InputTextAreaToggable = React.createClass({ className = className + ' ascribe-textarea-editable'; textarea = ( { - // Since refs will be overriden by this functions return statement, // we still want to be able to define refs for nested `Form` or `Property` // children, which is why we're upfront simply invoking the callback-ref- @@ -252,7 +268,8 @@ const Property = React.createClass({ setWarning: this.setWarning, disabled: !this.props.editable, ref: 'input', - name: this.props.name + name: this.props.name, + setExpanded: this.setExpanded }); }); } @@ -271,10 +288,6 @@ const Property = React.createClass({ } }, - handleCheckboxToggle() { - this.setState({expanded: !this.state.expanded}); - }, - getCheckbox() { const { checkboxLabel } = this.props; @@ -298,23 +311,20 @@ const Property = React.createClass({ render() { let footer = null; - let style = this.props.style ? mergeOptions({}, this.props.style) : {}; if(this.props.footer){ footer = (
{this.props.footer} -
); +
+ ); } - style.paddingBottom = !this.state.expanded ? 0 : null; - style.cursor = !this.props.editable ? 'not-allowed' : null; - return (
+ style={this.props.style}> {this.getCheckbox()}
{this.getLabelAndErrors()} - {this.renderChildren(style)} + {this.renderChildren(this.props.style)} {footer}
diff --git a/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js b/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js index 2eedbd4c..c9c7f9f4 100644 --- a/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js +++ b/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js @@ -4,77 +4,24 @@ import React from 'react'; import { mergeOptions } from '../../utils/general_utils'; -import EditionListStore from '../../stores/edition_list_store'; import EditionListActions from '../../actions/edition_list_actions'; -import UserStore from '../../stores/user_store'; -import UserActions from '../../actions/user_actions'; - -import PieceListStore from '../../stores/piece_list_store'; -import PieceListActions from '../../actions/piece_list_actions'; - import PieceListBulkModalSelectedEditionsWidget from './piece_list_bulk_modal_selected_editions_widget'; -import AclButtonList from '../ascribe_buttons/acl_button_list'; -import DeleteButton from '../ascribe_buttons/delete_button'; -import { getAvailableAcls } from '../../utils/acl_utils'; import { getLangText } from '../../utils/lang_utils.js'; let PieceListBulkModal = React.createClass({ propTypes: { - className: React.PropTypes.string - }, - - getInitialState() { - return mergeOptions( - EditionListStore.getState(), - UserStore.getState(), - PieceListStore.getState() - ); - }, - - - - componentDidMount() { - EditionListStore.listen(this.onChange); - UserStore.listen(this.onChange); - PieceListStore.listen(this.onChange); - - UserActions.fetchCurrentUser(); - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); - }, - - componentWillUnmount() { - EditionListStore.unlisten(this.onChange); - PieceListStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - - fetchSelectedPieceEditionList() { - let filteredPieceIdList = Object.keys(this.state.editionList) - .filter((pieceId) => { - return this.state.editionList[pieceId] - .filter((edition) => edition.selected).length > 0; - }); - return filteredPieceIdList; - }, - - fetchSelectedEditionList() { - let selectedEditionList = []; - - Object - .keys(this.state.editionList) - .forEach((pieceId) => { - let filteredEditionsForPiece = this.state.editionList[pieceId].filter((edition) => edition.selected); - selectedEditionList = selectedEditionList.concat(filteredEditionsForPiece); - }); - - return selectedEditionList; + availableAcls: React.PropTypes.object.isRequired, + className: React.PropTypes.string, + selectedEditions: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]), + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]) }, clearAllSelections() { @@ -82,22 +29,8 @@ let PieceListBulkModal = React.createClass({ EditionListActions.closeAllEditionLists(); }, - handleSuccess() { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); - - this.fetchSelectedPieceEditionList() - .forEach((pieceId) => { - EditionListActions.refreshEditionList({pieceId, filterBy: {}}); - }); - EditionListActions.clearAllEditionSelections(); - }, - render() { - let selectedEditions = this.fetchSelectedEditionList(); - let availableAcls = getAvailableAcls(selectedEditions, (aclName) => aclName !== 'acl_view'); - - if(Object.keys(availableAcls).length > 0) { + if (Object.keys(this.props.availableAcls).length) { return (
@@ -106,7 +39,7 @@ let PieceListBulkModal = React.createClass({
+ numberOfSelectedEditions={this.props.selectedEditions.length} />          

- - - + {this.props.children}
@@ -132,7 +57,6 @@ let PieceListBulkModal = React.createClass({ } else { return null; } - } }); diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js index 38de2af6..c463330c 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js @@ -28,7 +28,7 @@ let PieceListToolbarFilterWidget = React.createClass({ }, generateFilterByStatement(param) { - let filterBy = this.props.filterBy; + const filterBy = Object.assign({}, this.props.filterBy); if(filterBy) { // we need hasOwnProperty since the values are all booleans @@ -56,13 +56,13 @@ let PieceListToolbarFilterWidget = React.createClass({ */ filterBy(param) { return () => { - let filterBy = this.generateFilterByStatement(param); + const filterBy = this.generateFilterByStatement(param); this.props.applyFilterBy(filterBy); }; }, isFilterActive() { - let trueValuesOnly = Object.keys(this.props.filterBy).filter((acl) => acl); + const trueValuesOnly = Object.keys(this.props.filterBy).filter((acl) => acl); // We're hiding the star in that complicated matter so that, // the surrounding button is not resized up on appearance @@ -74,7 +74,7 @@ let PieceListToolbarFilterWidget = React.createClass({ }, render() { - let filterIcon = ( + const filterIcon = ( * @@ -140,4 +140,4 @@ let PieceListToolbarFilterWidget = React.createClass({ } }); -export default PieceListToolbarFilterWidget; \ No newline at end of file +export default PieceListToolbarFilterWidget; diff --git a/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js b/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js index b2d552a7..0eb4ad8f 100644 --- a/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js +++ b/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js @@ -1,7 +1,7 @@ 'use strict'; import React from 'react'; -import { History } from 'react-router'; +import { History, RouteContext } from 'react-router'; import UserStore from '../../../stores/user_store'; import UserActions from '../../../actions/user_actions'; @@ -37,7 +37,9 @@ export default function AuthProxyHandler({to, when}) { location: object }, - mixins: [History], + // We need insert `RouteContext` here in order to be able + // to use the `Lifecycle` widget in further down nested components + mixins: [History, RouteContext], getInitialState() { return UserStore.getState(); diff --git a/js/components/ascribe_settings/settings_container.js b/js/components/ascribe_settings/settings_container.js index 5b05e708..35a6fbe5 100644 --- a/js/components/ascribe_settings/settings_container.js +++ b/js/components/ascribe_settings/settings_container.js @@ -11,6 +11,7 @@ import WhitelabelActions from '../../actions/whitelabel_actions'; import AccountSettings from './account_settings'; import BitcoinWalletSettings from './bitcoin_wallet_settings'; import APISettings from './api_settings'; +import WebhookSettings from './webhook_settings'; import AclProxy from '../acl_proxy'; @@ -70,6 +71,7 @@ let SettingsContainer = React.createClass({ aclName="acl_view_settings_api"> + diff --git a/js/components/ascribe_settings/webhook_settings.js b/js/components/ascribe_settings/webhook_settings.js new file mode 100644 index 00000000..9deecbcd --- /dev/null +++ b/js/components/ascribe_settings/webhook_settings.js @@ -0,0 +1,165 @@ +'use strict'; + +import React from 'react'; + +import WebhookStore from '../../stores/webhook_store'; +import WebhookActions from '../../actions/webhook_actions'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import Form from '../ascribe_forms/form'; +import Property from '../ascribe_forms/property'; + +import AclProxy from '../acl_proxy'; + +import ActionPanel from '../ascribe_panel/action_panel'; +import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph'; + +import ApiUrls from '../../constants/api_urls'; +import AscribeSpinner from '../ascribe_spinner'; + +import { getLangText } from '../../utils/lang_utils'; + + +let WebhookSettings = React.createClass({ + propTypes: { + defaultExpanded: React.PropTypes.bool + }, + + getInitialState() { + return WebhookStore.getState(); + }, + + componentDidMount() { + WebhookStore.listen(this.onChange); + WebhookActions.fetchWebhooks(); + WebhookActions.fetchWebhookEvents(); + }, + + componentWillUnmount() { + WebhookStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + onRemoveWebhook(webhookId) { + return (event) => { + WebhookActions.removeWebhook(webhookId); + + let notification = new GlobalNotificationModel(getLangText('Webhook deleted'), 'success', 2000); + GlobalNotificationActions.appendGlobalNotification(notification); + }; + }, + + handleCreateSuccess() { + this.refs.webhookCreateForm.reset(); + WebhookActions.fetchWebhooks(true); + let notification = new GlobalNotificationModel(getLangText('Webhook successfully created'), 'success', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + getWebhooks(){ + let content = ; + + if (this.state.webhooks) { + content = this.state.webhooks.map(function(webhook, i) { + const event = webhook.event.split('.')[0]; + return ( + +
+ {event.toUpperCase()} +
+
+ {webhook.target} +
+
+ } + buttons={ +
+
+ +
+
+ }/> + ); + }, this); + } + return content; + }, + + getEvents() { + if (this.state.webhookEvents && this.state.webhookEvents.length) { + return ( + + + ); + } + return null; + }, + + + render() { + return ( + +
+

+ Webhooks allow external services to receive notifications from ascribe. + Currently we support webhook notifications when someone transfers, consigns, loans or shares + (by email) a work to you. +

+

+ To get started, simply choose the prefered action that you want to be notified upon and supply + a target url. +

+
+ +
+ { this.getEvents() } + + + +
+
+
+ {this.getWebhooks()} +
+ ); + } +}); + +export default WebhookSettings; \ No newline at end of file diff --git a/js/components/ascribe_slides_container/slides_container.js b/js/components/ascribe_slides_container/slides_container.js index 8ed66c1d..39d515a3 100644 --- a/js/components/ascribe_slides_container/slides_container.js +++ b/js/components/ascribe_slides_container/slides_container.js @@ -1,7 +1,7 @@ 'use strict'; import React from 'react/addons'; -import { History } from 'react-router'; +import { History, Lifecycle } from 'react-router'; import SlidesContainerBreadcrumbs from './slides_container_breadcrumbs'; @@ -17,14 +17,16 @@ const SlidesContainer = React.createClass({ pending: string, complete: string }), - location: object + location: object, + pageExitWarning: string }, - mixins: [History], + mixins: [History, Lifecycle], getInitialState() { return { - containerWidth: 0 + containerWidth: 0, + pageExitWarning: null }; }, @@ -41,6 +43,10 @@ const SlidesContainer = React.createClass({ window.removeEventListener('resize', this.handleContainerResize); }, + routerWillLeave() { + return this.props.pageExitWarning; + }, + handleContainerResize() { this.setState({ // +30 to get rid of the padding of the container which is 15px + 15px left and right diff --git a/js/components/ascribe_social_share/facebook_share_button.js b/js/components/ascribe_social_share/facebook_share_button.js index 87a2aef6..aa0b6691 100644 --- a/js/components/ascribe_social_share/facebook_share_button.js +++ b/js/components/ascribe_social_share/facebook_share_button.js @@ -8,7 +8,6 @@ import { InjectInHeadUtils } from '../../utils/inject_utils'; let FacebookShareButton = React.createClass({ propTypes: { - url: React.PropTypes.string, type: React.PropTypes.string }, @@ -28,12 +27,14 @@ let FacebookShareButton = React.createClass({ * To circumvent this, we always have the sdk parse the entire DOM on the initial load * (see FacebookHandler) and then use FB.XFBML.parse() on the mounting component later. */ - if (!InjectInHeadUtils.isPresent('script', AppConstants.facebook.sdkUrl)) { - InjectInHeadUtils.inject(AppConstants.facebook.sdkUrl); - } else { - // Parse() searches the children of the element we give it, not the element itself. - FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement); - } + + InjectInHeadUtils + .inject(AppConstants.facebook.sdkUrl) + .then(() => { FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement) }); + }, + + shouldComponentUpdate(nextProps) { + return this.props.type !== nextProps.type; }, render() { @@ -41,7 +42,6 @@ let FacebookShareButton = React.createClass({ ); diff --git a/js/components/ascribe_spinner.js b/js/components/ascribe_spinner.js index e1daf5b2..ad97d484 100644 --- a/js/components/ascribe_spinner.js +++ b/js/components/ascribe_spinner.js @@ -7,26 +7,26 @@ let AscribeSpinner = React.createClass({ propTypes: { classNames: React.PropTypes.string, size: React.PropTypes.oneOf(['sm', 'md', 'lg']), - color: React.PropTypes.oneOf(['blue', 'dark-blue', 'light-blue', 'pink', 'black', 'loop']) + color: React.PropTypes.oneOf(['black', 'blue', 'dark-blue', 'light-blue', 'pink', 'white', 'loop']) }, getDefaultProps() { return { inline: false, - size: 'md', - color: 'loop' + size: 'md' }; }, render() { + const { classNames: classes, color, size } = this.props; + return (
-
-
A
+ className={classNames('spinner-wrapper-' + size, + color ? 'spinner-wrapper-' + color : null, + classes)}> +
+
A
); } diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js index 3c993aea..9ad1facb 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js @@ -23,7 +23,6 @@ let FileDragAndDrop = React.createClass({ onDrop: React.PropTypes.func.isRequired, onDragOver: React.PropTypes.func, - onInactive: React.PropTypes.func, handleDeleteFile: React.PropTypes.func, handleCancelFile: React.PropTypes.func, handlePauseFile: React.PropTypes.func, @@ -70,28 +69,21 @@ let FileDragAndDrop = React.createClass({ handleDrop(event) { event.preventDefault(); event.stopPropagation(); - let files; - if(this.props.dropzoneInactive) { - // if there is a handle function for doing stuff - // when the dropzone is inactive, then call it - if(this.props.onInactive) { - this.props.onInactive(); + if (!this.props.dropzoneInactive) { + let files; + + // handle Drag and Drop + if(event.dataTransfer && event.dataTransfer.files.length > 0) { + files = event.dataTransfer.files; + } else if(event.target.files) { // handle input type file + files = event.target.files; } - return; - } - // handle Drag and Drop - if(event.dataTransfer && event.dataTransfer.files.length > 0) { - files = event.dataTransfer.files; - } else if(event.target.files) { // handle input type file - files = event.target.files; + if(typeof this.props.onDrop === 'function' && files) { + this.props.onDrop(files); + } } - - if(typeof this.props.onDrop === 'function' && files) { - this.props.onDrop(files); - } - }, handleDeleteFile(fileId) { @@ -123,31 +115,25 @@ let FileDragAndDrop = React.createClass({ }, handleOnClick() { - let evt; - // when multiple is set to false and the user already uploaded a piece, - // do not propagate event - if(this.props.dropzoneInactive) { - // if there is a handle function for doing stuff - // when the dropzone is inactive, then call it - if(this.props.onInactive) { - this.props.onInactive(); + // do not propagate event if the drop zone's inactive, + // for example when multiple is set to false and the user already uploaded a piece + if (!this.props.dropzoneInactive) { + let evt; + + try { + evt = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); + } catch(e) { + // For browsers that do not support the new MouseEvent syntax + evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); } - return; - } - try { - evt = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - } catch(e) { - // For browsers that do not support the new MouseEvent syntax - evt = document.createEvent('MouseEvents'); - evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + this.refs.fileSelector.getDOMNode().dispatchEvent(evt); } - - this.refs.fileSelector.getDOMNode().dispatchEvent(evt); }, getErrorDialog(failedFiles) { diff --git a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js index aabf19d9..94c85f4f 100644 --- a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js +++ b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js @@ -32,6 +32,20 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = handleDeleteFile: func }, + getInitialState() { + return { + disabled: this.getUploadingFiles().length !== 0 + }; + }, + + componentWillReceiveProps(nextProps) { + if(this.props.filesToUpload !== nextProps.filesToUpload) { + this.setState({ + disabled: this.getUploadingFiles(nextProps.filesToUpload).length !== 0 + }); + } + }, + handleDrop(event) { event.preventDefault(); event.stopPropagation(); @@ -42,43 +56,62 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = } }, - getUploadingFiles() { - return this.props.filesToUpload.filter((file) => file.status === FileStatus.UPLOADING); + getUploadingFiles(filesToUpload = this.props.filesToUpload) { + return filesToUpload.filter((file) => file.status === FileStatus.UPLOADING); }, getUploadedFile() { return this.props.filesToUpload.filter((file) => file.status === FileStatus.UPLOAD_SUCESSFUL)[0]; }, + clearSelection() { + this.refs.fileSelector.getDOMNode().value = ''; + }, + handleOnClick() { - const uploadingFiles = this.getUploadingFiles(); - const uploadedFile = this.getUploadedFile(); + if(!this.state.disabled) { + let evt; + const uploadingFiles = this.getUploadingFiles(); + const uploadedFile = this.getUploadedFile(); - if(uploadedFile) { - this.props.handleCancelFile(uploadedFile.id); - } - if(uploadingFiles.length === 0) { - // We only want the button to be clickable if there are no files currently uploading - - // Firefox only recognizes the simulated mouse click if bubbles is set to true, - // but since Google Chrome propagates the event much further than needed, we - // need to stop propagation as soon as the event is created - var evt = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); + this.clearSelection(); + if(uploadingFiles.length) { + this.props.handleCancelFile(uploadingFiles[0].id); + } else if(uploadedFile && !uploadedFile.s3UrlSafe) { + this.props.handleCancelFile(uploadedFile.id); + } else if(uploadedFile && uploadedFile.s3UrlSafe) { + this.props.handleDeleteFile(uploadedFile.id); + } + try { + evt = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); + } catch(e) { + // For browsers that do not support the new MouseEvent syntax + evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + } evt.stopPropagation(); - this.refs.fileinput.getDOMNode().dispatchEvent(evt); + this.refs.fileSelector.getDOMNode().dispatchEvent(evt); } }, + onClickCancel() { + this.clearSelection(); + const uploadingFile = this.getUploadingFiles()[0]; + this.props.handleCancelFile(uploadingFile.id); + }, + onClickRemove() { + this.clearSelection(); const uploadedFile = this.getUploadedFile(); this.props.handleDeleteFile(uploadedFile.id); }, + getButtonLabel() { let { filesToUpload, fileClassToUpload } = this.props; @@ -94,8 +127,16 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = getUploadedFileLabel() { const uploadedFile = this.getUploadedFile(); + const uploadingFiles = this.getUploadingFiles(); - if(uploadedFile) { + if(uploadingFiles.length) { + return ( + + {' ' + truncateTextAtCharIndex(uploadingFiles[0].name, 40) + ' '} + [{getLangText('cancel upload')}] + + ); + } else if(uploadedFile) { return ( @@ -111,8 +152,11 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = }, render() { - let { multiple, - allowedExtensions } = this.props; + const { + multiple, + allowedExtensions } = this.props; + const { disabled } = this.state; + /* * We do not want a button that submits here. @@ -122,14 +166,19 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = */ return (
- + disabled={disabled}> {this.getButtonLabel()} - + {this.getUploadedFileLabel()}
); diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index a9dd1039..b11a877f 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -47,7 +47,6 @@ const ReactS3FineUploader = React.createClass({ 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 - onInactive: func, // for when the user does something while the uploader's inactive // Handle form validation setIsUploadReady: func, //TODO: rename to setIsUploaderValidated @@ -317,7 +316,7 @@ const ReactS3FineUploader = React.createClass({ // Cancel uploads and clear previously selected files on the input element cancelUploads(id) { - !!id ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll(); + typeof id !== 'undefined' ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll(); // Reset the file input element to clear the previously selected files so that // the user can reselect them again. @@ -425,11 +424,13 @@ const ReactS3FineUploader = React.createClass({ if(fileId < filesToUpload.length) { const changeSet = { $set: url }; - const newFilesToUpload = React.addons.update(filesToUpload, { [fileId]: { thumbnailUrl: changeSet } }); + const newFilesToUpload = React.addons.update(filesToUpload, { + [fileId]: { thumbnailUrl: changeSet } + }); this.setState({ filesToUpload: newFilesToUpload }); } else { - throw new Error("You're accessing an index out of range of filesToUpload"); + throw new Error('Accessing an index out of range of filesToUpload'); } }, @@ -1052,13 +1053,12 @@ const ReactS3FineUploader = React.createClass({ render() { const { errorState: { errorClass }, filesToUpload, uploadInProgress } = this.state; const { - multiple, areAssetsDownloadable, areAssetsEditable, - onInactive, enableLocalHashing, fileClassToUpload, fileInputElement: FileInputElement, + multiple, showErrorPrompt, uploadMethod } = this.props; @@ -1069,7 +1069,6 @@ const ReactS3FineUploader = React.createClass({ multiple, areAssetsDownloadable, areAssetsEditable, - onInactive, enableLocalHashing, uploadMethod, fileClassToUpload, diff --git a/js/components/contract_notification.js b/js/components/contract_notification.js deleted file mode 100644 index cd6ceb53..00000000 --- a/js/components/contract_notification.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -import React from 'react'; - -import NotificationStore from '../stores/notification_store'; - -import { mergeOptions } from '../utils/general_utils'; - -let ContractNotification = React.createClass({ - getInitialState() { - return mergeOptions( - NotificationStore.getState() - ); - }, - - componentDidMount() { - NotificationStore.listen(this.onChange); - }, - - componentWillUnmount() { - NotificationStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - - render() { - - return ( - null - ); - } -}); - -export default ContractNotification; \ No newline at end of file diff --git a/js/components/error_not_found_page.js b/js/components/error_not_found_page.js index 61f83196..0e111ce7 100644 --- a/js/components/error_not_found_page.js +++ b/js/components/error_not_found_page.js @@ -6,6 +6,16 @@ import { getLangText } from '../utils/lang_utils'; let ErrorNotFoundPage = React.createClass({ + propTypes: { + message: React.PropTypes.string + }, + + getDefaultProps() { + return { + message: getLangText("Oops, the page you are looking for doesn't exist.") + }; + }, + render() { return (
@@ -13,7 +23,7 @@ let ErrorNotFoundPage = React.createClass({

404

- {getLangText('Ups, the page you are looking for does not exist.')} + {this.props.message}

diff --git a/js/components/footer.js b/js/components/footer.js index 65088ee2..31145d4b 100644 --- a/js/components/footer.js +++ b/js/components/footer.js @@ -11,6 +11,7 @@ let Footer = React.createClass({


api | + {getLangText('faq')} | {getLangText('imprint')} | {getLangText('terms of service')} | {getLangText('privacy')} diff --git a/js/components/global_notification.js b/js/components/global_notification.js index 59663b28..c1477f67 100644 --- a/js/components/global_notification.js +++ b/js/components/global_notification.js @@ -1,7 +1,9 @@ 'use strict'; import React from 'react'; +import classNames from 'classnames'; +import GlobalNotificationActions from '../actions/global_notification_actions'; import GlobalNotificationStore from '../stores/global_notification_store'; import Row from 'react-bootstrap/lib/Row'; @@ -9,14 +11,18 @@ import Col from 'react-bootstrap/lib/Col'; import { mergeOptions } from '../utils/general_utils'; +const MAX_NOTIFICATION_BUBBLE_CONTAINER_WIDTH = 768; + let GlobalNotification = React.createClass({ getInitialState() { + const notificationStore = GlobalNotificationStore.getState(); + return mergeOptions( { containerWidth: 0 }, - this.extractFirstElem(GlobalNotificationStore.getState().notificationQue) + notificationStore ); }, @@ -36,35 +42,8 @@ let GlobalNotification = React.createClass({ window.removeEventListener('resize', this.handleContainerResize); }, - extractFirstElem(l) { - if(l.length > 0) { - return { - show: true, - message: l[0] - }; - } else { - return { - show: false, - message: '' - }; - } - }, - onChange(state) { - let notification = this.extractFirstElem(state.notificationQue); - - // error handling for notifications - if(notification.message && notification.type === 'danger') { - console.logGlobal(new Error(notification.message.message)); - } - - if(notification.show) { - this.setState(notification); - } else { - this.setState({ - show: false - }); - } + this.setState(state); }, handleContainerResize() { @@ -73,32 +52,31 @@ let GlobalNotification = React.createClass({ }); }, - render() { - let notificationClass = 'ascribe-global-notification'; - let textClass; + renderNotification() { + const { + notificationQueue: [notification], + notificationStatus, + notificationsPaused, + containerWidth } = this.state; - if(this.state.containerWidth > 768) { - notificationClass = 'ascribe-global-notification-bubble'; - - if(this.state.show) { - notificationClass += ' ascribe-global-notification-bubble-on'; - } else { - notificationClass += ' ascribe-global-notification-bubble-off'; - } + const notificationClasses = []; + if (this.state.containerWidth > 768) { + notificationClasses.push('ascribe-global-notification-bubble'); + notificationClasses.push(notificationStatus === 'show' ? 'ascribe-global-notification-bubble-on' + : 'ascribe-global-notification-bubble-off'); } else { - notificationClass = 'ascribe-global-notification'; - - if(this.state.show) { - notificationClass += ' ascribe-global-notification-on'; - } else { - notificationClass += ' ascribe-global-notification-off'; - } - + notificationClasses.push('ascribe-global-notification'); + notificationClasses.push(notificationStatus === 'show' ? 'ascribe-global-notification-on' + : 'ascribe-global-notification-off'); } - if(this.state.message) { - switch(this.state.message.type) { + let textClass; + let message; + if (notification && !notificationsPaused) { + message = notification.message; + + switch(notification.type) { case 'success': textClass = 'ascribe-global-notification-success'; break; @@ -106,18 +84,23 @@ let GlobalNotification = React.createClass({ textClass = 'ascribe-global-notification-danger'; break; default: - console.warn('Could not find a matching type in global_notification.js'); + console.warn('Could not find a matching notification type in global_notification.js'); } } - + return ( +

+
{message}
+
+ ); + }, + + render() { return (
-
-
{this.state.message.message}
-
+ {this.renderNotification()}
@@ -125,4 +108,4 @@ let GlobalNotification = React.createClass({ } }); -export default GlobalNotification; \ No newline at end of file +export default GlobalNotification; diff --git a/js/components/header.js b/js/components/header.js index 51f91318..b3fd543e 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -1,9 +1,10 @@ 'use strict'; import React from 'react'; - import { Link } from 'react-router'; +import history from '../history'; + import Nav from 'react-bootstrap/lib/Nav'; import Navbar from 'react-bootstrap/lib/Navbar'; import CollapsibleNav from 'react-bootstrap/lib/CollapsibleNav'; @@ -58,11 +59,17 @@ let Header = React.createClass({ UserStore.listen(this.onChange); WhitelabelActions.fetchWhitelabel(); WhitelabelStore.listen(this.onChange); + + // react-bootstrap 0.25.1 has a bug in which it doesn't + // close the mobile expanded navigation after a click by itself. + // To get rid of this, we set the state of the component ourselves. + history.listen(this.onRouteChange); }, componentWillUnmount() { UserStore.unlisten(this.onChange); WhitelabelStore.unlisten(this.onChange); + //history.unlisten(this.onRouteChange); }, getLogo() { @@ -135,6 +142,13 @@ let Header = React.createClass({ this.refs.dropdownbutton.setDropdownState(false); }, + // On route change, close expanded navbar again since react-bootstrap doesn't close + // the collapsibleNav by itself on click. setState() isn't available on a ref so + // doing this explicitly is the only way for now. + onRouteChange() { + this.refs.navbar.state.navExpanded = false; + }, + render() { let account; let signup; @@ -201,8 +215,10 @@ let Header = React.createClass({ - + fixedTop={true} + ref="navbar"> + diff --git a/js/components/piece_list.js b/js/components/piece_list.js index 3d4309f8..9424117c 100644 --- a/js/components/piece_list.js +++ b/js/components/piece_list.js @@ -13,6 +13,9 @@ import AccordionList from './ascribe_accordion_list/accordion_list'; import AccordionListItemWallet from './ascribe_accordion_list/accordion_list_item_wallet'; import AccordionListItemTableEditions from './ascribe_accordion_list/accordion_list_item_table_editions'; +import AclButtonList from './ascribe_buttons/acl_button_list.js'; +import DeleteButton from './ascribe_buttons/delete_button'; + import Pagination from './ascribe_pagination/pagination'; import PieceListFilterDisplay from './piece_list_filter_display'; @@ -22,7 +25,8 @@ import PieceListToolbar from './ascribe_piece_list_toolbar/piece_list_toolbar'; import AscribeSpinner from './ascribe_spinner'; -import { mergeOptions } from '../utils/general_utils'; +import { getAvailableAcls } from '../utils/acl_utils'; +import { mergeOptions, isShallowEqual } from '../utils/general_utils'; import { getLangText } from '../utils/lang_utils'; import { setDocumentTitle } from '../utils/dom_utils'; @@ -30,8 +34,11 @@ import { setDocumentTitle } from '../utils/dom_utils'; let PieceList = React.createClass({ propTypes: { accordionListItemType: React.PropTypes.func, + bulkModalButtonListType: React.PropTypes.func, + canLoadPieceList: React.PropTypes.bool, redirectTo: React.PropTypes.string, customSubmitButton: React.PropTypes.element, + customThumbnailPlaceholder: React.PropTypes.func, filterParams: React.PropTypes.array, orderParams: React.PropTypes.array, orderBy: React.PropTypes.string, @@ -43,6 +50,8 @@ let PieceList = React.createClass({ getDefaultProps() { return { accordionListItemType: AccordionListItemWallet, + bulkModalButtonListType: AclButtonList, + canLoadPieceList: true, orderParams: ['artist_name', 'title'], filterParams: [{ label: getLangText('Show works I can'), @@ -54,23 +63,53 @@ let PieceList = React.createClass({ }] }; }, + getInitialState() { - return mergeOptions( - PieceListStore.getState(), - EditionListStore.getState() + const pieceListStore = PieceListStore.getState(); + const stores = mergeOptions( + pieceListStore, + EditionListStore.getState(), + { + isFilterDirty: false + } ); + + // Use the default filters but use the pieceListStore's settings if they're available + stores.filterBy = Object.assign(this.getDefaultFilterBy(), pieceListStore.filterBy); + + return stores; }, componentDidMount() { - let page = this.props.location.query.page || 1; - PieceListStore.listen(this.onChange); EditionListStore.listen(this.onChange); - let orderBy = this.props.orderBy ? this.props.orderBy : this.state.orderBy; - if (this.state.pieceList.length === 0 || this.state.page !== page){ - PieceListActions.fetchPieceList(page, this.state.pageSize, this.state.search, - orderBy, this.state.orderAsc, this.state.filterBy); + let page = this.props.location.query.page || 1; + if (this.props.canLoadPieceList && (this.state.pieceList.length === 0 || this.state.page !== page)) { + this.loadPieceList({ page }); + } + }, + + componentWillReceiveProps(nextProps) { + let filterBy; + let page = this.props.location.query.page || 1; + + // If the user hasn't changed the filter and the new default filter is different + // than the current filter, apply the new default filter + if (!this.state.isFilterDirty) { + const newDefaultFilterBy = this.getDefaultFilterBy(nextProps); + + // Only need to check shallowly since the filterBy shouldn't be nested + if (!isShallowEqual(this.state.filterBy, newDefaultFilterBy)) { + filterBy = newDefaultFilterBy; + page = 1; + } + } + + // Only load if we are applying a new filter or if it's the first time we can + // load the piece list + if (nextProps.canLoadPieceList && (filterBy || !this.props.canLoadPieceList)) { + this.loadPieceList({ page, filterBy }); } }, @@ -90,14 +129,29 @@ let PieceList = React.createClass({ this.setState(state); }, + getDefaultFilterBy(props = this.props) { + const { filterParams } = props; + const defaultFilterBy = {}; + + if (filterParams && typeof filterParams.forEach === 'function') { + filterParams.forEach(({ items }) => { + items.forEach((item) => { + if (typeof item === 'object' && item.defaultValue) { + defaultFilterBy[item.key] = true; + } + }); + }); + } + + return defaultFilterBy; + }, + paginationGoToPage(page) { return () => { // if the users clicks a pager of the pagination, // the site should go to the top document.body.scrollTop = document.documentElement.scrollTop = 0; - PieceListActions.fetchPieceList(page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, - this.state.filterBy); + this.loadPieceList({ page }); }; }, @@ -116,29 +170,35 @@ let PieceList = React.createClass({ }, searchFor(searchTerm) { - PieceListActions.fetchPieceList(1, this.state.pageSize, searchTerm, this.state.orderBy, - this.state.orderAsc, this.state.filterBy); - this.history.pushState(null, this.props.location.pathname, {page: 1}); + this.loadPieceList({ + page: 1, + search: searchTerm + }); + this.history.pushState(null, this.props.location.pathname, {page: 1}); }, applyFilterBy(filterBy){ - // first we need to apply the filter on the piece list - PieceListActions.fetchPieceList(1, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, filterBy) - .then(() => { - // but also, we need to filter all the open edition lists - this.state.pieceList - .forEach((piece) => { - // but only if they're actually open - if(this.state.isEditionListOpenForPieceId[piece.id].show) { - EditionListActions.refreshEditionList({ - pieceId: piece.id, - filterBy - }); - } + this.setState({ + isFilterDirty: true + }); - }); - }); + // first we need to apply the filter on the piece list + this + .loadPieceList({ page: 1, filterBy }) + .then(() => { + // but also, we need to filter all the open edition lists + this.state.pieceList + .forEach((piece) => { + // but only if they're actually open + if(this.state.isEditionListOpenForPieceId[piece.id].show) { + EditionListActions.refreshEditionList({ + pieceId: piece.id, + filterBy + }); + } + + }); + }); // we have to redirect the user always to page one as it could be that there is no page two // for filtered pieces @@ -150,35 +210,97 @@ let PieceList = React.createClass({ orderBy, this.state.orderAsc, this.state.filterBy); }, + loadPieceList({ page, filterBy = this.state.filterBy, search = this.state.search }) { + const orderBy = this.state.orderBy || this.props.orderBy; + + return PieceListActions.fetchPieceList(page, this.state.pageSize, search, + orderBy, this.state.orderAsc, filterBy); + }, + + fetchSelectedPieceEditionList() { + let filteredPieceIdList = Object.keys(this.state.editionList) + .filter((pieceId) => { + return this.state.editionList[pieceId] + .filter((edition) => edition.selected).length > 0; + }); + return filteredPieceIdList; + }, + + fetchSelectedEditionList() { + let selectedEditionList = []; + + Object + .keys(this.state.editionList) + .forEach((pieceId) => { + let filteredEditionsForPiece = this.state.editionList[pieceId].filter((edition) => edition.selected); + selectedEditionList = selectedEditionList.concat(filteredEditionsForPiece); + }); + + return selectedEditionList; + }, + + handleAclSuccess() { + PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, + this.state.orderBy, this.state.orderAsc, this.state.filterBy); + + this.fetchSelectedPieceEditionList() + .forEach((pieceId) => { + EditionListActions.refreshEditionList({pieceId}); + }); + EditionListActions.clearAllEditionSelections(); + }, + render() { - let loadingElement = ; - let AccordionListItemType = this.props.accordionListItemType; + const { + accordionListItemType: AccordionListItemType, + bulkModalButtonListType: BulkModalButtonListType, + customSubmitButton, + customThumbnailPlaceholder, + filterParams, + orderParams } = this.props; + + const loadingElement = ; + + const selectedEditions = this.fetchSelectedEditionList(); + const availableAcls = getAvailableAcls(selectedEditions, (aclName) => aclName !== 'acl_view'); setDocumentTitle(getLangText('Collection')); - return (
- {this.props.customSubmitButton ? - this.props.customSubmitButton : + {customSubmitButton ? + customSubmitButton : } - + + + + + + filterParams={filterParams}/>
diff --git a/js/components/register_piece.js b/js/components/register_piece.js index 322b9934..8211e91e 100644 --- a/js/components/register_piece.js +++ b/js/components/register_piece.js @@ -44,11 +44,8 @@ let RegisterPiece = React.createClass( { return mergeOptions( UserStore.getState(), WhitelabelStore.getState(), - PieceListStore.getState(), - { - selectedLicense: 0, - isFineUploaderActive: false - }); + PieceListStore.getState() + ); }, componentDidMount() { @@ -66,13 +63,6 @@ let RegisterPiece = React.createClass( { onChange(state) { this.setState(state); - - if(this.state.currentUser && this.state.currentUser.email) { - // we should also make the fineuploader component editable again - this.setState({ - isFineUploaderActive: true - }); - } }, handleSuccess(response){ @@ -118,7 +108,7 @@ let RegisterPiece = React.createClass( { {this.props.children} diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_login_container.js b/js/components/whitelabel/prize/portfolioreview/components/pr_login_container.js new file mode 100644 index 00000000..e69de29b diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js b/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js index a2a70a97..0fbca419 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js @@ -63,6 +63,11 @@ const PRRegisterPiece = React.createClass({

Portfolio Review

{getLangText('Submission closing on %s', ' 22 Dec 2015')}

+

For more information, visit:  + + portfolio-review.de + +

{getLangText("You're submitting as %s. ", currentUser.email)} {getLangText('Change account?')} diff --git a/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js b/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js index 93ca50f3..982af7b0 100644 --- a/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js +++ b/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js @@ -51,8 +51,7 @@ import { setDocumentTitle } from '../../../../../../utils/dom_utils'; */ let PieceContainer = React.createClass({ propTypes: { - params: React.PropTypes.object, - location: React.PropTypes.object + params: React.PropTypes.object }, getInitialState() { @@ -111,7 +110,7 @@ let PieceContainer = React.createClass({ }, render() { - if(this.state.piece && this.state.piece.title) { + if(this.state.piece && this.state.piece.id) { /* This really needs a refactor! @@ -162,7 +161,7 @@ let PieceContainer = React.createClass({ piece={this.state.piece} currentUser={this.state.currentUser}/> }> - + ); } else { @@ -292,8 +291,8 @@ let PrizePieceRatings = React.createClass({ url={ApiUrls.ownership_loans_pieces_request} email={this.props.currentUser.email} gallery={this.props.piece.prize.name} - startdate={today} - enddate={endDate} + startDate={today} + endDate={endDate} showPersonalMessage={true} showPassword={false} handleSuccess={this.handleLoanSuccess} /> @@ -426,8 +425,7 @@ let PrizePieceRatings = React.createClass({ let PrizePieceDetails = React.createClass({ propTypes: { - piece: React.PropTypes.object, - location: React.PropTypes.object + piece: React.PropTypes.object }, render() { @@ -464,8 +462,7 @@ let PrizePieceDetails = React.createClass({ overrideForm={true} pieceId={this.props.piece.id} otherData={this.props.piece.other_data} - multiple={true} - location={location}/> + multiple={true} /> ); diff --git a/js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js b/js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js new file mode 100644 index 00000000..f360c932 --- /dev/null +++ b/js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js @@ -0,0 +1,15 @@ +'use strict' + +import React from 'react'; + +let Vivi23AccordionListItemThumbnailPlaceholder = React.createClass({ + render() { + return ( + + 23 + + ); + } +}); + +export default Vivi23AccordionListItemThumbnailPlaceholder; diff --git a/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js b/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js new file mode 100644 index 00000000..302495a0 --- /dev/null +++ b/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js @@ -0,0 +1,78 @@ +'use strict'; + +import React from 'react'; + +import Button from 'react-bootstrap/lib/Button'; +import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; + +import WhitelabelActions from '../../../../../actions/whitelabel_actions'; +import WhitelabelStore from '../../../../../stores/whitelabel_store'; + +import { mergeOptions } from '../../../../../utils/general_utils'; +import { getLangText } from '../../../../../utils/lang_utils'; +import { setDocumentTitle } from '../../../../../utils/dom_utils'; + +let Vivi23Landing = React.createClass({ + getInitialState() { + return WhitelabelStore.getState(); + }, + + componentWillMount() { + setDocumentTitle('23VIVI Marketplace'); + }, + + componentDidMount() { + WhitelabelStore.listen(this.onChange); + WhitelabelActions.fetchWhitelabel(); + }, + + componentWillUnmount() { + WhitelabelStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + return ( +

+
+
+
+ +
+ {getLangText('Artwork from the 23VIVI Marketplace is powered by') + ' '} + +
+
+
+
+

+ {getLangText('Existing ascribe user?')} +

+ + + +
+
+

+ {getLangText('Do you need an account?')} +

+ + + +
+
+
+
+
+ ); + } +}); + +export default Vivi23Landing; diff --git a/js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js b/js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js new file mode 100644 index 00000000..0bfb8aa0 --- /dev/null +++ b/js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js @@ -0,0 +1,24 @@ +'use strict' + +import React from 'react'; + +import Vivi23AccordionListItemThumbnailPlaceholder from './23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder'; + +import MarketPieceList from '../market/market_piece_list'; + +let Vivi23PieceList = React.createClass({ + propTypes: { + location: React.PropTypes.object + }, + + render() { + return ( + + ); + } + +}); + +export default Vivi23PieceList; diff --git a/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js b/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js index b263e517..26a186ca 100644 --- a/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js +++ b/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js @@ -30,7 +30,7 @@ let WalletPieceContainer = React.createClass({ render() { - if(this.props.piece && this.props.piece.title) { + if(this.props.piece && this.props.piece.id) { return ( + expanded={!disabled || !!piece.extra_data.artist_bio}> + expanded={!disabled || !!piece.extra_data.artist_contact_information}> + expanded={!disabled || !!piece.extra_data.conceptual_overview}> + expanded={!disabled || !!piece.extra_data.medium}> + expanded={!disabled || !!piece.extra_data.size_duration}> + expanded={!disabled || !!piece.extra_data.display_instructions}> + expanded={!disabled || !!piece.extra_data.additional_details}> +
-
+
diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js index 470da761..42b7c1ad 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js @@ -53,8 +53,6 @@ let CylandRegisterPiece = React.createClass({ PieceStore.getState(), WhitelabelStore.getState(), { - selectedLicense: 0, - isFineUploaderActive: false, step: 0 }); }, @@ -90,13 +88,6 @@ let CylandRegisterPiece = React.createClass({ onChange(state) { this.setState(state); - - if(this.state.currentUser && this.state.currentUser.email) { - // we should also make the fineuploader component editable again - this.setState({ - isFineUploaderActive: true - }); - } }, handleRegisterSuccess(response){ @@ -167,11 +158,6 @@ let CylandRegisterPiece = React.createClass({ } }, - // basically redirects to the second slide (index: 1), when the user is not logged in - onLoggedOut() { - this.history.pushState(null, '/login'); - }, - render() { let today = new Moment(); @@ -197,9 +183,8 @@ let CylandRegisterPiece = React.createClass({ enableLocalHashing={false} headerMessage={getLangText('Submit to Cyland Archive')} submitMessage={getLangText('Submit')} - isFineUploaderActive={this.state.isFineUploaderActive} + isFineUploaderActive={true} handleSuccess={this.handleRegisterSuccess} - onLoggedOut={this.onLoggedOut} location={this.props.location}/> @@ -229,8 +214,8 @@ let CylandRegisterPiece = React.createClass({ url={ApiUrls.ownership_loans_pieces} email={this.state.whitelabel.user} gallery="Cyland Archive" - startdate={today} - enddate={datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain} + startDate={today} + endDate={datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain} showStartDate={false} showEndDate={false} showPersonalMessage={false} diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js index 4e2f6a63..df58b7c7 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js @@ -123,7 +123,7 @@ let IkonotvPieceContainer = React.createClass({ ); } - if(this.state.piece && this.state.piece.title) { + if(this.state.piece && this.state.piece.id) { setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', ')); return ( + expanded={!this.props.disabled || !!this.props.piece.extra_data.artist_website}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.gallery_website}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.additional_websites}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.conceptual_overview}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.medium}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.size_duration}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.copyright}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.courtesy_of}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.copyright_of_photography}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.additional_details}> + location={this.props.location} + pageExitWarning={pageExitWarning}>
@@ -255,9 +250,8 @@ let IkonotvRegisterPiece = React.createClass({ enableLocalHashing={false} headerMessage={getLangText('Register work')} submitMessage={getLangText('Register')} - isFineUploaderActive={this.state.isFineUploaderActive} + isFineUploaderActive={true} handleSuccess={this.handleRegisterSuccess} - onLoggedOut={this.onLoggedOut} location={this.props.location}/> diff --git a/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js b/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js new file mode 100644 index 00000000..e68b1781 --- /dev/null +++ b/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js @@ -0,0 +1,84 @@ +'use strict'; + +import React from 'react'; + +import Button from 'react-bootstrap/lib/Button'; +import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; + +import WhitelabelActions from '../../../../../actions/whitelabel_actions'; +import WhitelabelStore from '../../../../../stores/whitelabel_store'; + +import { mergeOptions } from '../../../../../utils/general_utils'; +import { getLangText } from '../../../../../utils/lang_utils'; +import { setDocumentTitle } from '../../../../../utils/dom_utils'; + + +let LumenusLanding = React.createClass({ + + getInitialState() { + return mergeOptions( + WhitelabelStore.getState() + ); + }, + + componentWillMount() { + setDocumentTitle('Lumenus Marketplace'); + }, + + componentDidMount() { + WhitelabelStore.listen(this.onChange); + WhitelabelActions.fetchWhitelabel(); + }, + + componentWillUnmount() { + WhitelabelStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + return ( +
+
+
+
+ +
+ {getLangText('Artwork from the Lumenus Marketplace is powered by') + ' '} + + + +
+
+
+
+

+ {getLangText('Existing ascribe user?')} +

+ + + +
+
+

+ {getLangText('Do you need an account?')} +

+ + + +
+
+
+
+
+ ); + } +}); + +export default LumenusLanding; diff --git a/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js b/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js new file mode 100644 index 00000000..1dcdd4e5 --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js @@ -0,0 +1,74 @@ +'use strict'; + +import React from 'react'; + +import MarketSubmitButton from './market_submit_button'; + +import DeleteButton from '../../../../../ascribe_buttons/delete_button'; +import EmailButton from '../../../../../ascribe_buttons/acls/email_button'; +import TransferButton from '../../../../../ascribe_buttons/acls/transfer_button'; +import UnconsignButton from '../../../../../ascribe_buttons/acls/unconsign_button'; + +import UserActions from '../../../../../../actions/user_actions'; +import UserStore from '../../../../../../stores/user_store'; + +let MarketAclButtonList = React.createClass({ + propTypes: { + availableAcls: React.PropTypes.object.isRequired, + className: React.PropTypes.string, + pieceOrEditions: React.PropTypes.array, + handleSuccess: React.PropTypes.func, + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]) + }, + + getInitialState() { + return UserStore.getState(); + }, + + componentDidMount() { + UserStore.listen(this.onChange); + UserActions.fetchCurrentUser(); + }, + + componentWillUnmount() { + UserStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + let { availableAcls, className, pieceOrEditions, handleSuccess } = this.props; + return ( +
+ + + + + {this.props.children} +
+ ); + } +}); + +export default MarketAclButtonList; diff --git a/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js b/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js new file mode 100644 index 00000000..d8ef4c41 --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js @@ -0,0 +1,160 @@ +'use strict'; + +import React from 'react'; +import classNames from 'classnames'; + +import MarketAdditionalDataForm from '../market_forms/market_additional_data_form'; + +import AclFormFactory from '../../../../../ascribe_forms/acl_form_factory'; +import ConsignForm from '../../../../../ascribe_forms/form_consign'; + +import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper'; + +import AclProxy from '../../../../../acl_proxy'; + +import PieceActions from '../../../../../../actions/piece_actions'; +import WhitelabelActions from '../../../../../../actions/whitelabel_actions'; +import WhitelabelStore from '../../../../../../stores/whitelabel_store'; + +import ApiUrls from '../../../../../../constants/api_urls'; + +import { getAclFormMessage, getAclFormDataId } from '../../../../../../utils/form_utils'; +import { getLangText } from '../../../../../../utils/lang_utils'; + +let MarketSubmitButton = React.createClass({ + propTypes: { + availableAcls: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object, + editions: React.PropTypes.array.isRequired, + handleSuccess: React.PropTypes.func.isRequired, + className: React.PropTypes.string, + }, + + getInitialState() { + return WhitelabelStore.getState(); + }, + + componentDidMount() { + WhitelabelStore.listen(this.onChange); + + WhitelabelActions.fetchWhitelabel(); + }, + + componentWillUnmount() { + WhitelabelStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + canEditionBeSubmitted(edition) { + if (edition && edition.extra_data && edition.other_data) { + const { extra_data, other_data } = edition; + + if (extra_data.artist_bio && extra_data.work_description && + extra_data.technology_details && extra_data.display_instructions && + other_data.length > 0) { + return true; + } + } + + return false; + }, + + getFormDataId() { + return getAclFormDataId(false, this.props.editions); + }, + + getAggregateEditionDetails() { + const { editions } = this.props; + + // Currently, we only care if all the given editions are from the same parent piece + // and if they can be submitted + return editions.reduce((details, curEdition) => { + return { + solePieceId: details.solePieceId === curEdition.parent ? details.solePieceId : null, + canSubmit: details.canSubmit && this.canEditionBeSubmitted(curEdition) + }; + }, { + solePieceId: editions.length > 0 ? editions[0].parent : null, + canSubmit: this.canEditionBeSubmitted(editions[0]) + }); + }, + + handleAdditionalDataSuccess(pieceId) { + // Fetch newly updated piece to update the views + PieceActions.fetchOne(pieceId); + + this.refs.consignModal.show(); + }, + + render() { + const { availableAcls, currentUser, className, editions, handleSuccess } = this.props; + const { whitelabel: { name: whitelabelName = 'Market', user: whitelabelAdminEmail } } = this.state; + const { solePieceId, canSubmit } = this.getAggregateEditionDetails(); + const message = getAclFormMessage({ + aclName: 'acl_consign', + entities: editions, + isPiece: false, + additionalMessage: getLangText('Suggested price:'), + senderName: currentUser.username + }); + + const triggerButton = ( + + ); + const consignForm = ( + + ); + + if (solePieceId && !canSubmit) { + return ( + + + + + + + {consignForm} + + + ); + } else { + return ( + + + {consignForm} + + + ); + } + } +}); + +export default MarketSubmitButton; diff --git a/js/components/whitelabel/wallet/components/market/market_detail/market_edition_container.js b/js/components/whitelabel/wallet/components/market/market_detail/market_edition_container.js new file mode 100644 index 00000000..97284dbc --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_detail/market_edition_container.js @@ -0,0 +1,24 @@ +'use strict'; + +import React from 'react'; + +import MarketFurtherDetails from './market_further_details'; + +import MarketAclButtonList from '../market_buttons/market_acl_button_list'; + +import EditionContainer from '../../../../../ascribe_detail/edition_container'; + +let MarketEditionContainer = React.createClass({ + propTypes: EditionContainer.propTypes, + + render() { + return ( + + ); + } +}); + +export default MarketEditionContainer; diff --git a/js/components/whitelabel/wallet/components/market/market_detail/market_further_details.js b/js/components/whitelabel/wallet/components/market/market_detail/market_further_details.js new file mode 100644 index 00000000..4e1e3ee8 --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_detail/market_further_details.js @@ -0,0 +1,23 @@ +'use strict'; + +import React from 'react'; + +import MarketAdditionalDataForm from '../market_forms/market_additional_data_form' + +let MarketFurtherDetails = React.createClass({ + propTypes: { + pieceId: React.PropTypes.number, + handleSuccess: React.PropTypes.func, + }, + + render() { + return ( + + ); + } +}); + +export default MarketFurtherDetails; diff --git a/js/components/whitelabel/wallet/components/market/market_detail/market_piece_container.js b/js/components/whitelabel/wallet/components/market/market_detail/market_piece_container.js new file mode 100644 index 00000000..d41ade56 --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_detail/market_piece_container.js @@ -0,0 +1,21 @@ +'use strict'; + +import React from 'react'; + +import MarketFurtherDetails from './market_further_details'; + +import PieceContainer from '../../../../../ascribe_detail/piece_container'; + +let MarketPieceContainer = React.createClass({ + propTypes: PieceContainer.propTypes, + + render() { + return ( + + ); + } +}); + +export default MarketPieceContainer; diff --git a/js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js b/js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js new file mode 100644 index 00000000..d136c9cf --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js @@ -0,0 +1,235 @@ +'use strict'; + +import React from 'react'; + +import Form from '../../../../../ascribe_forms/form'; +import Property from '../../../../../ascribe_forms/property'; + +import InputTextAreaToggable from '../../../../../ascribe_forms/input_textarea_toggable'; + +import FurtherDetailsFileuploader from '../../../../../ascribe_detail/further_details_fileuploader'; +import AscribeSpinner from '../../../../../ascribe_spinner'; + +import GlobalNotificationModel from '../../../../../../models/global_notification_model'; +import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; + +import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_s3_fine_uploader_utils'; + +import PieceActions from '../../../../../../actions/piece_actions'; +import PieceStore from '../../../../../../stores/piece_store'; + +import ApiUrls from '../../../../../../constants/api_urls'; +import AppConstants from '../../../../../../constants/application_constants'; + +import requests from '../../../../../../utils/requests'; +import { mergeOptions } from '../../../../../../utils/general_utils'; +import { getLangText } from '../../../../../../utils/lang_utils'; + + +let MarketAdditionalDataForm = React.createClass({ + propTypes: { + pieceId: React.PropTypes.oneOfType([ + React.PropTypes.number, + React.PropTypes.string + ]), + editable: React.PropTypes.bool, + isInline: React.PropTypes.bool, + showHeading: React.PropTypes.bool, + showNotification: React.PropTypes.bool, + submitLabel: React.PropTypes.string, + handleSuccess: React.PropTypes.func + }, + + getDefaultProps() { + return { + editable: true, + submitLabel: getLangText('Register work') + }; + }, + + getInitialState() { + const pieceStore = PieceStore.getState(); + + return mergeOptions( + pieceStore, + { + // Allow the form to be submitted if there's already an additional image uploaded + isUploadReady: this.isUploadReadyOnChange(pieceStore.piece), + forceUpdateKey: 0 + }); + }, + + componentDidMount() { + PieceStore.listen(this.onChange); + + if (this.props.pieceId) { + PieceActions.fetchOne(this.props.pieceId); + } + }, + + componentWillUnmount() { + PieceStore.unlisten(this.onChange); + }, + + onChange(state) { + Object.assign({}, state, { + // Allow the form to be submitted if the updated piece already has an additional image uploaded + isUploadReady: this.isUploadReadyOnChange(state.piece), + + /** + * Increment the forceUpdateKey to force the form to rerender on each change + * + * THIS IS A HACK TO MAKE SURE THE FORM ALWAYS DISPLAYS THE MOST RECENT STATE + * BECAUSE SOME OF OUR FORM ELEMENTS DON'T UPDATE FROM PROP CHANGES (ie. + * InputTextAreaToggable). + */ + forceUpdateKey: this.state.forceUpdateKey + 1 + }); + + this.setState(state); + }, + + getFormData() { + let extradata = {}; + let formRefs = this.refs.form.refs; + + // Put additional fields in extra data object + Object + .keys(formRefs) + .forEach((fieldName) => { + extradata[fieldName] = formRefs[fieldName].state.value; + }); + + return { + extradata: extradata, + piece_id: this.state.piece.id + }; + }, + + isUploadReadyOnChange(piece) { + return piece && piece.other_data && piece.other_data.length > 0; + }, + + handleSuccessWithNotification() { + if (typeof this.props.handleSuccess === 'function') { + this.props.handleSuccess(); + } + + let notification = new GlobalNotificationModel(getLangText('Further details successfully updated'), 'success', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + setIsUploadReady(isReady) { + this.setState({ + isUploadReady: isReady + }); + }, + + render() { + const { editable, isInline, handleSuccess, showHeading, showNotification, submitLabel } = this.props; + const { piece } = this.state; + let buttons, heading; + + let spinner = ; + + if (!isInline) { + buttons = ( + + ); + + spinner = ( +
+

+ {spinner} +

+
+ ); + + heading = showHeading ? ( +
+

+ {getLangText('Provide additional details')} +

+
+ ) : null; + } + + if (piece && piece.id) { + return ( +
+ {heading} + + + + + + + + + + + + + + + ); + } else { + return ( +
+ {spinner} +
+ ); + } + } +}); + +export default MarketAdditionalDataForm; diff --git a/js/components/whitelabel/wallet/components/market/market_piece_list.js b/js/components/whitelabel/wallet/components/market/market_piece_list.js new file mode 100644 index 00000000..1c74e6de --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_piece_list.js @@ -0,0 +1,90 @@ +'use strict'; + +import React from 'react'; + +import MarketAclButtonList from './market_buttons/market_acl_button_list'; + +import PieceList from '../../../../piece_list'; + +import UserActions from '../../../../../actions/user_actions'; +import UserStore from '../../../../../stores/user_store'; +import WhitelabelActions from '../../../../../actions/whitelabel_actions'; +import WhitelabelStore from '../../../../../stores/whitelabel_store'; + +import { setDocumentTitle } from '../../../../../utils/dom_utils'; +import { mergeOptions } from '../../../../../utils/general_utils'; +import { getLangText } from '../../../../../utils/lang_utils'; + +let MarketPieceList = React.createClass({ + propTypes: { + customThumbnailPlaceholder: React.PropTypes.func, + location: React.PropTypes.object + }, + + getInitialState() { + return mergeOptions( + UserStore.getState(), + WhitelabelStore.getState() + ); + }, + + componentWillMount() { + setDocumentTitle(getLangText('Collection')); + }, + + componentDidMount() { + UserStore.listen(this.onChange); + WhitelabelStore.listen(this.onChange); + + UserActions.fetchCurrentUser(); + WhitelabelActions.fetchWhitelabel(); + }, + + componentWillUnmount() { + UserStore.unlisten(this.onChange); + WhitelabelStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + const { customThumbnailPlaceholder, location } = this.props; + const { + currentUser: { email: userEmail }, + whitelabel: { + name: whitelabelName = 'Market', + user: whitelabelAdminEmail + } } = this.state; + + let filterParams = null; + let canLoadPieceList = false; + + if (userEmail && whitelabelAdminEmail) { + canLoadPieceList = true; + const isUserAdmin = userEmail === whitelabelAdminEmail; + + filterParams = [{ + label: getLangText('Show works I can'), + items: [{ + key: isUserAdmin ? 'acl_transfer' : 'acl_consign', + label: getLangText(isUserAdmin ? 'transfer' : 'consign to %s', whitelabelName), + defaultValue: true + }] + }]; + } + + return ( + + ); + } +}); + +export default MarketPieceList; diff --git a/js/components/whitelabel/wallet/components/market/market_register_piece.js b/js/components/whitelabel/wallet/components/market/market_register_piece.js new file mode 100644 index 00000000..387934f9 --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_register_piece.js @@ -0,0 +1,174 @@ +'use strict'; + +import React from 'react'; +import { History } from 'react-router'; + +import Col from 'react-bootstrap/lib/Col'; +import Row from 'react-bootstrap/lib/Row'; + +import MarketAdditionalDataForm from './market_forms/market_additional_data_form'; + +import Property from '../../../../ascribe_forms/property'; +import RegisterPieceForm from '../../../../ascribe_forms/form_register_piece'; + +import PieceActions from '../../../../../actions/piece_actions'; +import PieceListStore from '../../../../../stores/piece_list_store'; +import PieceListActions from '../../../../../actions/piece_list_actions'; +import UserStore from '../../../../../stores/user_store'; +import UserActions from '../../../../../actions/user_actions'; +import WhitelabelActions from '../../../../../actions/whitelabel_actions'; +import WhitelabelStore from '../../../../../stores/whitelabel_store'; + +import SlidesContainer from '../../../../ascribe_slides_container/slides_container'; + +import { getLangText } from '../../../../../utils/lang_utils'; +import { setDocumentTitle } from '../../../../../utils/dom_utils'; +import { mergeOptions } from '../../../../../utils/general_utils'; + +let MarketRegisterPiece = React.createClass({ + propTypes: { + location: React.PropTypes.object + }, + + mixins: [History], + + getInitialState(){ + return mergeOptions( + PieceListStore.getState(), + UserStore.getState(), + WhitelabelStore.getState(), + { + step: 0 + }); + }, + + componentDidMount() { + PieceListStore.listen(this.onChange); + UserStore.listen(this.onChange); + WhitelabelStore.listen(this.onChange); + + UserActions.fetchCurrentUser(); + WhitelabelActions.fetchWhitelabel(); + + // Reset the piece store to make sure that we don't display old data + // if the user repeatedly registers works + PieceActions.updatePiece({}); + }, + + componentWillUnmount() { + PieceListStore.unlisten(this.onChange); + UserStore.unlisten(this.onChange); + WhitelabelStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + handleRegisterSuccess(response) { + this.refreshPieceList(); + + // Use the response's piece for the next step if available + let pieceId = null; + if (response && response.piece) { + pieceId = response.piece.id; + PieceActions.updatePiece(response.piece); + } + + this.incrementStep(); + this.refs.slidesContainer.nextSlide({ piece_id: pieceId }); + }, + + handleAdditionalDataSuccess() { + this.refreshPieceList(); + + this.history.pushState(null, '/collection'); + }, + + // We need to increase the step to lock the forms that are already filled out + incrementStep() { + this.setState({ + step: this.state.step + 1 + }); + }, + + getPieceFromQueryParam() { + const queryParams = this.props.location.query; + + // Since every step of this register process is atomic, + // we may need to enter the process at step 1 or 2. + // If this is the case, we'll need the piece number to complete submission. + // It is encoded in the URL as a queryParam and we're checking for it here. + return queryParams && queryParams.piece_id; + }, + + refreshPieceList() { + PieceListActions.fetchPieceList( + this.state.page, + this.state.pageSize, + this.state.searchTerm, + this.state.orderBy, + this.state.orderAsc, + this.state.filterBy + ); + }, + + render() { + const { + step, + whitelabel: { + name: whitelabelName = 'Market' + } } = this.state; + + setDocumentTitle(getLangText('Register a new piece')); + + return ( + +
+ + + 0} + enableLocalHashing={false} + headerMessage={getLangText('Consign to %s', whitelabelName)} + submitMessage={getLangText('Proceed to additional details')} + isFineUploaderActive={true} + enableSeparateThumbnail={false} + handleSuccess={this.handleRegisterSuccess} + location={this.props.location}> + + + + + + +
+
+ + + + + +
+
+ ); + } +}); + +export default MarketRegisterPiece; diff --git a/js/components/whitelabel/wallet/constants/wallet_api_urls.js b/js/components/whitelabel/wallet/constants/wallet_api_urls.js index 2cdc0054..8ad2eb81 100644 --- a/js/components/whitelabel/wallet/constants/wallet_api_urls.js +++ b/js/components/whitelabel/wallet/constants/wallet_api_urls.js @@ -4,22 +4,30 @@ import walletConstants from './wallet_application_constants'; // gets subdomain as a parameter function getWalletApiUrls(subdomain) { - if (subdomain === 'cyland'){ + if (subdomain === 'cyland') { return { 'pieces_list': walletConstants.walletApiEndpoint + subdomain + '/pieces/', 'piece': walletConstants.walletApiEndpoint + subdomain + '/pieces/${piece_id}/', 'piece_extradata': walletConstants.walletApiEndpoint + subdomain + '/pieces/${piece_id}/extradata/', 'user': walletConstants.walletApiEndpoint + subdomain + '/users/' }; - } - else if (subdomain === 'ikonotv'){ + } else if (subdomain === 'ikonotv') { return { 'pieces_list': walletConstants.walletApiEndpoint + subdomain + '/pieces/', 'piece': walletConstants.walletApiEndpoint + subdomain + '/pieces/${piece_id}/', 'user': walletConstants.walletApiEndpoint + subdomain + '/users/' }; + } else if (subdomain === 'lumenus' || subdomain === '23vivi') { + return { + 'editions_list': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/pieces/${piece_id}/editions/', + 'edition': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/editions/${bitcoin_id}/', + 'pieces_list': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/pieces/', + 'piece': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/pieces/${piece_id}/', + 'piece_extradata': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/pieces/${piece_id}/extradata/', + 'user': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/users/' + }; } return {}; } -export default getWalletApiUrls; \ No newline at end of file +export default getWalletApiUrls; diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index 5056716a..c2810fd0 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -32,7 +32,7 @@ let WalletApp = React.createClass({ // if the path of the current activeRoute is not defined, then this is the IndexRoute if ((!path || history.isActive('/login') || history.isActive('/signup') || history.isActive('/contract_notifications')) - && (['ikonotv']).indexOf(subdomain) > -1) { + && (['cyland', 'ikonotv', 'lumenus', '23vivi']).indexOf(subdomain) > -1) { header = (
); } else { header =
; diff --git a/js/components/whitelabel/wallet/wallet_routes.js b/js/components/whitelabel/wallet/wallet_routes.js index 8e4d5197..0a4e3a58 100644 --- a/js/components/whitelabel/wallet/wallet_routes.js +++ b/js/components/whitelabel/wallet/wallet_routes.js @@ -16,6 +16,8 @@ import SettingsContainer from '../../../components/ascribe_settings/settings_con import ContractSettings from '../../../components/ascribe_settings/contract_settings'; import ErrorNotFoundPage from '../../../components/error_not_found_page'; +import CCRegisterPiece from './components/cc/cc_register_piece'; + import CylandLanding from './components/cyland/cyland_landing'; import CylandPieceContainer from './components/cyland/cyland_detail/cyland_piece_container'; import CylandRegisterPiece from './components/cyland/cyland_register_piece'; @@ -23,12 +25,20 @@ import CylandPieceList from './components/cyland/cyland_piece_list'; import IkonotvLanding from './components/ikonotv/ikonotv_landing'; import IkonotvPieceList from './components/ikonotv/ikonotv_piece_list'; -import ContractAgreementForm from '../../../components/ascribe_forms/form_contract_agreement'; +import SendContractAgreementForm from '../../../components/ascribe_forms/form_send_contract_agreement'; import IkonotvRegisterPiece from './components/ikonotv/ikonotv_register_piece'; import IkonotvPieceContainer from './components/ikonotv/ikonotv_detail/ikonotv_piece_container'; import IkonotvContractNotifications from './components/ikonotv/ikonotv_contract_notifications'; -import CCRegisterPiece from './components/cc/cc_register_piece'; +import MarketPieceList from './components/market/market_piece_list'; +import MarketRegisterPiece from './components/market/market_register_piece'; +import MarketPieceContainer from './components/market/market_detail/market_piece_container'; +import MarketEditionContainer from './components/market/market_detail/market_edition_container'; + +import LumenusLanding from './components/lumenus/lumenus_landing'; + +import Vivi23Landing from './components/23vivi/23vivi_landing'; +import Vivi23PieceList from './components/23vivi/23vivi_piece_list'; import AuthProxyHandler from '../../../components/ascribe_routes/proxy_routes/auth_proxy_handler'; @@ -128,7 +138,7 @@ let ROUTES = { component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(ContractSettings)}/> + ), + 'lumenus': ( + + + + + + + + + + + + + + + + ), + '23vivi': ( + + + + + + + + + + + + + + + ) }; - function getRoutes(commonRoutes, subdomain) { if(subdomain in ROUTES) { return ROUTES[subdomain]; @@ -160,5 +240,4 @@ function getRoutes(commonRoutes, subdomain) { } } - export default getRoutes; diff --git a/js/constants/api_urls.js b/js/constants/api_urls.js index a07f29b1..e7f11141 100644 --- a/js/constants/api_urls.js +++ b/js/constants/api_urls.js @@ -72,6 +72,9 @@ let ApiUrls = { 'users_username': AppConstants.apiEndpoint + 'users/username/', 'users_profile': AppConstants.apiEndpoint + 'users/profile/', 'wallet_settings': AppConstants.apiEndpoint + 'users/wallet_settings/', + 'webhook': AppConstants.apiEndpoint + 'webhooks/${webhook_id}/', + 'webhooks': AppConstants.apiEndpoint + 'webhooks/', + 'webhooks_events': AppConstants.apiEndpoint + 'webhooks/events/', 'whitelabel_settings': AppConstants.apiEndpoint + 'whitelabel/settings/${subdomain}/', 'delete_s3_file': AppConstants.serverUrl + 's3/delete/', 'prize_list': AppConstants.apiEndpoint + 'prize/' diff --git a/js/constants/application_constants.js b/js/constants/application_constants.js index 79d00747..74edc51b 100644 --- a/js/constants/application_constants.js +++ b/js/constants/application_constants.js @@ -51,6 +51,20 @@ const constants = { 'permissions': ['register', 'edit', 'share', 'del_from_collection'], 'type': 'wallet' }, + { + 'subdomain': 'lumenus', + 'name': 'Lumenus', + 'logo': 'https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/lumenus/lumenus-logo.png', + 'permissions': ['register', 'edit', 'share', 'del_from_collection'], + 'type': 'wallet' + }, + { + 'subdomain': '23vivi', + 'name': '23VIVI', + 'logo': 'https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/23vivi/23vivi-logo.png', + 'permissions': ['register', 'edit', 'share', 'del_from_collection'], + 'type': 'wallet' + }, { 'subdomain': 'portfolioreview', 'name': 'Portfolio Review', @@ -124,7 +138,12 @@ const constants = { }, 'twitter': { 'sdkUrl': 'https://platform.twitter.com/widgets.js' - } + }, + + 'errorMessagesToIgnore': [ + 'Authentication credentials were not provided.', + 'Informations d\'authentification non fournies.' + ] }; export default constants; diff --git a/js/mixins/react_error.js b/js/mixins/react_error.js new file mode 100644 index 00000000..14f33a61 --- /dev/null +++ b/js/mixins/react_error.js @@ -0,0 +1,16 @@ +'use strict'; + +import invariant from 'invariant'; + +const ReactError = { + throws(err) { + if(!err.handler) { + invariant(err.handler, 'Error thrown to ReactError did not have a `handler` function'); + console.logGlobal('Error thrown to ReactError did not have a `handler` function'); + } else { + err.handler(this, err); + } + } +}; + +export default ReactError; diff --git a/js/models/errors.js b/js/models/errors.js new file mode 100644 index 00000000..4573afe4 --- /dev/null +++ b/js/models/errors.js @@ -0,0 +1,31 @@ +'use strict'; + +import React from 'react'; + +import ErrorNotFoundPage from '../components/error_not_found_page'; + + +export class ResourceNotFoundError extends Error { + constructor(message) { + super(message); + this.name = this.constructor.name; + this.message = message; + + // `captureStackTrace` might not be available in IE: + // - http://stackoverflow.com/a/8460753/1263876 + if(Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor.name); + } + } + + handler(component, err) { + const monkeyPatchedKey = `_${this.name}MonkeyPatched`; + + if(!component.state[monkeyPatchedKey]) { + component.render = () => ; + component.setState({ + [monkeyPatchedKey]: true + }); + } + } +} diff --git a/js/sources/webhook_source.js b/js/sources/webhook_source.js new file mode 100644 index 00000000..5351c89c --- /dev/null +++ b/js/sources/webhook_source.js @@ -0,0 +1,46 @@ +'use strict'; + +import requests from '../utils/requests'; + +import WebhookActions from '../actions/webhook_actions'; + + +const WebhookSource = { + lookupWebhooks: { + remote() { + return requests.get('webhooks'); + }, + local(state) { + return state.webhooks && !Object.keys(state.webhooks).length ? state : {}; + }, + success: WebhookActions.successFetchWebhooks, + error: WebhookActions.errorWebhooks, + shouldFetch(state) { + return state.webhookMeta.invalidateCache || state.webhooks && !Object.keys(state.webhooks).length; + } + }, + + lookupWebhookEvents: { + remote() { + return requests.get('webhooks_events'); + }, + local(state) { + return state.webhookEvents && !Object.keys(state.webhookEvents).length ? state : {}; + }, + success: WebhookActions.successFetchWebhookEvents, + error: WebhookActions.errorWebhookEvents, + shouldFetch(state) { + return state.webhookEventsMeta.invalidateCache || state.webhookEvents && !Object.keys(state.webhookEvents).length; + } + }, + + performRemoveWebhook: { + remote(state) { + return requests.delete('webhook', {'webhook_id': state.webhookMeta.idToDelete }); + }, + success: WebhookActions.successRemoveWebhook, + error: WebhookActions.errorWebhooks + } +}; + +export default WebhookSource; \ No newline at end of file diff --git a/js/stores/edition_list_store.js b/js/stores/edition_list_store.js index 4ccada4e..107f9af4 100644 --- a/js/stores/edition_list_store.js +++ b/js/stores/edition_list_store.js @@ -60,7 +60,7 @@ class EditionListStore { * We often just have to refresh the edition list for a certain pieceId, * this method provides exactly that functionality without any side effects */ - onRefreshEditionList({pieceId, filterBy}) { + onRefreshEditionList({pieceId, filterBy = {}}) { // It may happen that the user enters the site logged in already // through /editions // If he then tries to delete a piece/edition and this method is called, diff --git a/js/stores/edition_store.js b/js/stores/edition_store.js index 14ee4fee..22e78d23 100644 --- a/js/stores/edition_store.js +++ b/js/stores/edition_store.js @@ -7,11 +7,17 @@ import EditionActions from '../actions/edition_actions'; class EditionStore { constructor() { this.edition = {}; + this.editionError = null; this.bindActions(EditionActions); } onUpdateEdition(edition) { this.edition = edition; + this.editionError = null; + } + + onEditionFailed(error) { + this.editionError = error; } } diff --git a/js/stores/global_notification_store.js b/js/stores/global_notification_store.js index 5a23fe1b..7414812b 100644 --- a/js/stores/global_notification_store.js +++ b/js/stores/global_notification_store.js @@ -4,36 +4,63 @@ import { alt } from '../alt'; import GlobalNotificationActions from '../actions/global_notification_actions'; +const GLOBAL_NOTIFICATION_COOLDOWN = 400; + class GlobalNotificationStore { constructor() { - this.notificationQue = []; + this.notificationQueue = []; + this.notificationStatus = 'ready'; + this.notificationsPaused = false; this.bindActions(GlobalNotificationActions); } onAppendGlobalNotification(newNotification) { - let notificationDelay = 0; - for(let i = 0; i < this.notificationQue.length; i++) { - notificationDelay += this.notificationQue[i].dismissAfter; - } + this.notificationQueue.push(newNotification); - this.notificationQue.push(newNotification); - setTimeout(GlobalNotificationActions.emulateEmptyStore, notificationDelay + newNotification.dismissAfter); + if (!this.notificationsPaused && this.notificationStatus === 'ready') { + this.showNextNotification(); + } } - onEmulateEmptyStore() { - let actualNotificitionQue = this.notificationQue.slice(); + showNextNotification() { + this.notificationStatus = 'show'; - this.notificationQue = []; + setTimeout(GlobalNotificationActions.cooldownGlobalNotifications, this.notificationQueue[0].dismissAfter); + } - setTimeout(() => { - this.notificationQue = actualNotificitionQue.slice(); - GlobalNotificationActions.shiftGlobalNotification(); - }, 400); + onCooldownGlobalNotifications() { + // When still paused on cooldown, don't shift the queue so we can repeat the current notification. + if (!this.notificationsPaused) { + this.notificationStatus = 'cooldown'; + + // Leave some time between consecutive notifications + setTimeout(GlobalNotificationActions.shiftGlobalNotification, GLOBAL_NOTIFICATION_COOLDOWN); + } else { + this.notificationStatus = 'ready'; + } } onShiftGlobalNotification() { - this.notificationQue.shift(); + this.notificationQueue.shift(); + + if (!this.notificationsPaused && this.notificationQueue.length > 0) { + this.showNextNotification(); + } else { + this.notificationStatus = 'ready'; + } + } + + onPauseGlobalNotifications() { + this.notificationsPaused = true; + } + + onResumeGlobalNotifications() { + this.notificationsPaused = false; + + if (this.notificationStatus === 'ready' && this.notificationQueue.length > 0) { + this.showNextNotification(); + } } } diff --git a/js/stores/piece_store.js b/js/stores/piece_store.js index ccef50b1..3b04736b 100644 --- a/js/stores/piece_store.js +++ b/js/stores/piece_store.js @@ -7,11 +7,13 @@ import PieceActions from '../actions/piece_actions'; class PieceStore { constructor() { this.piece = {}; + this.pieceError = null; this.bindActions(PieceActions); } onUpdatePiece(piece) { this.piece = piece; + this.pieceError = null; } onUpdateProperty({key, value}) { @@ -21,6 +23,10 @@ class PieceStore { throw new Error('There is no piece defined in PieceStore or the piece object does not have the property you\'re looking for.'); } } + + onPieceFailed(err) { + this.pieceError = err; + } } export default alt.createStore(PieceStore, 'PieceStore'); diff --git a/js/stores/webhook_store.js b/js/stores/webhook_store.js new file mode 100644 index 00000000..7dfcc61d --- /dev/null +++ b/js/stores/webhook_store.js @@ -0,0 +1,88 @@ +'use strict'; + +import { alt } from '../alt'; +import WebhookActions from '../actions/webhook_actions'; + +import WebhookSource from '../sources/webhook_source'; + +class WebhookStore { + constructor() { + this.webhooks = []; + this.webhookEvents = []; + this.webhookMeta = { + invalidateCache: false, + err: null, + idToDelete: null + }; + this.webhookEventsMeta = { + invalidateCache: false, + err: null + }; + + this.bindActions(WebhookActions); + this.registerAsync(WebhookSource); + } + + onFetchWebhooks(invalidateCache) { + this.webhookMeta.invalidateCache = invalidateCache; + this.getInstance().lookupWebhooks(); + } + + onSuccessFetchWebhooks({ webhooks }) { + this.webhookMeta.invalidateCache = false; + this.webhookMeta.err = null; + this.webhooks = webhooks; + + this.webhookEventsMeta.invalidateCache = true; + this.getInstance().lookupWebhookEvents(); + } + + onFetchWebhookEvents(invalidateCache) { + this.webhookEventsMeta.invalidateCache = invalidateCache; + this.getInstance().lookupWebhookEvents(); + } + + onSuccessFetchWebhookEvents({ events }) { + this.webhookEventsMeta.invalidateCache = false; + this.webhookEventsMeta.err = null; + + // remove all events that have already been used. + const usedEvents = this.webhooks + .reduce((tempUsedEvents, webhook) => { + tempUsedEvents.push(webhook.event.split('.')[0]); + return tempUsedEvents; + }, []); + + this.webhookEvents = events.filter((event) => { + return usedEvents.indexOf(event) === -1; + }); + } + + onRemoveWebhook(id) { + this.webhookMeta.invalidateCache = true; + this.webhookMeta.idToDelete = id; + + if(!this.getInstance().isLoading()) { + this.getInstance().performRemoveWebhook(); + } + } + + onSuccessRemoveWebhook() { + this.webhookMeta.idToDelete = null; + if(!this.getInstance().isLoading()) { + this.getInstance().lookupWebhooks(); + } + } + + onErrorWebhooks(err) { + console.logGlobal(err); + this.webhookMeta.err = err; + } + + onErrorWebhookEvents(err) { + console.logGlobal(err); + this.webhookEventsMeta.err = err; + } +} + +export default alt.createStore(WebhookStore, 'WebhookStore'); diff --git a/js/utils/acl_utils.js b/js/utils/acl_utils.js index fc3987c1..dd39a380 100644 --- a/js/utils/acl_utils.js +++ b/js/utils/acl_utils.js @@ -4,7 +4,7 @@ import { sanitize, intersectLists } from './general_utils'; export function getAvailableAcls(editions, filterFn) { let availableAcls = []; - if (!editions || editions.constructor !== Array){ + if (!editions || editions.constructor !== Array) { return []; } // if you copy a javascript array of objects using slice, then @@ -33,23 +33,23 @@ export function getAvailableAcls(editions, filterFn) { }); // If no edition has been selected, availableActions is empty - // If only one edition has been selected, their actions are available - // If more than one editions have been selected, their acl properties are intersected - if(editionsCopy.length >= 1) { + // If only one edition has been selected, its actions are available + // If more than one editions have been selected, intersect all their acl properties + if (editionsCopy.length >= 1) { availableAcls = editionsCopy[0].acl; - } - if(editionsCopy.length >= 2) { - for(let i = 1; i < editionsCopy.length; i++) { - availableAcls = intersectLists(availableAcls, editionsCopy[i].acl); + + if (editionsCopy.length >= 2) { + for (let i = 1; i < editionsCopy.length; i++) { + availableAcls = intersectLists(availableAcls, editionsCopy[i].acl); + } } } // convert acls back to key-value object let availableAclsObj = {}; - for(let i = 0; i < availableAcls.length; i++) { + for (let i = 0; i < availableAcls.length; i++) { availableAclsObj[availableAcls[i]] = true; } - return availableAclsObj; -} \ No newline at end of file +} diff --git a/js/utils/error_utils.js b/js/utils/error_utils.js index e80819dc..a10f1268 100644 --- a/js/utils/error_utils.js +++ b/js/utils/error_utils.js @@ -12,7 +12,8 @@ import AppConstants from '../constants/application_constants'; * @param {boolean} ignoreSentry Defines whether or not the error should be submitted to Sentry * @param {string} comment Will also be submitted to Sentry, but will not be logged */ -function logGlobal(error, ignoreSentry, comment) { +function logGlobal(error, ignoreSentry = AppConstants.errorMessagesToIgnore.indexOf(error.message) > -1, + comment) { console.error(error); if(!ignoreSentry) { diff --git a/js/utils/file_utils.js b/js/utils/file_utils.js index f5d6ba99..9c1423b3 100644 --- a/js/utils/file_utils.js +++ b/js/utils/file_utils.js @@ -89,11 +89,16 @@ export function computeHashOfFile(file) { /** * Extracts a file extension from a string, by splitting by dot and taking * the last substring + * + * If a file without an extension is submitted (file), then + * this method just returns an empty string. * @param {string} s file's name + extension * @return {string} file extension * * Via: http://stackoverflow.com/a/190878/1263876 */ export function extractFileExtensionFromString(s) { - return s.split('.').pop(); + const explodedFileName = s.split('.'); + return explodedFileName.length > 1 ? explodedFileName.pop() + : ''; } \ No newline at end of file diff --git a/js/utils/form_utils.js b/js/utils/form_utils.js index c15eb067..8d12a8c1 100644 --- a/js/utils/form_utils.js +++ b/js/utils/form_utils.js @@ -2,6 +2,8 @@ import { getLangText } from './lang_utils'; +import AppConstants from '../constants/application_constants'; + /** * Get the data ids of the given piece or editions. * @param {boolean} isPiece Is the given entities parameter a piece? (False: array of editions) @@ -70,6 +72,10 @@ export function getAclFormMessage(options) { throw new Error('Your specified aclName did not match a an acl class.'); } + if (options.additionalMessage) { + message += '\n\n' + options.additionalMessage; + } + if (options.senderName) { message += '\n\n'; message += getLangText('Truly yours,'); diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index b15a0525..a3336d80 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -1,5 +1,11 @@ 'use strict'; +/** + * Checks shallow equality + * Re-export of shallow from shallow-equals + */ +export { default as isShallowEqual } from 'shallow-equals'; + /** * Takes an object and returns a shallow copy without any keys * that fail the passed in filter function. @@ -109,7 +115,7 @@ function _doesObjectListHaveDuplicates(l) { export function mergeOptions(...l) { // If the objects submitted in the list have duplicates,in their key names, // abort the merge and tell the function's user to check his objects. - if(_doesObjectListHaveDuplicates(l)) { + if (_doesObjectListHaveDuplicates(l)) { throw new Error('The objects you submitted for merging have duplicates. Merge aborted.'); } diff --git a/js/utils/inject_utils.js b/js/utils/inject_utils.js index 174ac8b6..e9430a5e 100644 --- a/js/utils/inject_utils.js +++ b/js/utils/inject_utils.js @@ -12,16 +12,16 @@ let mapTag = { css: 'link' }; +let tags = {}; + function injectTag(tag, src) { - return Q.Promise((resolve, reject) => { - if (isPresent(tag, src)) { - resolve(); - } else { + if(!tags[src]) { + tags[src] = Q.Promise((resolve, reject) => { let attr = mapAttr[tag]; let element = document.createElement(tag); if (tag === 'script') { - element.onload = () => resolve(); - element.onerror = () => reject(); + element.onload = resolve; + element.onerror = reject; } else { resolve(); } @@ -30,14 +30,10 @@ function injectTag(tag, src) { if (tag === 'link') { element.rel = 'stylesheet'; } - } - }); -} + }); + } -function isPresent(tag, src) { - let attr = mapAttr[tag]; - let query = `head > ${tag}[${attr}="${src}"]`; - return document.querySelector(query); + return tags[src]; } function injectStylesheet(src) { @@ -65,7 +61,6 @@ export const InjectInHeadUtils = { * you don't want to embed everything inside the build file. */ - isPresent, injectStylesheet, injectScript, inject diff --git a/js/utils/regex_utils.js b/js/utils/regex_utils.js new file mode 100644 index 00000000..af948b2b --- /dev/null +++ b/js/utils/regex_utils.js @@ -0,0 +1,7 @@ +'use strict' + +export function isEmail(string) { + // This is a bit of a weak test for an email, but you really can't win them all + // http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address + return !!string && string.match(/.*@.*\..*/); +} diff --git a/js/utils/requests.js b/js/utils/requests.js index a7300634..9195661d 100644 --- a/js/utils/requests.js +++ b/js/utils/requests.js @@ -30,6 +30,15 @@ class Requests { reject(error); } else if(body && body.detail) { reject(new Error(body.detail)); + } else if(!body.success) { + let error = new Error('Client Request Error'); + error.json = { + status: response.status, + statusText: response.statusText, + type: response.type, + url: response.url + }; + reject(error); } else { resolve(body); } @@ -100,8 +109,7 @@ class Requests { return newUrl; } - request(verb, url, options) { - options = options || {}; + request(verb, url, options = {}) { let merged = Object.assign({}, this.httpOptions, options); let csrftoken = getCookie(AppConstants.csrftoken); if (csrftoken) { @@ -129,13 +137,10 @@ class Requests { } _putOrPost(url, paramsAndBody, method) { - let paramsCopy = Object.assign({}, paramsAndBody); let params = omitFromObject(paramsAndBody, ['body']); let newUrl = this.prepareUrl(url, params); - let body = null; - if (paramsCopy && paramsCopy.body) { - body = JSON.stringify(paramsCopy.body); - } + let body = paramsAndBody && paramsAndBody.body ? JSON.stringify(paramsAndBody.body) + : null; return this.request(method, newUrl, { body }); } diff --git a/package.json b/package.json index 63c6d1e0..be5c1202 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "gulp-uglify": "^1.2.0", "gulp-util": "^3.0.4", "harmonize": "^1.4.2", - "history": "^1.11.1", + "history": "^1.13.1", "invariant": "^2.1.1", "isomorphic-fetch": "^2.0.2", "jest-cli": "^0.4.0", @@ -80,11 +80,12 @@ "react": "0.13.2", "react-bootstrap": "0.25.1", "react-datepicker": "^0.12.0", - "react-router": "^1.0.0-rc3", + "react-router": "1.0.0", "react-router-bootstrap": "^0.19.0", "react-star-rating": "~1.3.2", "react-textarea-autosize": "^2.5.2", "reactify": "^1.1.0", + "shallow-equals": "0.0.0", "shmui": "^0.1.0", "spark-md5": "~1.0.0", "uglifyjs": "^2.4.10", diff --git a/sass/ascribe-fonts/ascribe-fonts.scss b/sass/ascribe-fonts/ascribe-fonts.scss index 11b42851..6f95a616 100644 --- a/sass/ascribe-fonts/ascribe-fonts.scss +++ b/sass/ascribe-fonts/ascribe-fonts.scss @@ -1,24 +1,10 @@ -[class^="icon-ascribe-"], [class*=" icon-ascribe-"] { - font-family: 'ascribe-logo'; - speak: none; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - /* These glyphs are generated using: https://icomoon.io Even though it seems radically complicated, check out the site, its fairly straight forward. If someone wants you to add a new glyph go to the site, - drop in the regular ascribe-logo font and select all icons. + drop in the regular ascribe-font font and select all icons. Then also add the new glyph, name and address it correctly and download the font again. @@ -26,18 +12,19 @@ */ @font-face { - font-family: 'ascribe-logo'; - src:url('#{$BASE_URL}static/fonts/ascribe-logo.eot?q6qoae'); - src:url('#{$BASE_URL}static/fonts/ascribe-logo.eot?q6qoae#iefix') format('embedded-opentype'), - url('#{$BASE_URL}static/fonts/ascribe-logo.ttf?q6qoae') format('truetype'), - url('#{$BASE_URL}static/fonts/ascribe-logo.woff?q6qoae') format('woff'), - url('#{$BASE_URL}static/fonts/ascribe-logo.svg?q6qoae#ascribe-logo') format('svg'); + font-family: 'ascribe-font'; + src:url('#{$BASE_URL}static/fonts/ascribe-font.eot?q6qoae'); + src:url('#{$BASE_URL}static/fonts/ascribe-font.eot?q6qoae#iefix') format('embedded-opentype'), + url('#{$BASE_URL}static/fonts/ascribe-font.ttf?q6qoae') format('truetype'), + url('#{$BASE_URL}static/fonts/ascribe-font.woff?q6qoae') format('woff'), + url('#{$BASE_URL}static/fonts/ascribe-font.svg?q6qoae#ascribe-font') format('svg'); font-weight: normal; font-style: normal; } [class^="icon-ascribe-"], [class*=" icon-ascribe-"] { - font-family: 'ascribe-logo'; + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'ascribe-font' !important; speak: none; font-style: normal; font-weight: normal; @@ -80,35 +67,15 @@ .icon-ascribe-logo:before { content: "\e808"; } - .icon-ascribe-ok:before { content: "\e809"; font-size: .7em; } +.icon-ascribe-thin-cross:before { + content: "\e810"; +} .btn-glyph-ascribe { font-size: 18px; padding: 4px 12px 0 10px } - -.ascribe-logo-circle { - border: 6px solid #F6F6F6; - border-radius: 10em; - position: relative; - top: 10%; - left: 10%; - - display: block; - width: 80%; - height: 80%; - - > span { - color: #F6F6F6; - position: absolute; - top: -.29em; - left: .16em; - - font-size: 5em; - font-weight: normal; - } -} \ No newline at end of file diff --git a/sass/ascribe_accordion_list.scss b/sass/ascribe_accordion_list.scss index c0b81096..791743fc 100644 --- a/sass/ascribe_accordion_list.scss +++ b/sass/ascribe_accordion_list.scss @@ -60,6 +60,34 @@ $ascribe-accordion-list-item-height: 100px; background-size: cover; } + .ascribe-logo-circle { + border: 6px solid #F6F6F6; + border-radius: 10em; + position: relative; + top: 10%; + left: 10%; + + display: block; + width: 80%; + height: 80%; + + > span { + color: #F6F6F6; + position: absolute; + top: -.29em; + left: .16em; + + font-size: 5em; + font-weight: normal; + } + } + + .ascribe-thumbnail-placeholder { + color: #F6F6F6; + font-size: 5em; + font-weight: normal; + } + //&::before { // content: ' '; // display: inline-block; @@ -211,10 +239,6 @@ $ascribe-accordion-list-item-height: 100px; -ms-user-select: none; -webkit-user-select: none; - &:hover { - color: $ascribe-dark-blue; - } - .glyphicon { font-size: .8em; top: 1px !important; diff --git a/sass/ascribe_acl_information.scss b/sass/ascribe_acl_information.scss index 063c8ae6..5a4708f0 100644 --- a/sass/ascribe_acl_information.scss +++ b/sass/ascribe_acl_information.scss @@ -22,4 +22,4 @@ .example { color: #616161; } -} \ No newline at end of file +} diff --git a/sass/ascribe_custom_style.scss b/sass/ascribe_custom_style.scss index 98cce937..96b97783 100644 --- a/sass/ascribe_custom_style.scss +++ b/sass/ascribe_custom_style.scss @@ -68,10 +68,15 @@ hr { .dropdown-menu { background-color: $ascribe--nav-bg-color; } + .navbar-nav > li > .dropdown-menu { + padding: 0; + } .dropdown-menu > li > a { color: $ascribe--nav-fg-prim-color; font-weight: $ascribe--font-weight-light; + padding-bottom: 9px; + padding-top: 9px; } .dropdown-menu > li > a:hover, @@ -79,6 +84,10 @@ hr { background-color: rgba($ascribe--button-default-color, .05); } + .dropdown-menu > .divider { + margin: 0; + } + .notification-menu { .dropdown-menu { background-color: white; @@ -257,6 +266,24 @@ hr { font-weight: $ascribe--font-weight-light; } +.btn-default { + background-color: $ascribe--button-default-color; + border-color: $ascribe--button-default-color; + + &:hover, + &:active, + &:focus, + &:active:hover, + &:active:focus, + &:active.focus, + &.active:hover, + &.active:focus, + &.active.focus { + background-color: lighten($ascribe--button-default-color, 20%); + border-color: lighten($ascribe--button-default-color, 20%); + } +} + // disabled buttons .btn-default.disabled, .btn-default.disabled:hover, @@ -280,9 +307,10 @@ fieldset[disabled] .btn-default.active { border-color: darken($ascribe--button-default-color, 20%); } -.btn-default { - background-color: $ascribe--button-default-color; - border-color: $ascribe--button-default-color; +.btn-secondary { + background-color: $ascribe--button-secondary-bg-color; + border-color: $ascribe--button-secondary-fg-color; + color: $ascribe--button-secondary-fg-color; &:hover, &:active, @@ -293,8 +321,9 @@ fieldset[disabled] .btn-default.active { &.active:hover, &.active:focus, &.active.focus { - background-color: lighten($ascribe--button-default-color, 20%); - border-color: lighten($ascribe--button-default-color, 20%); + background-color: $ascribe--button-secondary-fg-color; + border-color: $ascribe--button-secondary-bg-color; + color: $ascribe--button-secondary-bg-color; } } @@ -322,26 +351,6 @@ fieldset[disabled] .btn-secondary.active { color: darken($ascribe--button-secondary-fg-color, 20%); } -.btn-secondary { - background-color: $ascribe--button-secondary-bg-color; - border-color: $ascribe--button-secondary-fg-color; - color: $ascribe--button-secondary-fg-color; - - &:hover, - &:active, - &:focus, - &:active:hover, - &:active:focus, - &:active.focus, - &.active:hover, - &.active:focus, - &.active.focus { - background-color: $ascribe--button-secondary-fg-color; - border-color: $ascribe--button-secondary-bg-color; - color: $ascribe--button-secondary-bg-color; - } -} - .btn-tertiary { background-color: transparent; border-color: transparent; @@ -580,11 +589,6 @@ fieldset[disabled] .btn-secondary.active { background-color: lighten($ascribe--button-default-color, 20%); } -// uploader -.ascribe-progress-bar > .progress-bar { - background-color: lighten($ascribe--button-default-color, 20%); -} - .action-file { text-shadow: -1px 0 black, 0 1px black, diff --git a/sass/ascribe_notification_list.scss b/sass/ascribe_notification_list.scss index a09f7049..b5f46a4c 100644 --- a/sass/ascribe_notification_list.scss +++ b/sass/ascribe_notification_list.scss @@ -2,8 +2,9 @@ $break-small: 764px; $break-medium: 991px; $break-medium: 1200px; -.notification-header,.notification-wrapper { - width: 350px; +.notification-header, .notification-wrapper { + min-width: 350px; + width: 100%; } .notification-header { @@ -81,4 +82,4 @@ $break-medium: 1200px; border: 1px solid #cccccc; background-color: white; margin-top: 1px; -} \ No newline at end of file +} diff --git a/sass/ascribe_notification_page.scss b/sass/ascribe_notification_page.scss index 955609d2..7bb37446 100644 --- a/sass/ascribe_notification_page.scss +++ b/sass/ascribe_notification_page.scss @@ -31,16 +31,11 @@ margin-top: .5em; margin-bottom: 1em; - .loan-form { - margin-top: .5em; + &.embed-form { height: 45vh; } } - .loan-form { - height: 40vh; - } - .notification-contract-pdf-download { text-align: left; margin-left: 1em; @@ -69,4 +64,8 @@ padding-left: 0; width: 100%; } +} + +.ascribe-property.contract-appendix-form { + padding-left: 0; } \ No newline at end of file diff --git a/sass/ascribe_panel.scss b/sass/ascribe_panel.scss index 0f675605..f4b70a80 100644 --- a/sass/ascribe_panel.scss +++ b/sass/ascribe_panel.scss @@ -31,7 +31,7 @@ vertical-align: middle; &:first-child { - word-break: break-all; + word-break: break-word; font-size: .9em; } } diff --git a/sass/ascribe_piece_list_toolbar.scss b/sass/ascribe_piece_list_toolbar.scss index f033ee81..06cbd1a7 100644 --- a/sass/ascribe_piece_list_toolbar.scss +++ b/sass/ascribe_piece_list_toolbar.scss @@ -81,4 +81,8 @@ top: 2px; } } + + .dropdown-menu { + min-width: 170px; + } } diff --git a/sass/ascribe_spinner.scss b/sass/ascribe_spinner.scss index 7f02a383..133cc6b8 100644 --- a/sass/ascribe_spinner.scss +++ b/sass/ascribe_spinner.scss @@ -52,6 +52,13 @@ $ascribe--spinner-size-sm: 15px; } } +.spinner-wrapper-white { + color: $ascribe-white; + .spinner-circle { + border-color: $ascribe-white; + } +} + .spinner-wrapper-lg { width: $ascribe--spinner-size-lg; height: $ascribe--spinner-size-lg; @@ -107,17 +114,20 @@ $ascribe--spinner-size-sm: 15px; } .spinner-wrapper-lg .spinner-inner { font-size: $ascribe--spinner-size-lg; - top: -64px; + line-height: $ascribe--spinner-size-lg; + top: -50px; } .spinner-wrapper-md .spinner-inner { font-size: $ascribe--spinner-size-md; - top: -38px; + line-height: $ascribe--spinner-size-md; + top: -30px; } .spinner-wrapper-sm .spinner-inner { font-size: $ascribe--spinner-size-sm; - top: -19px; + line-height: $ascribe--spinner-size-sm; + top: -15px; } @-webkit-keyframes spin { @@ -146,4 +156,4 @@ $ascribe--spinner-size-sm: 15px; 40% { color: $ascribe-blue; } 60% { color: $ascribe-light-blue; } 80% { color: $ascribe-pink; } -} \ No newline at end of file +} diff --git a/sass/ascribe_uploader.scss b/sass/ascribe_uploader.scss index fa353ecd..ea02eb07 100644 --- a/sass/ascribe_uploader.scss +++ b/sass/ascribe_uploader.scss @@ -120,7 +120,7 @@ &.icon-ascribe-ok, &.icon-ascribe-ok:hover { cursor: default; - color: lighten($ascribe--button-default-color, 20%); + color: $ascribe-dark-blue; font-size: 4.2em; top: .2em; } @@ -328,9 +328,12 @@ } span { - font-size: 1.25em; + font-size: 1.2em; color: white; - text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; + text-shadow: -1px 0 $ascribe--button-default-color, + 0 1px $ascribe--button-default-color, + 1px 0 $ascribe--button-default-color, + 0 -1px $ascribe--button-default-color; } } diff --git a/sass/main.scss b/sass/main.scss index 8a732e96..5cc91e9a 100644 --- a/sass/main.scss +++ b/sass/main.scss @@ -350,7 +350,7 @@ hr { > span { font-size: 1.1em; - font-weight: 600; + font-weight: normal; color: #616161; padding-left: .3em; diff --git a/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss b/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss new file mode 100644 index 00000000..a5026272 --- /dev/null +++ b/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss @@ -0,0 +1,377 @@ +/** Sass cannot use a number as the first character of a variable, so we'll have to settle with vivi23 **/ +$vivi23--fg-color: black; +$vivi23--bg-color: white; +$vivi23--nav-fg-prim-color: $vivi23--fg-color; +$vivi23--nav-fg-sec-color: #3a3a3a; +$vivi23--nav-bg-color: $vivi23--bg-color; +$vivi23--nav-highlight-color: #f8f8f8; +$vivi23--button-default-color: $vivi23--fg-color; +$vivi23--highlight-color: #de2600; + + +.client--23vivi { + /** Landing page **/ + .route--landing { + display: table; + + > .container { + display: table-cell; + padding-bottom: 100px; + vertical-align: middle; + } + + .vivi23-landing { + font-weight: normal; + text-align: center; + } + + .vivi23-landing--header { + background-color: $vivi23--fg-color; + border: 1px solid $vivi23--fg-color; + color: $vivi23--bg-color; + padding: 2em; + + .vivi23-landing--header-logo { + margin-top: 1em; + margin-bottom: 2em; + height: 75px; + } + } + + .vivi23-landing--content { + border: 1px solid darken($vivi23--bg-color, 20%); + border-top: none; + padding: 2em; + } + } + + /** Navbar **/ + .navbar-default { + background-color: $vivi23--nav-fg-prim-color; + + .navbar-brand .icon-ascribe-logo { + color: $vivi23--bg-color; + &:hover { + color: darken($vivi23--bg-color, 20%); + } + } + + } + + .navbar-nav > li > a, + .navbar-nav > li > a:focus, + .navbar-nav > li > .active a, + .navbar-nav > li > .active a:focus { + color: $vivi23--nav-bg-color; + } + + .navbar-nav > li > a:hover { + color: darken($vivi23--nav-bg-color, 20%); + } + + .navbar-nav > .active a, + .navbar-nav > .active a:hover, + .navbar-nav > .active a:focus { + background-color: $vivi23--nav-fg-prim-color; + border-bottom-color: $vivi23--nav-bg-color; + color: $vivi23--nav-bg-color; + } + + .navbar-nav > .open > a, + .dropdown-menu > .active > a, + .dropdown-menu > li > a { + color: $vivi23--nav-fg-prim-color; + background-color: $vivi23--nav-bg-color; + } + + .navbar-nav > .open > a:hover, + .navbar-nav > .open > a:focus, + .dropdown-menu > .active > a:hover, + .dropdown-menu > .active > a:focus, + .dropdown-menu > li > a:hover, + .dropdown-menu > li > a:focus { + color: lighten($vivi23--nav-fg-prim-color, 20%); + background-color: $vivi23--nav-highlight-color; + } + + .navbar-collapse.collapsing, + .navbar-collapse.collapse.in { + background-color: $vivi23--nav-bg-color; + + .navbar-nav > .open > a, + .navbar-nav > .active > a { + background-color: $vivi23--nav-highlight-color; + } + } + + .navbar-collapse.collapsing li a, + .navbar-collapse.collapse.in li a { + color: $vivi23--nav-fg-prim-color; + } + .navbar-collapse.collapse.in li a:not(.ascribe-powered-by):hover { + color: lighten($vivi23--nav-fg-prim-color, 20%); + background-color: $vivi23--nav-highlight-color; + } + + .navbar-toggle { + border-color: $vivi23--highlight-color; + + .icon-bar { + background-color: $vivi23--highlight-color; + } + } + + .navbar-toggle:hover, + .navbar-toggle:focus { + border-color: lighten($vivi23--highlight-color, 10%); + background-color: $vivi23--nav-fg-prim-color; + + .icon-bar { + background-color: lighten($vivi23--highlight-color, 10%); + } + } + + .notification-menu { + .dropdown-menu { + background-color: $vivi23--nav-bg-color; + } + + .notification-header { + background-color: $vivi23--nav-fg-sec-color; + border-top-width: 0; + color: $vivi23--nav-bg-color; + } + + .notification-action { + color: $vivi23--highlight-color; + } + } + + /** Buttons **/ + // reset disabled button styling for btn-default + .btn-default.disabled, + .btn-default.disabled:hover, + .btn-default.disabled:focus, + .btn-default.disabled.focus, + .btn-default.disabled:active, + .btn-default.disabled.active, + .btn-default[disabled], + .btn-default[disabled]:hover, + .btn-default[disabled]:focus, + .btn-default[disabled].focus, + .btn-default[disabled]:active, + .btn-default[disabled].active, + fieldset[disabled] .btn-default, + fieldset[disabled] .btn-default:hover, + fieldset[disabled] .btn-default:focus, + fieldset[disabled] .btn-default.focus, + fieldset[disabled] .btn-default:active, + fieldset[disabled] .btn-default.active { + background-color: lighten($vivi23--button-default-color, 30%); + border-color: lighten($vivi23--button-default-color, 30%); + } + + .btn-default { + background-color: $vivi23--button-default-color; + border-color: $vivi23--button-default-color; + + &:hover, + &:active, + &:focus, + &:active:hover, + &:active:focus, + &:active.focus, + &.active:hover, + &.active:focus, + &.active.focus { + background-color: lighten($vivi23--button-default-color, 30%); + border-color: lighten($vivi23--button-default-color, 30%); + } + } + + // disabled buttons + .btn-secondary.disabled, + .btn-secondary.disabled:hover, + .btn-secondary.disabled:focus, + .btn-secondary.disabled.focus, + .btn-secondary.disabled:active, + .btn-secondary.disabled.active, + .btn-secondary[disabled], + .btn-secondary[disabled]:hover, + .btn-secondary[disabled]:focus, + .btn-secondary[disabled].focus, + .btn-secondary[disabled]:active, + .btn-secondary[disabled].active, + fieldset[disabled] .btn-secondary, + fieldset[disabled] .btn-secondary:hover, + fieldset[disabled] .btn-secondary:focus, + fieldset[disabled] .btn-secondary.focus, + fieldset[disabled] .btn-secondary:active, + fieldset[disabled] .btn-secondary.active { + background-color: darken($vivi23--bg-color, 20%); + border-color: $vivi23--button-default-color; + } + + .btn-secondary { + border-color: $vivi23--button-default-color; + color: $vivi23--button-default-color; + + &:hover, + &:active, + &:focus, + &:active:hover, + &:active:focus, + &:active.focus, + &.active:hover, + &.active:focus, + &.active.focus { + background-color: $vivi23--button-default-color; + border-color: $vivi23--button-default-color; + color: $vivi23--bg-color; + } + } + + .btn-tertiary { + &:hover, + &:active, + &ctive:hover, + &.active:hover{ + background-color: $vivi23--highlight-color; + border-color: $vivi23--highlight-color; + color: $vivi23--highlight-color; + } + } + + /** Other components **/ + .ascribe-piece-list-toolbar .btn-ascribe-add { + display: none; + } + + .ascribe-footer { + display: none; + } + + .ascribe-accordion-list-table-toggle:hover { + color: $vivi23--fg-color; + } + + .request-action-badge { + color: $vivi23--fg-color; + } + + .acl-information-dropdown-list .title { + color: $vivi23--fg-color; + } + + // filter widget + .ascribe-piece-list-toolbar-filter-widget button { + background-color: transparent !important; + border-color: transparent !important; + color: $vivi23--button-default-color !important; + + &:hover, + &:active { + background-color: $vivi23--button-default-color !important; + border-color: $vivi23--button-default-color !important; + color: $vivi23--bg-color !important; + } + } + + .icon-ascribe-search { + color: $vivi23--fg-color; + } + + // forms + .ascribe-property-wrapper:hover { + border-left-color: rgba($vivi23--fg-color, 0.5); + } + + .ascribe-property textarea, + .ascribe-property input, + .search-bar > .form-group > .input-group input { + &::-webkit-input-placeholder { + color: rgba($vivi23--fg-color, 0.5); + } + &::-moz-placeholder { + color: rgba($vivi23--fg-color, 0.5); + } + &:-ms-input-placeholder { + color: rgba($vivi23--fg-color, 0.5); + } + &:-moz-placeholder { + color: rgba($vivi23--fg-color, 0.5); + } + } + + .ascribe-property { + > div, + > input, + > pre, + > select, + > span:not(.glyphicon), + > p, + > p > span, + > textarea { + color: $vivi23--fg-color; + } + } + + // global notification + .ascribe-global-notification-success { + background-color: lighten($vivi23--fg-color, 20%); + } + + // uploader progress + .ascribe-progress-bar > .progress-bar { + background-color: lighten($vivi23--fg-color, 20%); + } + + .ascribe-progress-bar span { + text-shadow: -1px 0 lighten($vivi23--fg-color, 20%), + 0 1px lighten($vivi23--fg-color, 20%), + 1px 0 lighten($vivi23--fg-color, 20%), + 0 -1px lighten($vivi23--fg-color, 20%); + } + + .action-file.icon-ascribe-ok, + .action-file.icon-ascribe-ok:hover { + color: lighten($vivi23--fg-color, 20%); + } + + // spinner + .spinner-circle { + border-color: $vivi23--fg-color; + } + .spinner-inner { + display: none; + } + .btn-secondary .spinner-circle { + border-color: $vivi23--bg-color; + } + + // video player + .ascribe-media-player .vjs-default-skin { + .vjs-play-progress, + .vjs-volume-level { + background-color: $vivi23--highlight-color; + } + } + + // pager + .pager li > a, + .pager li > span { + background-color: $vivi23--fg-color; + border-color: $vivi23--fg-color; + } + .pager .disabled > a, + .pager .disabled > span { + background-color: $vivi23--fg-color !important; + border-color: $vivi23--fg-color; + } + + // intercom + #intercom-container .intercom-launcher-button { + background-color: $vivi23--button-default-color !important; + border-color: $vivi23--button-default-color !important; + } +} diff --git a/sass/whitelabel/wallet/cc/cc_custom_style.scss b/sass/whitelabel/wallet/cc/cc_custom_style.scss index 44cb0dd1..774f5b27 100644 --- a/sass/whitelabel/wallet/cc/cc_custom_style.scss +++ b/sass/whitelabel/wallet/cc/cc_custom_style.scss @@ -207,4 +207,20 @@ $cc--button-color: $cc--nav-fg-prim-color; .client--cc .acl-information-dropdown-list .title { color: $cc--button-color; +} + +.client--cc .action-file.icon-ascribe-ok, +.client--cc .action-file.icon-ascribe-ok:hover { + color: $cc--button-color; +} + +.client--cc .ascribe-progress-bar span { + text-shadow: -1px 0 $cc--button-color, + 0 1px $cc--button-color, + 1px 0 $cc--button-color, + 0 -1px $cc--button-color; +} + +.client--cc .upload-button-wrapper > span { + color: $cc--button-color; } \ No newline at end of file diff --git a/sass/whitelabel/wallet/cyland/cyland_custom_style.scss b/sass/whitelabel/wallet/cyland/cyland_custom_style.scss index eaf45621..6c4223ac 100644 --- a/sass/whitelabel/wallet/cyland/cyland_custom_style.scss +++ b/sass/whitelabel/wallet/cyland/cyland_custom_style.scss @@ -59,40 +59,14 @@ $cyland--button-color: $cyland--nav-fg-prim-color; display: none; } - -.client--cyland .icon-ascribe-search{ +.client--cyland .icon-ascribe-search { color: $cyland--button-color; } -.client--cyland .ascribe-piece-list-toolbar .btn-ascribe-add{ +.client--cyland .ascribe-piece-list-toolbar .btn-ascribe-add { display: none; } -// disabled buttons -.client--cyland { - .btn-default.disabled, - .btn-default.disabled:hover, - .btn-default.disabled:focus, - .btn-default.disabled.focus, - .btn-default.disabled:active, - .btn-default.disabled.active, - .btn-default[disabled], - .btn-default[disabled]:hover, - .btn-default[disabled]:focus, - .btn-default[disabled].focus, - .btn-default[disabled]:active, - .btn-default[disabled].active, - fieldset[disabled] .btn-default, - fieldset[disabled] .btn-default:hover, - fieldset[disabled] .btn-default:focus, - fieldset[disabled] .btn-default.focus, - fieldset[disabled] .btn-default:active, - fieldset[disabled] .btn-default.active { - background-color: darken($cyland--button-color, 20%); - border-color: darken($cyland--button-color, 20%); - } -} - // buttons! // thought of the day: // "every great atrocity is the result of people just following orders" @@ -129,6 +103,26 @@ $cyland--button-color: $cyland--nav-fg-prim-color; } } + .btn-secondary { + background-color: white; + border-color: $cyland--button-color; + color: $cyland--button-color; + + &:hover, + &:active, + &:focus, + &:active:hover, + &:active:focus, + &:active.focus, + &.active:hover, + &.active:focus, + &.active.focus { + background-color: $cyland--button-color; + border-color: $cyland--button-color; + color: white; + } + } + .open > .btn-default.dropdown-toggle:hover, .open > .btn-default.dropdown-toggle:focus, .open > .btn-default.dropdown-toggle.focus, @@ -148,6 +142,48 @@ $cyland--button-color: $cyland--nav-fg-prim-color; } } +.client--ikonotv { + .btn-default.disabled, + .btn-default.disabled:hover, + .btn-default.disabled:focus, + .btn-default.disabled.focus, + .btn-default.disabled:active, + .btn-default.disabled.active, + .btn-default[disabled], + .btn-default[disabled]:hover, + .btn-default[disabled]:focus, + .btn-default[disabled].focus, + .btn-default[disabled]:active, + .btn-default[disabled].active, + fieldset[disabled] .btn-default, + fieldset[disabled] .btn-default:hover, + fieldset[disabled] .btn-default:focus, + fieldset[disabled] .btn-default.focus, + fieldset[disabled] .btn-default:active, + fieldset[disabled] .btn-default.active { + background-color: darken($cyland--button-color, 20%); + border-color: darken($cyland--button-color, 20%); + } +} + +// landing page +.client--cyland { + .route--landing { + display: table; + + > .container { + display: table-cell; + padding-bottom: 100px; + vertical-align: middle; + } + + .cyland-landing { + font-weight: normal; + text-align: center; + } + } +} + // spinner! .client--cyland { .btn-spinner { @@ -182,4 +218,20 @@ $cyland--button-color: $cyland--nav-fg-prim-color; .client--cyland .acl-information-dropdown-list .title { color: $cyland--button-color; +} + +.client--cyland .action-file.icon-ascribe-ok, +.client--cyland .action-file.icon-ascribe-ok:hover { + color: $cyland--nav-fg-prim-color; +} + +.client--cyland .ascribe-progress-bar span { + text-shadow: -1px 0 $cyland--nav-fg-prim-color, + 0 1px $cyland--nav-fg-prim-color, + 1px 0 $cyland--nav-fg-prim-color, + 0 -1px $cyland--nav-fg-prim-color; +} + +.client--cyland .upload-button-wrapper > span { + color: $cyland--button-color; } \ No newline at end of file diff --git a/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss b/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss index 70a5cd18..8f330911 100644 --- a/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss +++ b/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss @@ -411,6 +411,26 @@ $ikono--font: 'Helvetica Neue', 'Helvetica', sans-serif !important; } } + .btn-secondary { + background-color: white; + border-color: $ikono--button-color; + color: $ikono--button-color; + + &:hover, + &:active, + &:focus, + &:active:hover, + &:active:focus, + &:active.focus, + &.active:hover, + &.active:focus, + &.active.focus { + background-color: $ikono--button-color; + border-color: $ikono--button-color; + color: white; + } + } + .open > .btn-default.dropdown-toggle:hover, .open > .btn-default.dropdown-toggle:focus, .open > .btn-default.dropdown-toggle.focus, @@ -524,4 +544,20 @@ $ikono--font: 'Helvetica Neue', 'Helvetica', sans-serif !important; .client--ikonotv .acl-information-dropdown-list .title { color: $ikono--button-color; +} + +.client--ikonotv .action-file.icon-ascribe-ok, +.client--ikonotv .action-file.icon-ascribe-ok:hover { + color: $ikono--button-color; +} + +.client--ikonotv .ascribe-progress-bar span { + text-shadow: -1px 0 $ikono--button-color, + 0 1px $ikono--button-color, + 1px 0 $ikono--button-color, + 0 -1px $ikono--button-color; +} + +.client--ikonotv .upload-button-wrapper > span { + color: $ikono--button-color; } \ No newline at end of file diff --git a/sass/whitelabel/wallet/index.scss b/sass/whitelabel/wallet/index.scss index 024fb3cc..01c374d9 100644 --- a/sass/whitelabel/wallet/index.scss +++ b/sass/whitelabel/wallet/index.scss @@ -1,6 +1,7 @@ @import 'cc/cc_custom_style'; @import 'cyland/cyland_custom_style'; @import 'ikonotv/ikonotv_custom_style'; +@import '23vivi/23vivi_custom_style'; .ascribe-wallet-app { border-radius: 0; From 511abc6a333b353f4fd381d38d6a969e24f7e672 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 8 Dec 2015 20:57:40 +0100 Subject: [PATCH 14/22] Fix missing PropType --- js/components/ascribe_detail/further_details_fileuploader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/components/ascribe_detail/further_details_fileuploader.js b/js/components/ascribe_detail/further_details_fileuploader.js index 18050de9..40e40ff1 100644 --- a/js/components/ascribe_detail/further_details_fileuploader.js +++ b/js/components/ascribe_detail/further_details_fileuploader.js @@ -14,7 +14,7 @@ import { getLangText } from '../../utils/lang_utils'; -const { func, bool, number, object, arrayOf } = React.PropTypes; +const { func, bool, number, object, string, arrayOf } = React.PropTypes; let FurtherDetailsFileuploader = React.createClass({ propTypes: { From 17a3ecbd44d9e57ce7730e2fa0f418e1aea5fd2b Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 8 Dec 2015 20:48:39 +0100 Subject: [PATCH 15/22] Add thin cross icon to FileDragAndDropError --- .../file_drag_and_drop_error_dialog.js | 2 +- .../ascribe_upload_button/upload_button.js | 2 +- sass/ascribe_uploader.scss | 15 +++++++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) 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 index 8696cc7f..7f6d4460 100644 --- 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 @@ -57,7 +57,7 @@ let FileDragAndDropErrorDialog = React.createClass({ {this.getRetryButton('Retry')}
- +
    diff --git a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js index 94c85f4f..50bae1ef 100644 --- a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js +++ b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js @@ -61,7 +61,7 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = }, getUploadedFile() { - return this.props.filesToUpload.filter((file) => file.status === FileStatus.UPLOAD_SUCESSFUL)[0]; + return this.props.filesToUpload.filter((file) => file.status === FileStatus.UPLOAD_SUCCESSFUL)[0]; }, clearSelection() { diff --git a/sass/ascribe_uploader.scss b/sass/ascribe_uploader.scss index ea02eb07..5ae4e1e1 100644 --- a/sass/ascribe_uploader.scss +++ b/sass/ascribe_uploader.scss @@ -170,6 +170,7 @@ } .file-drag-and-drop-error { + color: #333333; margin-bottom: 25px; overflow: hidden; text-align: center; @@ -270,20 +271,23 @@ .file-drag-and-drop-error-icon-container { background-color: #eeeeee; - display: inline-block; + display: table; float: left; height: 104px; position: relative; width: 104px; vertical-align: top; - .file-drag-and-drop-error-icon { - position: absolute; + .icon-ascribe-thin-cross { + display: table-cell; + font-size: 5.5em; + vertical-align: middle; } &.file-drag-and-drop-error-icon-container-multiple-files { background-color: #d7d7d7; left: -15px; + margin-bottom: 24px; z-index: 1; &::before { @@ -310,7 +314,10 @@ z-index: 3; } - .file-drag-and-drop-error-icon { + .icon-ascribe-thin-cross { + left: 44px; + position: absolute; + top: 38px; z-index: 4; } } From e7cf9a44a0ac4f27c5ba64690ef616dd3d49b9eb Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 9 Dec 2015 15:57:01 +0100 Subject: [PATCH 16/22] Add simple check for determining if user's offline while uploading --- js/components/ascribe_uploader/react_s3_fine_uploader.js | 8 ++++++-- js/constants/error_constants.js | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index b11a877f..19705925 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -479,8 +479,12 @@ const ReactS3FineUploader = React.createClass({ } else { matchedErrorClass = testErrorAgainstAll({ type, reason, xhr }); - // If none found, show the next error message - if (!matchedErrorClass) { + // If none found, check the internet connection + // TODO: use a better mechanism for checking network state, ie. offline.js + if ('onLine' in window.navigator && !window.navigator.onLine) { + matchedErrorClass = ErrorClasses.upload.offline; + } else { + // Otherwise, show the next error message in the queue matchedErrorClass = ErrorQueueStore.getNextError('upload'); } } diff --git a/js/constants/error_constants.js b/js/constants/error_constants.js index 19e06c18..d649e551 100644 --- a/js/constants/error_constants.js +++ b/js/constants/error_constants.js @@ -121,6 +121,9 @@ const ErrorClasses = { }, 'contactUs': { 'prettifiedText': getLangText("We're having a really hard time with your upload. Please contact us for more help.") + }, + 'offline': { + 'prettifiedText': getLangText('It looks like your Internet connection might have gone down during the upload. Please check your connection and try again.') } }, 'default': { From 90c75ca4c547d7ec3779a95f5ce14fb290762e1f Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 9 Dec 2015 17:08:47 +0100 Subject: [PATCH 17/22] Change uploader error text --- js/constants/error_constants.js | 21 ++++++++++++++------- js/stores/error_queue_store.js | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/js/constants/error_constants.js b/js/constants/error_constants.js index d649e551..6ec0d70d 100644 --- a/js/constants/error_constants.js +++ b/js/constants/error_constants.js @@ -95,11 +95,10 @@ import { getLangText } from '../utils/lang_utils'; const ErrorClasses = { 'upload': { 'requestTimeTooSkewed': { - 'prettifiedText': getLangText('It appears that the time set on your computer is too ' + - 'inaccurate compared to your current local time. As a security ' + - 'measure, we check to make sure that our users are not falsifying ' + - "their registration times. Please synchronize your computer's " + - 'clock and try again.'), + 'prettifiedText': getLangText('Check your time and date preferences. Sometimes being off by even ' + + 'a few minutes from our servers can cause a glitch preventing your ' + + 'upload. For a quick fix, make sure that you have the “set date and ' + + 'time automatically” option selected.'), 'test': { 'xhr': { 'response': 'RequestTimeTooSkewed' @@ -107,7 +106,7 @@ const ErrorClasses = { } }, 'chunkSignatureError': { - 'prettifiedText': getLangText('We are experiencing some problems with uploads at the moment and ' + + 'prettifiedText': getLangText("We're experiencing some problems with uploads at the moment and " + 'are working to resolve them. Please try again in a few hours.'), 'test': 'Problem signing the chunk' }, @@ -117,7 +116,15 @@ const ErrorClasses = { 'prettifiedText': getLangText('Are you on a slow or unstable network? Uploading large files requires a fast Internet connection.') }, 'tryDifferentBrowser': { - 'prettifiedText': getLangText("We still can't seem to upload your file. Maybe try another browser?") + 'prettifiedText': getLangText("We're still having trouble uploading your file. It might be your " + + "browser; try a different browser or make sure you’re using the " + + 'latest version.') + }, + 'largeFileSize': { + 'prettifiedText': getLangText('We handle files up to 25GB but your Internet connection may not. ' + + 'If your file is large and your bandwidth is limited, it may take ' + + 'some time to complete. If your upload doesn’t seem to be in ' + + 'progress at all, try restarting the process.') }, 'contactUs': { 'prettifiedText': getLangText("We're having a really hard time with your upload. Please contact us for more help.") diff --git a/js/stores/error_queue_store.js b/js/stores/error_queue_store.js index 802b767d..c08f303e 100644 --- a/js/stores/error_queue_store.js +++ b/js/stores/error_queue_store.js @@ -8,11 +8,11 @@ import { ErrorClasses } from '../constants/error_constants.js'; class ErrorQueueStore { constructor() { - const { upload: { slowConnection, tryDifferentBrowser } } = ErrorClasses; + const { upload: { largeFileSize, slowConnection, tryDifferentBrowser } } = ErrorClasses; this.errorQueues = { 'upload': { - queue: [slowConnection, tryDifferentBrowser], + queue: [slowConnection, tryDifferentBrowser, largeFileSize], loop: true } }; From 482fa51e9e6dcf8c228bc206cf4fdad95f03f246 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 10 Dec 2015 18:56:19 +0100 Subject: [PATCH 18/22] Finalize text changes --- .../file_drag_and_drop_error_dialog.js | 2 +- js/constants/error_constants.js | 19 +++++++------------ js/stores/error_queue_store.js | 4 ++-- 3 files changed, 10 insertions(+), 15 deletions(-) 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 index 7f6d4460..0e4bfaa2 100644 --- 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 @@ -38,7 +38,7 @@ let FileDragAndDropErrorDialog = React.createClass({ return (

    Let us help you

    -

    {getLangText('Still having problems? Send us a message!')}

    +

    {getLangText('Still having problems? Send us a message.')}

    {this.getRetryButton('Contact us', true)}
    ); diff --git a/js/constants/error_constants.js b/js/constants/error_constants.js index 6ec0d70d..96d5f61f 100644 --- a/js/constants/error_constants.js +++ b/js/constants/error_constants.js @@ -95,10 +95,9 @@ import { getLangText } from '../utils/lang_utils'; const ErrorClasses = { 'upload': { 'requestTimeTooSkewed': { - 'prettifiedText': getLangText('Check your time and date preferences. Sometimes being off by even ' + - 'a few minutes from our servers can cause a glitch preventing your ' + - 'upload. For a quick fix, make sure that you have the “set date and ' + - 'time automatically” option selected.'), + 'prettifiedText': getLangText('Check your time and date preferences and select "set date and time ' + + 'automatically." Being off by a few minutes from our servers can ' + + 'prevent your upload.'), 'test': { 'xhr': { 'response': 'RequestTimeTooSkewed' @@ -112,20 +111,16 @@ const ErrorClasses = { }, // Fallback error tips - 'slowConnection': { - 'prettifiedText': getLangText('Are you on a slow or unstable network? Uploading large files requires a fast Internet connection.') + 'largeFileSize': { + 'prettifiedText': getLangText('We handle files up to 25GB but your Internet connection may not. With ' + + 'large files and limited bandwith, it may take some time to complete. If ' + + 'it doesn’t seem to progress at all, try restarting the process.') }, 'tryDifferentBrowser': { 'prettifiedText': getLangText("We're still having trouble uploading your file. It might be your " + "browser; try a different browser or make sure you’re using the " + 'latest version.') }, - 'largeFileSize': { - 'prettifiedText': getLangText('We handle files up to 25GB but your Internet connection may not. ' + - 'If your file is large and your bandwidth is limited, it may take ' + - 'some time to complete. If your upload doesn’t seem to be in ' + - 'progress at all, try restarting the process.') - }, 'contactUs': { 'prettifiedText': getLangText("We're having a really hard time with your upload. Please contact us for more help.") }, diff --git a/js/stores/error_queue_store.js b/js/stores/error_queue_store.js index c08f303e..4e6413c1 100644 --- a/js/stores/error_queue_store.js +++ b/js/stores/error_queue_store.js @@ -8,11 +8,11 @@ import { ErrorClasses } from '../constants/error_constants.js'; class ErrorQueueStore { constructor() { - const { upload: { largeFileSize, slowConnection, tryDifferentBrowser } } = ErrorClasses; + const { upload: { largeFileSize, tryDifferentBrowser } } = ErrorClasses; this.errorQueues = { 'upload': { - queue: [slowConnection, tryDifferentBrowser, largeFileSize], + queue: [largeFileSize, tryDifferentBrowser], loop: true } }; From acd7c0f0a3ff5524e66d15210da2bd292f17eee0 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 5 Feb 2016 15:17:59 +0100 Subject: [PATCH 19/22] Fix PR comments --- .../file_drag_and_drop.js | 8 ++--- .../file_drag_and_drop_error_dialog.js | 2 +- .../react_s3_fine_uploader.js | 34 +++++++++---------- .../react_s3_fine_uploader_utils.js | 33 ++++++++---------- js/constants/error_constants.js | 5 +-- js/utils/general_utils.js | 9 ++--- 6 files changed, 43 insertions(+), 48 deletions(-) diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js index 9ad1facb..37fc3f23 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js @@ -191,9 +191,9 @@ let FileDragAndDrop = React.createClass({ const hasFiles = filesToUpload .filter((file) => { return file.status !== FileStatus.DELETED && - file.status !== FileStatus.CANCELED && - file.status !== FileStatus.UPLOAD_FAILED && - file.size !== -1; + file.status !== FileStatus.CANCELED && + file.status !== FileStatus.UPLOAD_FAILED && + file.size !== -1; }) .length > 0; @@ -201,7 +201,7 @@ let FileDragAndDrop = React.createClass({ let hasError = showError && errorClass && failedFiles.length > 0; // if !== -2: triggers a FileDragAndDrop-global spinner - if(hashingProgress !== -2) { + if (hashingProgress !== -2) { return (

    {getLangText('Computing hash(es)... This may take a few minutes.')}

    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 index 0e4bfaa2..a88d2e35 100644 --- 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 @@ -37,7 +37,7 @@ let FileDragAndDropErrorDialog = React.createClass({ getContactUsDetail() { return (
    -

    Let us help you

    +

    {getLangText('Let us help you')}

    {getLangText('Still having problems? Send us a message.')}

    {this.getRetryButton('Contact us', true)}
    diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index 19705925..ca802b6a 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -208,7 +208,7 @@ const ReactS3FineUploader = React.createClass({ messages: { unsupportedBrowser: '

    ' + getLangText('Upload is not functional in IE7 as IE7 has no support for CORS!') + '

    ' }, - formatFileName: function(name){// fix maybe + formatFileName: function(name) { // fix maybe if (name !== undefined && name.length > 26) { name = name.slice(0, 15) + '...' + name.slice(-15); } @@ -224,7 +224,7 @@ const ReactS3FineUploader = React.createClass({ csrfToken: getCookie(AppConstants.csrftoken), errorState: { manualRetryAttempt: 0, - errorClass: undefined + errorClass: null }, uploadInProgress: false, @@ -472,19 +472,19 @@ const ReactS3FineUploader = React.createClass({ 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) { + 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 none found, check the internet connection - // TODO: use a better mechanism for checking network state, ie. offline.js - if ('onLine' in window.navigator && !window.navigator.onLine) { - matchedErrorClass = ErrorClasses.upload.offline; - } else { - // Otherwise, show the next error message in the queue + if (!matchedErrorClass) { + // If none found, show the next error message in the queue for upload errors matchedErrorClass = ErrorQueueStore.getNextError('upload'); } } @@ -1030,14 +1030,14 @@ const ReactS3FineUploader = React.createClass({ const filesToDisplay = filesToUpload.filter((file) => { return file.status !== FileStatus.DELETED && - file.status !== FileStatus.CANCELED && - file.status !== FileStatus.UPLOAD_FAILED && - file.size !== -1; + file.status !== FileStatus.CANCELED && + file.status !== FileStatus.UPLOAD_FAILED && + file.size !== -1; }); if ((enableLocalHashing && !uploadMethod) || !areAssetsEditable || - (showErrorPrompt && errorState.errorClass) || - (!multiple && filesToDisplay.length > 0)) { + (showErrorPrompt && errorState.errorClass) || + (!multiple && filesToDisplay.length > 0)) { return true; } else { return false; @@ -1047,7 +1047,7 @@ const ReactS3FineUploader = React.createClass({ getAllowedExtensions() { const { validation } = this.props; - if(validation && validation.allowedExtensions && validation.allowedExtensions.length > 0) { + if (validation && validation.allowedExtensions && validation.allowedExtensions.length > 0) { return transformAllowedExtensionsToInputAcceptProp(validation.allowedExtensions); } else { return null; diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js b/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js index 05fb565e..615e8879 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js @@ -1,9 +1,10 @@ 'use strict'; import fineUploader from 'fineUploader'; + // Re-export qq.status from FineUploader with an additional online // state that we use to keep track of files from S3. -export const FileStatus = Object.assign(fineUploader.status, { +export const FileStatus = Object.assign({}, fineUploader.status, { ONLINE: 'online' }); @@ -17,11 +18,11 @@ export const formSubmissionValidation = { atLeastOneUploadedFile(files) { files = files.filter((file) => { return file.status !== FileStatus.DELETED && - file.status !== FileStatus.CANCELED && - file.status != FileStatus.UPLOADED_FAILED + file.status !== FileStatus.CANCELED && + file.status != FileStatus.UPLOADED_FAILED }); - if (files.length > 0 && files[0].status === FileStatus.UPLOAD_SUCCESSFUL) { + if (files.length && files[0].status === FileStatus.UPLOAD_SUCCESSFUL) { return true; } else { return false; @@ -35,13 +36,9 @@ export const formSubmissionValidation = { * @return {boolean} [description] */ fileOptional(files) { - let uploadingFiles = files.filter((file) => file.status === FileStatus.SUBMITTING); + const uploadingFiles = files.filter((file) => file.status === FileStatus.SUBMITTING); - if (uploadingFiles.length === 0) { - return true; - } else { - return false; - } + return uploadFiles.length === 0; } }; @@ -52,8 +49,8 @@ export const formSubmissionValidation = { */ export function displayValidFilesFilter(file) { return file.status !== FileStatus.DELETED && - file.status !== FileStatus.CANCELED && - file.status !== FileStatus.UPLOAD_FAILED; + file.status !== FileStatus.CANCELED && + file.status !== FileStatus.UPLOAD_FAILED; } /** @@ -63,8 +60,8 @@ export function displayValidFilesFilter(file) { */ export function displayRemovedFilesFilter(file) { return file.status === FileStatus.DELETED || - file.status === FileStatus.CANCELED || - file.status === FileStatus.UPLOAD_FAILED; + file.status === FileStatus.CANCELED || + file.status === FileStatus.UPLOAD_FAILED; } @@ -75,9 +72,9 @@ export function displayRemovedFilesFilter(file) { */ export function displayValidProgressFilesFilter(file) { return file.status !== FileStatus.DELETED && - file.status !== FileStatus.CANCELED && - file.status !== FileStatus.UPLOAD_FAILED && - file.status !== FileStatus.ONLINE; + file.status !== FileStatus.CANCELED && + file.status !== FileStatus.UPLOAD_FAILED && + file.status !== FileStatus.ONLINE; } @@ -93,7 +90,7 @@ export function displayValidProgressFilesFilter(file) { */ export function transformAllowedExtensionsToInputAcceptProp(allowedExtensions) { // add a dot in front of the extension - let prefixedAllowedExtensions = allowedExtensions.map((ext) => '.' + ext); + const prefixedAllowedExtensions = allowedExtensions.map((ext) => '.' + ext); // generate a comma separated list to add them to the DOM element // See: http://stackoverflow.com/questions/4328947/limit-file-format-when-using-input-type-file diff --git a/js/constants/error_constants.js b/js/constants/error_constants.js index 96d5f61f..4df095ee 100644 --- a/js/constants/error_constants.js +++ b/js/constants/error_constants.js @@ -179,7 +179,7 @@ function getPrettifiedError(error, errorClass) { * @return {(object)} Matched error class */ function testErrorAgainstAll(error) { - const type = error.type != null ? error.type : 'default'; + const type = error.type || 'default'; const errorGroup = ErrorClasses[type]; return Object @@ -198,7 +198,8 @@ function testErrorAgainstAll(error) { * @return {(object)} Returns the given class if the test succeeds. */ function testErrorAgainstClass(error, errorClass) { - // Automatically fail classes if no tests present + // Automatically fail classes if no tests present, since some of the error classes + // may not have an error to test against. if (!errorClass.test) { return; } diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index a3336d80..ea36bff9 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -223,15 +223,12 @@ export function omitFromObject(obj, filter) { * By default, applies strict equality using === * @return {boolean} True if obj matches the "match" object */ -export function deepMatchObject(obj, match, testFn) { +export function deepMatchObject(obj, match, testFn = (objProp, matchProp) => objProp === matchProp) { if (typeof match !== 'object') { throw new Error('Your specified match argument was not an object'); } - if (typeof testFn !== 'function') { - testFn = (objProp, matchProp) => { - return objProp === matchProp; - }; + throw new Error('Your specified test function was not a function'); } return Object @@ -239,7 +236,7 @@ export function deepMatchObject(obj, match, testFn) { .reduce((result, matchKey) => { if (!result) { return false; } - const objProp = obj[matchKey]; + const objProp = obj && obj[matchKey]; const matchProp = match[matchKey]; if (typeof matchProp === 'object') { From 91f9e83e814282695ccc7817b7b1567977b736ec Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 8 Feb 2016 10:40:46 +0100 Subject: [PATCH 20/22] Fix FileDragAndDropDialog merge mistake --- .../file_drag_and_drop_dialog.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js index 923f1572..6a59b8fc 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js @@ -102,19 +102,19 @@ let FileDragAndDropDialog = React.createClass({ ) ]; } - - return ( -
    -
    - {dialogElement} -
    - {/* Hide the uploader and just show that there's been on files uploaded yet when printing */} -

    - {getLangText('No files uploaded')} -

    -
    - ); } + + return ( +
    +
    + {dialogElement} +
    + {/* Hide the uploader and just show that there's been on files uploaded yet when printing */} +

    + {getLangText('No files uploaded')} +

    +
    + ); } }); From 537d6d52e84c4977ddf0cda0a1ccbc3aafddf8e8 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 8 Feb 2016 10:41:36 +0100 Subject: [PATCH 21/22] Small fixes for warnings --- js/components/ascribe_routes/proxy_handler.js | 6 +++--- .../ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js | 2 ++ js/components/ascribe_uploader/react_s3_fine_uploader.js | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/js/components/ascribe_routes/proxy_handler.js b/js/components/ascribe_routes/proxy_handler.js index 52084e6b..882fe65c 100644 --- a/js/components/ascribe_routes/proxy_handler.js +++ b/js/components/ascribe_routes/proxy_handler.js @@ -17,7 +17,7 @@ const WHEN_ENUM = ['loggedIn', 'loggedOut']; * * @param {enum/string} options.when ('loggedIn' || 'loggedOut') */ -export function AuthRedirect({to, when}) { +export function AuthRedirect({ to, when }) { // validate `when`, must be contained in `WHEN_ENUM`. // Throw an error otherwise. if (WHEN_ENUM.indexOf(when) === -1) { @@ -80,8 +80,8 @@ export function ProxyHandler(...redirectFunctions) { displayName: 'ProxyHandler', propTypes: { - // Provided from AscribeApp - currentUser: React.PropTypes.object.isRequired, + // Provided from AscribeApp, after the routes have been initialized + currentUser: React.PropTypes.object, whitelabel: React.PropTypes.object, // Provided from router diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js index 6a59b8fc..5df3558c 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js @@ -84,6 +84,7 @@ let FileDragAndDropDialog = React.createClass({ dialogElement = [ this.getDragDialog(fileClassToUpload.plural), ( {getLangText('choose %s to upload', fileClassToUpload.plural)} @@ -96,6 +97,7 @@ let FileDragAndDropDialog = React.createClass({ dialogElement = [ this.getDragDialog(fileClassToUpload.singular), ( {dialog} diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index ff44b294..90a72157 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -487,7 +487,7 @@ const ReactS3FineUploader = React.createClass({ return (enableLocalHashing && !uploadMethod) || !areAssetsEditable || (showErrorPrompt && errorState.errorClass) || - (!multiple && filesToDisplay.length); + (!multiple && filesToDisplay.length > 0); }, isFileValid(file) { From 028058a12f01af117674b307991063985c4bc55d Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 8 Feb 2016 11:03:41 +0100 Subject: [PATCH 22/22] Small things that could only be fixed after merging --- js/components/ascribe_uploader/react_s3_fine_uploader.js | 4 +--- js/constants/error_constants.js | 9 ++++++--- js/constants/uploader_constants.js | 3 +++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index 90a72157..cbdc6ab1 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -15,6 +15,7 @@ import GlobalNotificationActions from '../../actions/global_notification_actions import AppConstants from '../../constants/application_constants'; import { ErrorClasses, testErrorAgainstAll } from '../../constants/error_constants'; +import { RETRY_ATTEMPT_TO_SHOW_CONTACT_US } from '../../constants/uploader_constants'; import { displayValidFilesFilter, FileStatus, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils'; import { getCookie } from '../../utils/fetch_api_utils'; @@ -34,9 +35,6 @@ const { shape, element, arrayOf } = React.PropTypes; -// After 5 manual retries, show the contact us prompt. -const RETRY_ATTEMPT_TO_SHOW_CONTACT_US = 5; - const ReactS3FineUploader = React.createClass({ propTypes: { areAssetsDownloadable: bool, diff --git a/js/constants/error_constants.js b/js/constants/error_constants.js index 4df095ee..2baeaf10 100644 --- a/js/constants/error_constants.js +++ b/js/constants/error_constants.js @@ -1,5 +1,7 @@ 'use strict' +import { validationParts } from './uploader_constants'; + import { deepMatchObject } from '../utils/general_utils'; import { getLangText } from '../utils/lang_utils'; @@ -112,9 +114,10 @@ const ErrorClasses = { // Fallback error tips 'largeFileSize': { - 'prettifiedText': getLangText('We handle files up to 25GB but your Internet connection may not. With ' + - 'large files and limited bandwith, it may take some time to complete. If ' + - 'it doesn’t seem to progress at all, try restarting the process.') + 'prettifiedText': getLangText(`We handle files up to ${validationParts.sizeLimit.default / 1000000000}GB ` + + 'but your Internet connection may not. With large files and limited ' + + 'bandwith, it may take some time to complete. If it doesn’t seem to ' + + 'progress at all, try restarting the process.') }, 'tryDifferentBrowser': { 'prettifiedText': getLangText("We're still having trouble uploading your file. It might be your " + diff --git a/js/constants/uploader_constants.js b/js/constants/uploader_constants.js index c2f8559b..ae7787fe 100644 --- a/js/constants/uploader_constants.js +++ b/js/constants/uploader_constants.js @@ -30,3 +30,6 @@ export const validationTypes = { sizeLimit: sizeLimit.thumbnail } }; + +// Number of manual retries before showing a contact us screen on the uploader. +export const RETRY_ATTEMPT_TO_SHOW_CONTACT_US = 5;