From 3e28d6f4210aa508b36044ee40983cc0b0fed4d1 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 23 Nov 2015 17:12:47 +0100 Subject: [PATCH 001/197] 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 002/197] 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 003/197] 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 004/197] 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 005/197] 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 006/197] 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 007/197] 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 008/197] 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 009/197] 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 010/197] 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 011/197] 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 012/197] 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 013/197] 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 014/197] 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 015/197] 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 016/197] 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 017/197] 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 1ee9a4c7a142eff28904b0da8dc1a51a0a28c36a Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 9 Dec 2015 18:29:43 +0100 Subject: [PATCH 018/197] Hide download button on the marketplace additional data form when it's not inline --- .../ascribe_detail/further_details_fileuploader.js | 6 ++++-- .../file_drag_and_drop_preview_image.js | 2 +- .../market/market_forms/market_additional_data_form.js | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/js/components/ascribe_detail/further_details_fileuploader.js b/js/components/ascribe_detail/further_details_fileuploader.js index b044bbbc..60769598 100644 --- a/js/components/ascribe_detail/further_details_fileuploader.js +++ b/js/components/ascribe_detail/further_details_fileuploader.js @@ -22,11 +22,13 @@ let FurtherDetailsFileuploader = React.createClass({ submitFile: React.PropTypes.func, isReadyForFormSubmission: React.PropTypes.func, editable: React.PropTypes.bool, - multiple: React.PropTypes.bool + multiple: React.PropTypes.bool, + areAssetsDownloadable: React.PropTypes.bool }, getDefaultProps() { return { + areAssetsDownloadable: true, label: getLangText('Additional files'), multiple: false }; @@ -89,7 +91,7 @@ let FurtherDetailsFileuploader = React.createClass({ 'X-CSRFToken': getCookie(AppConstants.csrftoken) } }} - areAssetsDownloadable={true} + areAssetsDownloadable={this.props.areAssetsDownloadable} areAssetsEditable={this.props.editable} multiple={this.props.multiple} /> diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_image.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_image.js index 927a5b22..0d8d366c 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_image.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_image.js @@ -49,7 +49,7 @@ const FileDragAndDropPreviewImage = React.createClass({ }; let actionSymbol; - + // only if assets are actually downloadable, there should be a download icon if the process is already at // 100%. If not, no actionSymbol should be displayed if(progress === 100 && areAssetsDownloadable) { 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 index d136c9cf..2a22e743 100644 --- 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 @@ -173,12 +173,13 @@ let MarketAdditionalDataForm = React.createClass({ disabled={!this.props.editable || !piece.acl.acl_edit}> {heading} Date: Thu, 10 Dec 2015 13:01:48 +0100 Subject: [PATCH 019/197] Avoid unauthorized attempts at fetching ratings if user isn't an admin, judge, or jury --- .../ascribe_detail/prize_piece_container.js | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) 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 982af7b0..4f5c9107 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 @@ -112,7 +112,7 @@ let PieceContainer = React.createClass({ render() { if(this.state.piece && this.state.piece.id) { /* - + This really needs a refactor! - Tim @@ -122,7 +122,7 @@ let PieceContainer = React.createClass({ let artistName = ((this.state.currentUser.is_jury && !this.state.currentUser.is_judge) || (this.state.currentUser.is_judge && !this.state.piece.selected )) ? null : this.state.piece.artist_name; - + // Only show the artist email if you are a judge and the piece is shortlisted let artistEmail = (this.state.currentUser.is_judge && this.state.piece.selected ) ? : null; @@ -146,7 +146,7 @@ let PieceContainer = React.createClass({ - +

    {this.state.piece.title}

    @@ -176,8 +176,8 @@ let PieceContainer = React.createClass({ let NavigationHeader = React.createClass({ propTypes: { - piece: React.PropTypes.object, - currentUser: React.PropTypes.object + piece: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object.isRequired }, render() { @@ -213,9 +213,9 @@ let NavigationHeader = React.createClass({ let PrizePieceRatings = React.createClass({ propTypes: { - loadPiece: React.PropTypes.func, - piece: React.PropTypes.object, - currentUser: React.PropTypes.object + loadPiece: React.PropTypes.func.isRequired, + piece: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object.isRequired }, getInitialState() { @@ -227,9 +227,15 @@ let PrizePieceRatings = React.createClass({ componentDidMount() { PrizeRatingStore.listen(this.onChange); - PrizeRatingActions.fetchOne(this.props.piece.id); - PrizeRatingActions.fetchAverage(this.props.piece.id); PieceListStore.listen(this.onChange); + + this.fetchRatingsIfAuthorized(); + }, + + componentWillReceiveProps(nextProps) { + if (nextProps.currentUser.email !== this.props.currentUser.email) { + this.fetchRatingsIfAuthorized(); + } }, componentWillUnmount() { @@ -258,6 +264,21 @@ let PrizePieceRatings = React.createClass({ } }, + fetchRatingsIfAuthorized() { + const { + currentUser: { + is_admin: isAdmin, + is_judge: isJudge, + is_jury: isJury + }, + piece: { id: pieceId } } = this.props; + + if (isAdmin || isJudge || isJury) { + PrizeRatingActions.fetchOne(pieceId); + PrizeRatingActions.fetchAverage(pieceId); + } + }, + onRatingClick(event, args) { event.preventDefault(); PrizeRatingActions.createRating(this.props.piece.id, args.rating).then( @@ -425,12 +446,11 @@ let PrizePieceRatings = React.createClass({ let PrizePieceDetails = React.createClass({ propTypes: { - piece: React.PropTypes.object + piece: React.PropTypes.object.isRequired }, render() { - if (this.props.piece - && this.props.piece.prize + if (this.props.piece.prize && this.props.piece.prize.name && Object.keys(this.props.piece.extra_data).length !== 0){ return ( From e96e7ffa2b5525319461a0429cfd57329a690c57 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 10 Dec 2015 18:04:46 +0100 Subject: [PATCH 020/197] Use `null` to denote an empty React element instead of `{}` --- js/components/ascribe_forms/form.js | 2 +- .../components/pr_forms/pr_register_piece_form.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/js/components/ascribe_forms/form.js b/js/components/ascribe_forms/form.js index a2b7b9bc..f126b532 100644 --- a/js/components/ascribe_forms/form.js +++ b/js/components/ascribe_forms/form.js @@ -208,7 +208,7 @@ let Form = React.createClass({ if (this.state.submitted){ return this.props.spinner; } - if (this.props.buttons){ + if ('buttons' in this.props) { return this.props.buttons; } let buttons = null; diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js index 41f2c25a..34ce2f04 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js @@ -183,7 +183,7 @@ const PRRegisterPieceForm = React.createClass({ return (
    Date: Thu, 10 Dec 2015 18:56:19 +0100 Subject: [PATCH 021/197] 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 75fe89a0ff79ef06fc15e86ae1ed480619470908 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 16 Dec 2015 14:48:43 +0100 Subject: [PATCH 022/197] Limit edition creation to between 1-100 --- js/components/ascribe_forms/create_editions_form.js | 7 ++++--- js/components/register_piece.js | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/js/components/ascribe_forms/create_editions_form.js b/js/components/ascribe_forms/create_editions_form.js index 1e8ac23e..b2bf3c1c 100644 --- a/js/components/ascribe_forms/create_editions_form.js +++ b/js/components/ascribe_forms/create_editions_form.js @@ -19,7 +19,7 @@ let CreateEditionsForm = React.createClass({ pieceId: React.PropTypes.number }, - getFormData(){ + getFormData() { return { piece_id: parseInt(this.props.pieceId, 10) }; @@ -58,11 +58,12 @@ let CreateEditionsForm = React.createClass({ + min={1} + max={100} />
    ); } }); -export default CreateEditionsForm; \ No newline at end of file +export default CreateEditionsForm; diff --git a/js/components/register_piece.js b/js/components/register_piece.js index 8211e91e..50da9b02 100644 --- a/js/components/register_piece.js +++ b/js/components/register_piece.js @@ -65,7 +65,7 @@ let RegisterPiece = React.createClass( { this.setState(state); }, - handleSuccess(response){ + handleSuccess(response) { let notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); @@ -94,7 +94,8 @@ let RegisterPiece = React.createClass( { + min={1} + max={100} />
    ); } From fb917f1a09d2d79819f97a2a8224d6bc4bae851c Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 16 Dec 2015 14:50:39 +0100 Subject: [PATCH 023/197] Reset the Property value when its checkbox is unselected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user doesn’t want to apply the property, so it should reset to whatever it is by default. --- js/components/ascribe_forms/property.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/components/ascribe_forms/property.js b/js/components/ascribe_forms/property.js index 432591bf..5b46aa1f 100644 --- a/js/components/ascribe_forms/property.js +++ b/js/components/ascribe_forms/property.js @@ -241,6 +241,9 @@ const Property = React.createClass({ handleCheckboxToggle() { this.setExpanded(!this.state.expanded); + this.setState({ + value: this.state.initialValue + }); }, renderChildren(style) { From 9f553f973cda61afdf267251a1f5b267acef8322 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 16 Dec 2015 15:45:54 +0100 Subject: [PATCH 024/197] Check for the checkbox being unchecked before reseting value --- js/components/ascribe_forms/property.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/js/components/ascribe_forms/property.js b/js/components/ascribe_forms/property.js index 5b46aa1f..6d46a9d3 100644 --- a/js/components/ascribe_forms/property.js +++ b/js/components/ascribe_forms/property.js @@ -240,10 +240,17 @@ const Property = React.createClass({ }, handleCheckboxToggle() { - this.setExpanded(!this.state.expanded); - this.setState({ - value: this.state.initialValue - }); + const expanded = !this.state.expanded; + + this.setExpanded(expanded); + + // Reset the value to be the initial value when the checkbox is unticked since the + // user doesn't want to specify their own value. + if (!expanded) { + this.setState({ + value: this.state.initialValue + }); + } }, renderChildren(style) { From bf244e2bfaf8173b5552d36e7721d6e99040d4c5 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 16 Dec 2015 19:03:01 +0100 Subject: [PATCH 025/197] Add padding-bottom to ascribe-property instead of ascribe-property-wrapper Avoids having to style all the collapsible Properties with `padding-bottom: 0` to center labels when collapsed. --- js/components/ascribe_forms/form_consign.js | 3 +-- js/components/ascribe_forms/form_copyright_association.js | 5 ++--- js/components/ascribe_forms/form_loan.js | 3 +-- .../ascribe_forms/form_send_contract_agreement.js | 3 +-- js/components/ascribe_forms/form_signup.js | 5 ++--- js/components/ascribe_forms/form_submit_to_prize.js | 3 +-- .../ascribe_forms/input_contract_agreement_checkbox.js | 2 +- js/components/ascribe_settings/account_settings.js | 5 ++--- .../components/pr_forms/pr_register_piece_form.js | 3 +-- .../prize/simple_prize/components/prize_register_piece.js | 3 +-- sass/ascribe_property.scss | 8 ++------ 11 files changed, 15 insertions(+), 28 deletions(-) diff --git a/js/components/ascribe_forms/form_consign.js b/js/components/ascribe_forms/form_consign.js index 2f0ebf05..c92f4b6f 100644 --- a/js/components/ascribe_forms/form_consign.js +++ b/js/components/ascribe_forms/form_consign.js @@ -115,8 +115,7 @@ let ConsignForm = React.createClass({ + className="ascribe-property-collapsible-toggle"> diff --git a/js/components/ascribe_forms/form_copyright_association.js b/js/components/ascribe_forms/form_copyright_association.js index c378ddba..124a980a 100644 --- a/js/components/ascribe_forms/form_copyright_association.js +++ b/js/components/ascribe_forms/form_copyright_association.js @@ -48,8 +48,7 @@ let CopyrightAssociationForm = React.createClass({ + label={getLangText('Copyright Association')}> From e8bdeffad868fc2aae6e99bbe1243695d7ec7e32 Mon Sep 17 00:00:00 2001 From: vrde Date: Sat, 19 Dec 2015 17:40:53 +0100 Subject: [PATCH 032/197] Refactor, add setup.py --- .env-template | 4 +- package.json | 2 + {tests => test}/.eslintrc | 0 {tests => test}/README.md | 90 ++++++++++++++++++++++++++++++++--- test/setup.js | 33 +++++++++++++ {tests => test}/test-login.js | 8 +--- 6 files changed, 122 insertions(+), 15 deletions(-) rename {tests => test}/.eslintrc (100%) rename {tests => test}/README.md (55%) create mode 100644 test/setup.js rename {tests => test}/test-login.js (78%) diff --git a/.env-template b/.env-template index 593923ec..a836e649 100644 --- a/.env-template +++ b/.env-template @@ -1,2 +1,2 @@ -ONION_SAUCELABS_USER=ascribe -ONION_SAUCELABS_APIKEY= +SAUCE_USERNAME=ascribe +SAUCE_ACCESS_KEY= diff --git a/package.json b/package.json index b2bbd68c..ba8bfc45 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "scripts": { "lint": "eslint ./js", + "preinstall": "export SAUCE_CONNECT_DOWNLOAD_ON_INSTALL=true", "postinstall": "npm run build", "build": "gulp build --production", "start": "node server.js" @@ -40,6 +41,7 @@ "dotenv": "^1.2.0", "jest-cli": "^0.4.0", "mocha": "^2.3.4", + "sauce-connect-launcher": "^0.13.0", "wd": "^0.4.0" }, "dependencies": { diff --git a/tests/.eslintrc b/test/.eslintrc similarity index 100% rename from tests/.eslintrc rename to test/.eslintrc diff --git a/tests/README.md b/test/README.md similarity index 55% rename from tests/README.md rename to test/README.md index b95df74c..2c8d6d46 100644 --- a/tests/README.md +++ b/test/README.md @@ -34,7 +34,11 @@ The components involved are: - **[Selenium WebDriver](https://www.npmjs.com/package/wd)**: it's a library that can control a browser. You can use the **WebDriver** to load new URLs, click around, fill out forms, submit forms etc. It's basically a way to - control remotely a browser. There are other implementations in Python, PHP, + control remotely a browser. The protocol (language agnostic) is called + [JsonWire](https://code.google.com/p/selenium/wiki/JsonWireProtocol), `wd` + wraps it and gives you a nice + [API](https://github.com/admc/wd/blob/master/doc/jsonwire-full-mapping.md) + you can use in JavaScript. There are other implementations in Python, PHP, Java, etc. Also, a **WebDriver** can be initialized with a list of [desired capabilities](https://code.google.com/p/selenium/wiki/DesiredCapabilities) describing which features (like the platform, browser name and version) you @@ -53,9 +57,12 @@ The components involved are: systems. (They do other things, check out their websites). - **[SauceConnect](https://wiki.saucelabs.com/display/DOCS/Setting+Up+Sauce+Connect)**: - it allows Saucelabs to connect to your `localhost` to test the app. (There - is also a [Node.js wrapper](https://www.npmjs.com/package/sauce-connect), so - you can use it programmatically within your code for tests). + is a Java software by Saucelabs to connect to your `localhost` to test the + application. There is also a Node.js wrapper + [sauce-connect-launcher](https://www.npmjs.com/package/sauce-connect-launcher), + so you can use it programmatically within your code for tests. Please note + that this module is just a wrapper around the actual software. Running `npm + install` should install the additional Java software as well. On the JavaScript side, we use: @@ -74,8 +81,49 @@ On the JavaScript side, we use: environment variables from `.env` into `process.env`. +## How to set up your `.env` config file +In the root of this repository there is a file called `.env-template`. Create a +copy and call it `.env`. This file will store some values we need to connect to +Saucelabs. + +There are two values to be set: + - `SAUCE_ACCESS_KEY` + - `SAUCE_USERNAME` + +The two keys are the [default +ones](https://github.com/admc/wd#environment-variables-for-saucelabs) used by +many products related to Saucelabs. This allow us to keep the configuration +fairly straightforward and simple. + +After logging in to https://saucelabs.com/, you can find your **api key** under +the **My Account**. Copy paste the value in your `.env` file. + + ## Anatomy of a test +First, you need to learn how [Mocha](https://mochajs.org/) works. Brew a coffee +(or tea, if coffee is not your cup of tea), sit down and read the docs. + +Done? Great, let's move on and analyze how a test is written. + +From a very high level, the flow of a test is the following: + 1. load a page with a specific URL + 2. do something on the page (click a button, submit a form, etc.) + 3. maybe wait some seconds, or wait if something has changed + 4. check if the new page contains some text you expect to be there + +This is not set in stone, so go crazy if you want. But keep in mind that we +have a one page application, there might be some gotchas on how to wait for +stuff to happen. I suggest you to read the section [Wait for +something](https://github.com/admc/wd#waiting-for-something) to understand +better which tools you have to solve this problem. +Again, take a look to the [`wd` implementation of the JsonWire +protocol](https://github.com/admc/wd/blob/master/doc/jsonwire-full-mapping.md) +to know all the methods you can use to control the browser. + + +Import the libraries we need. + ```javascript 'use strict'; @@ -84,33 +132,61 @@ require('dotenv').load(); const wd = require('wd'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); +``` + + +Set up `chai` to use `chaiAsPromised`. + +```javascript chai.use(chaiAsPromised); chai.should(); ``` +`browser` is the main object to interact with Saucelab "real" browsers. We will +use this object a lot. It allow us to load pages, click around, check if a +specific text is present etc. ```javascript describe('Login logs users in', function() { let browser; +``` +Create the driver to control the browser. +```javascript before(function() { - browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80, - process.env.ONION_SAUCELABS_USER, - process.env.ONION_SAUCELABS_APIKEY, + browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80); return browser.init({ browserName: 'chrome' }); }); +``` +This function will be executed before each `it` function. Here we point the browser to a specific URL. + +```javascript beforeEach(function() { return browser.get('http://www.ascribe.ninja/app/login'); }); +``` +While this function will be executed after each `it` function. `quit` will destroy the browser session. + +```javascript after(function() { return browser.quit(); }); +``` +The actual test. We query the `browser` object to get the title of the page. +Note that `.title()` returns a `promise` **but**, since we are using +`chaiAsPromised`, we have some syntactic sugar to handle the promise in line, +without writing new functions. + +```javascript it('should contain "Log in" in the title', function() { return browser.title().should.become('Log in'); }); }); ``` + +## How to run the test suite + diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 00000000..0313a4ae --- /dev/null +++ b/test/setup.js @@ -0,0 +1,33 @@ +'use strict'; + +require('dotenv').load(); + +const sauceConnectLauncher = require('sauce-connect-launcher'); +let globalSauceProcess; + + +if (process.env.SAUCE_AUTO_CONNECT) { + before(function(done) { + // Creating the tunnel takes a bit of time. For this case we can safely disable it. + this.timeout(0); + + sauceConnectLauncher(function (err, sauceConnectProcess) { + if (err) { + console.error(err.message); + return; + } + globalSauceProcess = sauceConnectProcess; + done(); + }); + }); + + + after(function (done) { + // Creating the tunnel takes a bit of time. For this case we can safely disable it. + this.timeout(0); + + if (globalSauceProcess) { + globalSauceProcess.close(done); + } + }); +} diff --git a/tests/test-login.js b/test/test-login.js similarity index 78% rename from tests/test-login.js rename to test/test-login.js index 249a26c2..82bdad9d 100644 --- a/tests/test-login.js +++ b/test/test-login.js @@ -1,7 +1,5 @@ 'use strict'; -require('dotenv').load(); - const wd = require('wd'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); @@ -10,13 +8,11 @@ chai.should(); describe('Login logs users in', function() { + this.timeout(0); let browser; before(function() { - browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80, - process.env.ONION_SAUCELABS_USER, - process.env.ONION_SAUCELABS_APIKEY); - + browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80); return browser.init({ browserName: 'chrome' }); }); From 2e3e5d9a60b1e09718574a9e5adf9349337f7cb7 Mon Sep 17 00:00:00 2001 From: vrde Date: Sat, 19 Dec 2015 17:54:28 +0100 Subject: [PATCH 033/197] Small updates to the readme --- test/README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/README.md b/test/README.md index 2c8d6d46..0aab26bd 100644 --- a/test/README.md +++ b/test/README.md @@ -11,15 +11,16 @@ You will notice that the setup is a bit convoluted. This section will explain you why. Testing single functions in JavaScript is not that hard (if you don't need to interact with the DOM), and can be easily achieved using frameworks like [Mocha](https://mochajs.org/). Integration and cross browser testing is, -on the other side, a huge [PITA](https://saucelabs.com/selenium/selenium-grid). -Moreover, "browser testing" includes also "mobile browser testing". Moreover, -the same browser (type and version) can behave in a different way on different -operating systems. +on the other side, a huge PITA. Moreover, "browser testing" includes also +"mobile browser testing". On the top of that the same browser (type and +version) can behave in a different way on different operating systems. To achieve that you can have your own cluster of machines with different operating systems and browsers or, if you don't want to spend the rest of your life configuring an average of 100 browsers for each different operating -system, you can pay someone else to do that. +system, you can pay someone else to do that. Check out [this +article](https://saucelabs.com/selenium/selenium-grid) if you want to know why +using Selenium Grid is better than a DIY approach. We decided to use [saucelabs](https://saucelabs.com/) cloud (they support [over 700 combinations](https://saucelabs.com/platforms/) of operating systems and @@ -77,7 +78,7 @@ On the JavaScript side, we use: writing callbacks but just chaining operators. Check out their `README` on GitHub to see an example. - - [dotenv](https://github.com/motdotla/dotenv): a super nice package to loads + - [dotenv](https://github.com/motdotla/dotenv): a super nice package to load environment variables from `.env` into `process.env`. From 418022dff506025313bd315d6a6756dca08ffe08 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 21 Dec 2015 11:40:59 +0100 Subject: [PATCH 034/197] wip --- .../further_details_fileuploader.js | 2 ++ .../ascribe_forms/form_register_piece.js | 10 ++++++++-- .../ascribe_forms/input_fineuploader.js | 3 +++ .../ascribe_uploader/react_s3_fine_uploader.js | 12 ++++++++---- .../pr_forms/pr_register_piece_form.js | 17 +++++++++++++---- js/constants/application_constants.js | 4 ++++ 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/js/components/ascribe_detail/further_details_fileuploader.js b/js/components/ascribe_detail/further_details_fileuploader.js index b044bbbc..acdbe9e0 100644 --- a/js/components/ascribe_detail/further_details_fileuploader.js +++ b/js/components/ascribe_detail/further_details_fileuploader.js @@ -20,6 +20,7 @@ let FurtherDetailsFileuploader = React.createClass({ otherData: React.PropTypes.arrayOf(React.PropTypes.object), setIsUploadReady: React.PropTypes.func, submitFile: React.PropTypes.func, + onValidationFailed: React.PropTypes.func, isReadyForFormSubmission: React.PropTypes.func, editable: React.PropTypes.bool, multiple: React.PropTypes.bool @@ -60,6 +61,7 @@ let FurtherDetailsFileuploader = React.createClass({ }} validation={AppConstants.fineUploader.validation.additionalData} submitFile={this.props.submitFile} + onValidationFailed={this.props.onValidationFailed} setIsUploadReady={this.props.setIsUploadReady} isReadyForFormSubmission={this.props.isReadyForFormSubmission} session={{ diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index 83d38b50..596f8a56 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -109,6 +109,11 @@ let RegisterPieceForm = React.createClass({ ); }, + handleThumbnailValidationFailed(thumbnailFile) { + // If the validation fails, set the thumbnail as submittable since its optional + this.refs.submitButton.setReadyStateForKey('thumbnailKeyReady', true); + }, + isThumbnailDialogExpanded() { const { enableSeparateThumbnail } = this.props; const { digitalWorkFile } = this.state; @@ -194,14 +199,15 @@ let RegisterPieceForm = React.createClass({ url: ApiUrls.blob_thumbnails }} handleChangedFile={this.handleChangedThumbnail} + onValidationFailed={this.handleThumbnailValidationFailed} isReadyForFormSubmission={formSubmissionValidation.fileOptional} keyRoutine={{ url: AppConstants.serverUrl + 's3/key/', fileClass: 'thumbnail' }} validation={{ - itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit, - sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit, + itemLimit: AppConstants.fineUploader.validation.workThumbnail.itemLimit, + sizeLimit: AppConstants.fineUploader.validation.workThumbnail.sizeLimit, allowedExtensions: ['png', 'jpg', 'jpeg', 'gif'] }} setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')} diff --git a/js/components/ascribe_forms/input_fineuploader.js b/js/components/ascribe_forms/input_fineuploader.js index db5bae05..fa9c72b6 100644 --- a/js/components/ascribe_forms/input_fineuploader.js +++ b/js/components/ascribe_forms/input_fineuploader.js @@ -52,6 +52,7 @@ const InputFineUploader = React.createClass({ plural: string }), handleChangedFile: func, + onValidationFailed: func, // Provided by `Property` onChange: React.PropTypes.func @@ -107,6 +108,7 @@ const InputFineUploader = React.createClass({ isFineUploaderActive, isReadyForFormSubmission, keyRoutine, + onValidationFailed, setIsUploadReady, uploadMethod, validation, @@ -127,6 +129,7 @@ const InputFineUploader = React.createClass({ createBlobRoutine={createBlobRoutine} validation={validation} submitFile={this.submitFile} + onValidationFailed={onValidationFailed} setIsUploadReady={setIsUploadReady} isReadyForFormSubmission={isReadyForFormSubmission} areAssetsDownloadable={areAssetsDownloadable} diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index eb211504..877146a5 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -50,6 +50,7 @@ const ReactS3FineUploader = React.createClass({ }), handleChangedFile: func, // is for when a file is dropped or selected submitFile: func, // is for when a file has been successfully uploaded, TODO: rename to handleSubmitFile + onValidationFailed: func, autoUpload: bool, debug: bool, objectProperties: shape({ @@ -523,13 +524,16 @@ const ReactS3FineUploader = React.createClass({ }, isFileValid(file) { - if(file.size > this.props.validation.sizeLimit) { + if (file.size > this.props.validation.sizeLimit) { + const fileSizeInMegaBytes = this.props.validation.sizeLimit / 1000000; - let fileSizeInMegaBytes = this.props.validation.sizeLimit / 1000000; - - let notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000); + const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000); GlobalNotificationActions.appendGlobalNotification(notification); + if (typeof this.props.onValidationFailed === 'function') { + this.props.onValidationFailed(file); + } + return false; } else { return true; diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js index a8d946b5..6b67467e 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js @@ -139,7 +139,7 @@ const PRRegisterPieceForm = React.createClass({ }, /** - * This method is overloaded so that we can track the ready-state + * These two methods are overloaded so that we can track the ready-state * of each uploader in the component * @param {string} uploaderKey Name of the uploader's key to track */ @@ -151,6 +151,14 @@ const PRRegisterPieceForm = React.createClass({ }; }, + handleOptionalFileValidationFailed(uploaderKey) { + return () => { + this.setState({ + [uploaderKey]: true + }); + }; + }, + getSubmitButton() { const { digitalWorkKeyReady, thumbnailKeyReady, @@ -303,7 +311,7 @@ const PRRegisterPieceForm = React.createClass({ + label={getLangText('Featured Cover photo (max 2MB)')}> Date: Mon, 21 Dec 2015 11:44:13 +0100 Subject: [PATCH 035/197] Small cleanups and fixes for spacing, unused props, etc --- js/actions/contract_list_actions.js | 16 ++--- .../ascribe_settings/contract_settings.js | 36 ++++------ .../contract_settings_update_button.js | 68 +++++++++---------- js/fetchers/ownership_fetcher.js | 14 ++-- 4 files changed, 61 insertions(+), 73 deletions(-) diff --git a/js/actions/contract_list_actions.js b/js/actions/contract_list_actions.js index 1c5c0913..d368ac73 100644 --- a/js/actions/contract_list_actions.js +++ b/js/actions/contract_list_actions.js @@ -28,12 +28,10 @@ class ContractListActions { } - changeContract(contract){ + changeContract(contract) { return Q.Promise((resolve, reject) => { OwnershipFetcher.changeContract(contract) - .then((res) => { - resolve(res); - }) + .then(resolve) .catch((err)=> { console.logGlobal(err); reject(err); @@ -41,13 +39,11 @@ class ContractListActions { }); } - removeContract(contractId){ - return Q.Promise( (resolve, reject) => { + removeContract(contractId) { + return Q.Promise((resolve, reject) => { OwnershipFetcher.deleteContract(contractId) - .then((res) => { - resolve(res); - }) - .catch( (err) => { + .then(resolve) + .catch((err) => { console.logGlobal(err); reject(err); }); diff --git a/js/components/ascribe_settings/contract_settings.js b/js/components/ascribe_settings/contract_settings.js index 71d97542..be723295 100644 --- a/js/components/ascribe_settings/contract_settings.js +++ b/js/components/ascribe_settings/contract_settings.js @@ -28,11 +28,7 @@ import { mergeOptions, truncateTextAtCharIndex } from '../../utils/general_utils let ContractSettings = React.createClass({ - propTypes: { - location: React.PropTypes.object - }, - - getInitialState(){ + getInitialState() { return mergeOptions( ContractListStore.getState(), UserStore.getState() @@ -64,40 +60,39 @@ let ContractSettings = React.createClass({ ContractListActions.removeContract(contract.id) .then((response) => { ContractListActions.fetchContractList(true); - let notification = new GlobalNotificationModel(response.notification, 'success', 4000); + const notification = new GlobalNotificationModel(response.notification, 'success', 4000); GlobalNotificationActions.appendGlobalNotification(notification); }) .catch((err) => { - let notification = new GlobalNotificationModel(err, 'danger', 10000); + const notification = new GlobalNotificationModel(err, 'danger', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }); }; }, - getPublicContracts(){ + getPublicContracts() { return this.state.contractList.filter((contract) => contract.is_public); }, - getPrivateContracts(){ + getPrivateContracts() { return this.state.contractList.filter((contract) => !contract.is_public); }, render() { - let publicContracts = this.getPublicContracts(); - let privateContracts = this.getPrivateContracts(); + const publicContracts = this.getPublicContracts(); + const privateContracts = this.getPrivateContracts(); let createPublicContractForm = null; setDocumentTitle(getLangText('Contracts settings')); - if(publicContracts.length === 0) { + if (publicContracts.length === 0) { createPublicContractForm = ( + }} /> ); } @@ -114,7 +109,7 @@ let ContractSettings = React.createClass({ {publicContracts.map((contract, i) => { return ( + contract={contract} /> + }} /> {privateContracts.map((contract, i) => { return ( + contract={contract} /> {/* So that ReactS3FineUploader is not complaining */}} - signature={{ - endpoint: AppConstants.serverUrl + 's3/signature/', - customHeaders: { - 'X-CSRFToken': getCookie(AppConstants.csrftoken) - } - }} - deleteFile={{ - enabled: true, - method: 'DELETE', - endpoint: AppConstants.serverUrl + 's3/delete', - customHeaders: { - 'X-CSRFToken': getCookie(AppConstants.csrftoken) - } - }} - fileClassToUpload={{ - singular: getLangText('UPDATE'), - plural: getLangText('UPDATE') - }} - isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} - submitFile={this.submitFile} /> + fileInputElement={UploadButton({ showLabel: false })} + keyRoutine={{ + url: AppConstants.serverUrl + 's3/key/', + fileClass: 'contract' + }} + createBlobRoutine={{ + url: ApiUrls.blob_contracts + }} + validation={{ + itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit, + sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit, + allowedExtensions: ['pdf'] + }} + setIsUploadReady={() =>{/* So that ReactS3FineUploader is not complaining */}} + signature={{ + endpoint: AppConstants.serverUrl + 's3/signature/', + customHeaders: { + 'X-CSRFToken': getCookie(AppConstants.csrftoken) + } + }} + deleteFile={{ + enabled: true, + method: 'DELETE', + endpoint: AppConstants.serverUrl + 's3/delete', + customHeaders: { + 'X-CSRFToken': getCookie(AppConstants.csrftoken) + } + }} + fileClassToUpload={{ + singular: getLangText('UPDATE'), + plural: getLangText('UPDATE') + }} + isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} + submitFile={this.submitFile} /> ); } }); diff --git a/js/fetchers/ownership_fetcher.js b/js/fetchers/ownership_fetcher.js index b0d88927..f498ffa1 100644 --- a/js/fetchers/ownership_fetcher.js +++ b/js/fetchers/ownership_fetcher.js @@ -15,7 +15,7 @@ let OwnershipFetcher = { /** * Fetch the contracts of the logged-in user from the API. */ - fetchContractList(isActive, isPublic, issuer){ + fetchContractList(isActive, isPublic, issuer) { let queryParams = { isActive, isPublic, @@ -28,7 +28,7 @@ let OwnershipFetcher = { /** * Create a contractagreement between the logged-in user and the email from the API with contract. */ - createContractAgreement(signee, contractObj){ + createContractAgreement(signee, contractObj) { return requests.post(ApiUrls.ownership_contract_agreements, { body: {signee: signee, contract: contractObj.id }}); }, @@ -44,23 +44,23 @@ let OwnershipFetcher = { return requests.get(ApiUrls.ownership_contract_agreements, queryParams); }, - confirmContractAgreement(contractAgreement){ + confirmContractAgreement(contractAgreement) { return requests.put(ApiUrls.ownership_contract_agreements_confirm, {contract_agreement_id: contractAgreement.id}); }, - denyContractAgreement(contractAgreement){ + denyContractAgreement(contractAgreement) { return requests.put(ApiUrls.ownership_contract_agreements_deny, {contract_agreement_id: contractAgreement.id}); }, - fetchLoanPieceRequestList(){ + fetchLoanPieceRequestList() { return requests.get(ApiUrls.ownership_loans_pieces_request); }, - changeContract(contractObj){ + changeContract(contractObj) { return requests.put(ApiUrls.ownership_contract, { body: contractObj, contract_id: contractObj.id }); }, - deleteContract(contractObjId){ + deleteContract(contractObjId) { return requests.delete(ApiUrls.ownership_contract, {contract_id: contractObjId}); } }; From 3467e9f526cdcbb4cb895656da34f6be31354287 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 21 Dec 2015 11:45:50 +0100 Subject: [PATCH 036/197] Make behaviour of ContractSettingsUpdateButton more robust MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also fixes the “Contract could not be updated” notification that would occur after every attempt to update the contract. --- .../contract_settings_update_button.js | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/js/components/ascribe_settings/contract_settings_update_button.js b/js/components/ascribe_settings/contract_settings_update_button.js index c09ba94f..f3e92c69 100644 --- a/js/components/ascribe_settings/contract_settings_update_button.js +++ b/js/components/ascribe_settings/contract_settings_update_button.js @@ -24,37 +24,41 @@ let ContractSettingsUpdateButton = React.createClass({ }, submitFile(file) { - let contract = this.props.contract; - // override the blob with the key's value - contract.blob = file.key; + const contract = Object.assign(this.props.contract, { blob: file.key }); // send it to the server ContractListActions .changeContract(contract) .then((res) => { - // Display feedback to the user - let notification = new GlobalNotificationModel(getLangText('Contract %s successfully updated', res.name), 'success', 5000); + const notification = new GlobalNotificationModel(getLangText('Contract %s successfully updated', contract.name), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); // and refresh the contract list to get the updated contracs - return ContractListActions.fetchContractList(true); - }) - .then(() => { - // Also, reset the fineuploader component so that the user can again 'update' his contract - this.refs.fineuploader.reset(); - }) - .catch((err) => { - console.logGlobal(err); - let notification = new GlobalNotificationModel(getLangText('Contract could not be updated'), 'success', 5000); + return ContractListActions + .fetchContractList(true) + // Also, reset the fineuploader component if fetch is successful so that the user can again 'update' his contract + .then(this.refs.fineuploader.reset) + .catch((err) => { + const notification = new GlobalNotificationModel(getLangText('Latest contract failed to load'), 'danger', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + + return Promise.reject(err); + }); + }, (err) => { + const notification = new GlobalNotificationModel(getLangText('Contract could not be updated'), 'danger', 5000); GlobalNotificationActions.appendGlobalNotification(notification); - }); + + return Promise.reject(err); + }) + .catch(console.logGlobal); }, render() { return ( Date: Mon, 21 Dec 2015 11:46:46 +0100 Subject: [PATCH 037/197] Add prop to hide label of UploadButton --- .../ascribe_upload_button/upload_button.js | 47 ++++++++++--------- sass/ascribe_uploader.scss | 23 +++++---- 2 files changed, 38 insertions(+), 32 deletions(-) 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 6612f968..ffd26a13 100644 --- a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js +++ b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js @@ -1,6 +1,7 @@ 'use strict'; import React from 'react'; +import classNames from 'classnames'; import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils'; import { getLangText } from '../../../utils/lang_utils'; @@ -9,7 +10,7 @@ import { truncateTextAtCharIndex } from '../../../utils/general_utils'; const { func, array, bool, shape, string } = React.PropTypes; -export default function UploadButton({ className = 'btn btn-default btn-sm' } = {}) { +export default function UploadButton({ className = 'btn btn-default btn-sm', showLabel = true } = {}) { return React.createClass({ displayName: 'UploadButton', @@ -119,28 +120,28 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = }, getUploadedFileLabel() { - const uploadedFile = this.getUploadedFile(); - const uploadingFiles = this.getUploadingFiles(); + if (showLabel) { + const uploadedFile = this.getUploadedFile(); + const uploadingFiles = this.getUploadingFiles(); - if(uploadingFiles.length) { - return ( - - {' ' + truncateTextAtCharIndex(uploadingFiles[0].name, 40) + ' '} - [{getLangText('cancel upload')}] - - ); - } else if(uploadedFile) { - return ( - - - {' ' + truncateTextAtCharIndex(uploadedFile.name, 40) + ' '} - [{getLangText('remove')}] - - ); - } else { - return ( - {getLangText('No file chosen')} - ); + if (uploadingFiles.length) { + return ( + + {' ' + truncateTextAtCharIndex(uploadingFiles[0].name, 40) + ' '} + [{getLangText('cancel upload')}] + + ); + } else if (uploadedFile) { + return ( + + + {' ' + truncateTextAtCharIndex(uploadedFile.name, 40) + ' '} + [{getLangText('remove')}] + + ); + } else { + return {getLangText('No file chosen')}; + } } }, @@ -158,7 +159,7 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = * Therefore the wrapping component needs to be an `anchor` tag instead of a `button` */ return ( -
    +
    {/* The button needs to be of `type="button"` as it would otherwise submit the form its in. diff --git a/sass/ascribe_uploader.scss b/sass/ascribe_uploader.scss index e6dd66cc..e6228916 100644 --- a/sass/ascribe_uploader.scss +++ b/sass/ascribe_uploader.scss @@ -29,7 +29,7 @@ .file-drag-and-drop-dialog { margin: 1.5em 0 1.5em 0; - + > p:first-child { font-size: 1.5em !important; margin-bottom: 0; @@ -189,15 +189,20 @@ height: 12px; } -.upload-button-wrapper { +.ascribe-upload-button { + display: inline-block; text-align: left; - .btn { - font-size: 1em; - margin-right: 1em; - } + &.ascribe-upload-button-has-label { + display: block; - span + .btn { - margin-left: 1em; + .btn { + font-size: 1em; + margin-right: 1em; + } + + span + .btn { + margin-left: 1em; + } } -} \ No newline at end of file +} From c335dd38826485dbe4a6906aadf7c5f670524316 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 4 Jan 2016 13:08:32 +0100 Subject: [PATCH 038/197] On subdomain get error, use the default subdomain settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also small cleanups and avoids adding `client—undefined` class to body for default subdomain --- js/app.js | 32 ++++++++++++++------------------ js/routes.js | 12 ++++-------- js/utils/constants_utils.js | 14 +++++++++----- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/js/app.js b/js/app.js index dc8204cf..42ad1473 100644 --- a/js/app.js +++ b/js/app.js @@ -17,7 +17,7 @@ import getRoutes from './routes'; import requests from './utils/requests'; import { updateApiUrls } from './constants/api_urls'; -import { getSubdomainSettings } from './utils/constants_utils'; +import { getDefaultSubdomainSettings, getSubdomainSettings } from './utils/constants_utils'; import { initLogging } from './utils/error_utils'; import { getSubdomain } from './utils/general_utils'; @@ -50,11 +50,10 @@ requests.defaults({ class AppGateway { start() { - let settings; - let subdomain = getSubdomain(); - try { - settings = getSubdomainSettings(subdomain); + const subdomain = getSubdomain(); + const settings = getSubdomainSettings(subdomain); + AppConstants.whitelabel = settings; updateApiUrls(settings.type, subdomain); this.load(settings); @@ -62,28 +61,25 @@ class AppGateway { // if there are no matching subdomains, we're routing // to the default frontend console.logGlobal(err); - this.load(); + this.load(getDefaultSubdomainSettings()); } } load(settings) { - let type = 'default'; - let subdomain = 'www'; + const { subdomain, type } = settings; let redirectRoute = (); - if (settings) { - type = settings.type; - subdomain = settings.subdomain; - } + if (subdomain) { + // Some whitelabels have landing pages so we should not automatically redirect from / to /collection. + // Only www and cc do not have a landing page. + if (subdomain !== 'cc') { + redirectRoute = null; + } - // www and cc do not have a landing page - if(subdomain && subdomain !== 'cc') { - redirectRoute = null; + // Adds a client specific class to the body for whitelabel styling + window.document.body.classList.add('client--' + subdomain); } - // Adds a client specific class to the body for whitelabel styling - window.document.body.classList.add('client--' + subdomain); - // Send the applicationWillBoot event to the third-party stores EventActions.applicationWillBoot(settings); diff --git a/js/routes.js b/js/routes.js index 49a284af..025a0fb6 100644 --- a/js/routes.js +++ b/js/routes.js @@ -28,7 +28,7 @@ import RegisterPiece from './components/register_piece'; import { ProxyHandler, AuthRedirect } from './components/ascribe_routes/proxy_handler'; -let COMMON_ROUTES = ( +const COMMON_ROUTES = ( subdomain === sdSettings.subdomain); + const settings = AppConstants.subdomains.filter((sdSettings) => subdomain === sdSettings.subdomain); - if(settings.length === 1) { + if (settings.length === 1) { return settings[0]; - } else if(settings.length === 0) { + } else if (settings.length === 0) { console.warn('There are no subdomain settings for the subdomain: ' + subdomain); - return appConstants.defaultDomain; + return AppConstants.defaultDomain; } else { throw new Error('Matched multiple subdomains. Adjust constants file.'); } From 4a3a368fc50b303db266d13f22ee92994cfd9b21 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 4 Jan 2016 13:09:03 +0100 Subject: [PATCH 039/197] Remove unused `logo` and `permissions` fields from subdomain constants --- js/constants/application_constants.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/js/constants/application_constants.js b/js/constants/application_constants.js index 824ed4b2..a230eb0c 100644 --- a/js/constants/application_constants.js +++ b/js/constants/application_constants.js @@ -24,52 +24,38 @@ const constants = { { 'subdomain': 'cc', 'name': 'Creative Commons France', - 'logo': 'https://s3-us-west-2.amazonaws.com/ascribe0/public/creativecommons/cc.logo.sm.png', - 'permissions': ['register', 'edit', 'share', 'del_from_collection'], 'type': 'wallet', 'ga': 'UA-60614729-4' }, { 'subdomain': 'sluice', 'name': 'Sluice Art Fair', - 'logo': 'http://sluice.info/images/logo.gif', - 'permissions': ['register', 'edit', 'share', 'del_from_collection'], 'type': 'prize', 'ga': 'UA-60614729-5' }, { 'subdomain': 'cyland', 'name': 'Cyland media art lab', - 'logo': 'https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/cyland/logo.gif', - 'permissions': ['register', 'edit', 'share', 'del_from_collection'], 'type': 'wallet' }, { 'subdomain': 'ikonotv', 'name': 'IkonoTV', - 'logo': 'https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/ikonotv/ikono-logo-black.png', - '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', - 'logo': 'http://notfoundlogo.de', - 'permissions': ['register', 'edit', 'share', 'del_from_collection'], 'type': 'prize' } ], From 3c72ee331d847416d3f30dc924b48682ac99958c Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 4 Jan 2016 13:14:50 +0100 Subject: [PATCH 040/197] Convert AppGateway to be single instance --- js/app.js | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/js/app.js b/js/app.js index 42ad1473..9118fbe3 100644 --- a/js/app.js +++ b/js/app.js @@ -33,22 +33,8 @@ import NotificationsHandler from './third_party/notifications'; import FacebookHandler from './third_party/facebook'; /* eslint-enable */ -initLogging(); -let headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json' -}; - -requests.defaults({ - urlMap: ApiUrls, - http: { - headers: headers, - credentials: 'include' - } -}); - -class AppGateway { +const AppGateway = { start() { try { const subdomain = getSubdomain(); @@ -63,7 +49,7 @@ class AppGateway { console.logGlobal(err); this.load(getDefaultSubdomainSettings()); } - } + }, load(settings) { const { subdomain, type } = settings; @@ -97,8 +83,21 @@ class AppGateway { // Send the applicationDidBoot event to the third-party stores EventActions.applicationDidBoot(settings); } -} +}; -let ag = new AppGateway(); -ag.start(); +// Initialize pre-start components +initLogging(); +requests.defaults({ + urlMap: ApiUrls, + http: { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + credentials: 'include' + } +}); + +// And bootstrap app +AppGateway.start(); From 41c9a10c84c765f0c55bfe3e97cb6c82c479217f Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 5 Jan 2016 17:28:45 +0100 Subject: [PATCH 041/197] Use a default shouldRedirect that just always returns true --- js/components/piece_list.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/js/components/piece_list.js b/js/components/piece_list.js index de979e65..dfe30a9c 100644 --- a/js/components/piece_list.js +++ b/js/components/piece_list.js @@ -53,7 +53,6 @@ let PieceList = React.createClass({ accordionListItemType: AccordionListItemWallet, bulkModalButtonListType: AclButtonList, canLoadPieceList: true, - orderParams: ['artist_name', 'title'], filterParams: [{ label: getLangText('Show works I can'), items: [ @@ -61,7 +60,9 @@ let PieceList = React.createClass({ 'acl_consign', 'acl_create_editions' ] - }] + }], + orderParams: ['artist_name', 'title'], + shouldRedirect: () => true }; }, From cf6dbb26f3e3f7a9179c9ed5dec9287d189e0c9c Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 6 Jan 2016 15:13:09 +0100 Subject: [PATCH 042/197] Fix loading of Facebook share button --- js/actions/facebook_actions.js | 14 ++++++ .../facebook_share_button.js | 50 +++++++++++++------ js/third_party/facebook.js | 17 +++++-- sass/ascribe_social_share.scss | 4 ++ 4 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 js/actions/facebook_actions.js diff --git a/js/actions/facebook_actions.js b/js/actions/facebook_actions.js new file mode 100644 index 00000000..2e784fba --- /dev/null +++ b/js/actions/facebook_actions.js @@ -0,0 +1,14 @@ +'use strict'; + +import { altThirdParty } from '../alt'; + + +class FacebookActions { + constructor() { + this.generateActions( + 'sdkReady' + ); + } +} + +export default altThirdParty.createActions(FacebookActions); diff --git a/js/components/ascribe_social_share/facebook_share_button.js b/js/components/ascribe_social_share/facebook_share_button.js index aa0b6691..682bc6f1 100644 --- a/js/components/ascribe_social_share/facebook_share_button.js +++ b/js/components/ascribe_social_share/facebook_share_button.js @@ -2,6 +2,8 @@ import React from 'react'; +import FacebookHandler from '../../third_party/facebook'; + import AppConstants from '../../constants/application_constants'; import { InjectInHeadUtils } from '../../utils/inject_utils'; @@ -17,24 +19,40 @@ let FacebookShareButton = React.createClass({ }; }, - componentDidMount() { - /** - * Ideally we would only use FB.XFBML.parse() on the component that we're - * mounting, but doing this when we first load the FB sdk causes unpredictable behaviour. - * The button sometimes doesn't get initialized, likely because FB hasn't properly - * been initialized yet. - * - * 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. - */ - - InjectInHeadUtils - .inject(AppConstants.facebook.sdkUrl) - .then(() => { FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement) }); + getInitialState() { + return FacebookHandler.getState(); }, - shouldComponentUpdate(nextProps) { - return this.props.type !== nextProps.type; + componentDidMount() { + FacebookHandler.listen(this.onChange); + + this.loadFacebook(); + }, + + shouldComponentUpdate(nextProps, nextState) { + // Don't update if the props haven't changed or the FB SDK loading status is still the same + return this.props.type !== nextProps.type || nextState.loaded !== this.state.loaded; + }, + + componentDidUpdate() { + // If the component changes, we need to reparse the share button's XFBML. + // To prevent cases where the Facebook SDK hasn't been loaded yet at this stage, + // let's make sure that it's injected before trying to reparse. + this.loadFacebook(); + }, + + onChange(state) { + this.setState(state); + }, + + loadFacebook() { + InjectInHeadUtils + .inject(AppConstants.facebook.sdkUrl) + .then(() => { + if (this.state.loaded) { + FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parent) + } + }); }, render() { diff --git a/js/third_party/facebook.js b/js/third_party/facebook.js index eab0b0e0..9e40e7ab 100644 --- a/js/third_party/facebook.js +++ b/js/third_party/facebook.js @@ -1,13 +1,18 @@ 'use strict'; import { altThirdParty } from '../alt'; + import EventActions from '../actions/event_actions'; +import FacebookActions from '../actions/facebook_actions'; import AppConstants from '../constants/application_constants' class FacebookHandler { constructor() { + this.loaded = false; + this.bindActions(EventActions); + this.bindActions(FacebookActions); } onApplicationWillBoot(settings) { @@ -16,15 +21,19 @@ class FacebookHandler { window.fbAsyncInit = () => { FB.init({ appId: AppConstants.facebook.appId, - // Force FB to parse everything on first load to make sure all the XFBML components are initialized. - // If we don't do this, we can run into issues with components on the first load who are not be - // initialized. - xfbml: true, + // Don't parse anything on the first load as we will parse all XFBML components as necessary. + xfbml: false, version: 'v2.5', cookie: false }); + + FacebookActions.sdkReady(); }; } + + onSdkReady() { + this.loaded = true; + } } export default altThirdParty.createStore(FacebookHandler, 'FacebookHandler'); diff --git a/sass/ascribe_social_share.scss b/sass/ascribe_social_share.scss index 0a577fb6..64e6e06b 100644 --- a/sass/ascribe_social_share.scss +++ b/sass/ascribe_social_share.scss @@ -7,4 +7,8 @@ width: 56px; box-sizing: content-box; /* We want to ignore padding in these calculations as we use the sdk buttons' height and width */ padding: 1px 0; + + &:not(:first-child) { + margin-left: 1px; + } } From 7f1e4ce0db150651b5c157b9b678abf09f473c1a Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 6 Jan 2016 15:23:48 +0100 Subject: [PATCH 043/197] Rename third party handler files to match their declarations --- js/app.js | 17 ++++++----------- js/third_party/{debug.js => debug_handler.js} | 1 - .../{facebook.js => facebook_handler.js} | 0 js/third_party/{ga.js => ga_handler.js} | 0 .../{intercom.js => intercom_handler.js} | 0 ...otifications.js => notifications_handler.js} | 0 js/third_party/{raven.js => raven_handler.js} | 0 7 files changed, 6 insertions(+), 12 deletions(-) rename js/third_party/{debug.js => debug_handler.js} (99%) rename js/third_party/{facebook.js => facebook_handler.js} (100%) rename js/third_party/{ga.js => ga_handler.js} (100%) rename js/third_party/{intercom.js => intercom_handler.js} (100%) rename js/third_party/{notifications.js => notifications_handler.js} (100%) rename js/third_party/{raven.js => raven_handler.js} (100%) diff --git a/js/app.js b/js/app.js index dc8204cf..c4645296 100644 --- a/js/app.js +++ b/js/app.js @@ -6,9 +6,7 @@ import React from 'react'; import { Router, Redirect } from 'react-router'; import history from './history'; -/* eslint-disable */ import fetch from 'isomorphic-fetch'; -/* eslint-enable */ import ApiUrls from './constants/api_urls'; @@ -23,15 +21,13 @@ import { getSubdomain } from './utils/general_utils'; import EventActions from './actions/event_actions'; -/* eslint-disable */ // You can comment out the modules you don't need -// import DebugHandler from './third_party/debug'; -import GoogleAnalyticsHandler from './third_party/ga'; -import RavenHandler from './third_party/raven'; -import IntercomHandler from './third_party/intercom'; -import NotificationsHandler from './third_party/notifications'; -import FacebookHandler from './third_party/facebook'; -/* eslint-enable */ +// import DebugHandler from './third_party/debug_handler'; +import FacebookHandler from './third_party/facebook_handler'; +import GoogleAnalyticsHandler from './third_party/ga_handler'; +import IntercomHandler from './third_party/intercom_handler'; +import NotificationsHandler from './third_party/notifications_handler'; +import RavenHandler from './third_party/raven_handler'; initLogging(); @@ -105,4 +101,3 @@ class AppGateway { let ag = new AppGateway(); ag.start(); - diff --git a/js/third_party/debug.js b/js/third_party/debug_handler.js similarity index 99% rename from js/third_party/debug.js rename to js/third_party/debug_handler.js index 23fe4d04..cd11bf72 100644 --- a/js/third_party/debug.js +++ b/js/third_party/debug_handler.js @@ -4,7 +4,6 @@ import { altThirdParty } from '../alt'; import EventActions from '../actions/event_actions'; - class DebugHandler { constructor() { let symbols = []; diff --git a/js/third_party/facebook.js b/js/third_party/facebook_handler.js similarity index 100% rename from js/third_party/facebook.js rename to js/third_party/facebook_handler.js diff --git a/js/third_party/ga.js b/js/third_party/ga_handler.js similarity index 100% rename from js/third_party/ga.js rename to js/third_party/ga_handler.js diff --git a/js/third_party/intercom.js b/js/third_party/intercom_handler.js similarity index 100% rename from js/third_party/intercom.js rename to js/third_party/intercom_handler.js diff --git a/js/third_party/notifications.js b/js/third_party/notifications_handler.js similarity index 100% rename from js/third_party/notifications.js rename to js/third_party/notifications_handler.js diff --git a/js/third_party/raven.js b/js/third_party/raven_handler.js similarity index 100% rename from js/third_party/raven.js rename to js/third_party/raven_handler.js From 74a587906b2ed9173c30dfe607468b061a22cab4 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 7 Jan 2016 18:29:55 +0100 Subject: [PATCH 044/197] Add anchorize utility to textareas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And Tim thought this was going to be a piece of cake… next time he can play with the url regex. --- .../ascribe_detail/further_details.js | 34 ++++---- .../ascribe_forms/form_piece_extradata.js | 29 ++++--- .../ascribe_forms/input_textarea_toggable.js | 41 +++++----- js/utils/dom_utils.js | 82 +++++++++++++++++-- js/utils/regex_utils.js | 53 +++++++++++- 5 files changed, 181 insertions(+), 58 deletions(-) diff --git a/js/components/ascribe_detail/further_details.js b/js/components/ascribe_detail/further_details.js index c178fb93..387398b5 100644 --- a/js/components/ascribe_detail/further_details.js +++ b/js/components/ascribe_detail/further_details.js @@ -32,13 +32,13 @@ let FurtherDetails = React.createClass({ }; }, - showNotification(){ + showNotification() { this.props.handleSuccess(); - let notification = new GlobalNotificationModel('Details updated', 'success'); + const notification = new GlobalNotificationModel('Details updated', 'success'); GlobalNotificationActions.appendGlobalNotification(notification); }, - submitFile(file){ + submitFile(file) { this.setState({ otherDataKey: file.key }); @@ -51,6 +51,8 @@ let FurtherDetails = React.createClass({ }, render() { + const { editable, extraData, otherData, pieceId } = this.props; + return ( @@ -58,33 +60,33 @@ let FurtherDetails = React.createClass({ name='artist_contact_info' title='Artist Contact Info' handleSuccess={this.showNotification} - editable={this.props.editable} - pieceId={this.props.pieceId} - extraData={this.props.extraData} - /> + editable={editable} + pieceId={pieceId} + extraData={extraData} + convertLinks /> + editable={editable} + pieceId={pieceId} + extraData={extraData} /> + editable={editable} + pieceId={pieceId} + extraData={extraData} />
    diff --git a/js/components/ascribe_forms/form_piece_extradata.js b/js/components/ascribe_forms/form_piece_extradata.js index f6ee4177..3e45f509 100644 --- a/js/components/ascribe_forms/form_piece_extradata.js +++ b/js/components/ascribe_forms/form_piece_extradata.js @@ -18,38 +18,43 @@ let PieceExtraDataForm = React.createClass({ handleSuccess: React.PropTypes.func, name: React.PropTypes.string, title: React.PropTypes.string, + convertLinks: React.PropTypes.bool, editable: React.PropTypes.bool }, getFormData() { - let extradata = {}; - extradata[this.props.name] = this.refs.form.refs[this.props.name].state.value; return { - extradata: extradata, + extradata: { + [this.props.name]: this.refs.form.refs[this.props.name].state.value + }, piece_id: this.props.pieceId }; }, - + render() { - let defaultValue = this.props.extraData[this.props.name] || ''; - if (defaultValue.length === 0 && !this.props.editable){ + const { convertLinks, editable, extraData, handleSuccess, name, pieceId, title } = this.props; + const defaultValue = this.props.extraData[this.props.name] || ''; + + if (defaultValue.length === 0 && !editable){ return null; } - let url = requests.prepareUrl(ApiUrls.piece_extradata, {piece_id: this.props.pieceId}); + + const url = requests.prepareUrl(ApiUrls.piece_extradata, {piece_id: pieceId}); return (
    + disabled={!editable}> + name={name} + label={title}>
    diff --git a/js/components/ascribe_forms/input_textarea_toggable.js b/js/components/ascribe_forms/input_textarea_toggable.js index 0be8b87a..05a1f011 100644 --- a/js/components/ascribe_forms/input_textarea_toggable.js +++ b/js/components/ascribe_forms/input_textarea_toggable.js @@ -4,17 +4,20 @@ import React from 'react'; import TextareaAutosize from 'react-textarea-autosize'; +import { anchorize } from '../../utils/dom_utils'; + let InputTextAreaToggable = React.createClass({ propTypes: { autoFocus: React.PropTypes.bool, - disabled: React.PropTypes.bool, - rows: React.PropTypes.number.isRequired, - required: React.PropTypes.bool, + convertLinks: React.PropTypes.bool, defaultValue: React.PropTypes.string, - placeholder: React.PropTypes.string, + disabled: React.PropTypes.bool, onBlur: React.PropTypes.func, - onChange: React.PropTypes.func + onChange: React.PropTypes.func, + placeholder: React.PropTypes.string, + required: React.PropTypes.bool, + rows: React.PropTypes.number.isRequired }, getInitialState() { @@ -36,7 +39,7 @@ let InputTextAreaToggable = React.createClass({ componentDidUpdate() { // If the initial value of state.value is null, we want to set props.defaultValue // as a value. In all other cases TextareaAutosize.onChange is updating.handleChange already - if(this.state.value === null && this.props.defaultValue) { + if (this.state.value === null && this.props.defaultValue) { this.setState({ value: this.props.defaultValue }); @@ -49,28 +52,26 @@ let InputTextAreaToggable = React.createClass({ }, render() { - let className = 'form-control ascribe-textarea'; - let textarea = null; + const { convertLinks, disabled, onBlur, placeholder, required, rows } = this.props; + const { value } = this.state; - if(!this.props.disabled) { - className = className + ' ascribe-textarea-editable'; - textarea = ( + if (!disabled) { + return ( + onBlur={onBlur} + placeholder={placeholder} /> ); } else { - textarea =
    {this.state.value}
    ; + // Can only convert links when not editable, as textarea does not support anchors + return
    {convertLinks ? anchorize(value) : value}
    ; } - - return textarea; } }); diff --git a/js/utils/dom_utils.js b/js/utils/dom_utils.js index d009f90f..f0cd852c 100644 --- a/js/utils/dom_utils.js +++ b/js/utils/dom_utils.js @@ -1,5 +1,9 @@ 'use strict'; +import React from 'react'; + +import { getLinkRegex, isEmail } from './regex_utils'; + /** * Set the title in the browser window. */ @@ -13,21 +17,24 @@ export function setDocumentTitle(title) { * @param {object} elementAttributes: hash table containing the attributes of the relevant element */ function constructHeadElement(elementType, elementId, elementAttributes) { - let head = (document.head || document.getElementsByTagName('head')[0]); - let element = document.createElement(elementType); - let oldElement = document.getElementById(elementId); + const head = (document.head || document.getElementsByTagName('head')[0]); + const element = document.createElement(elementType); + const oldElement = document.getElementById(elementId); + element.setAttribute('id', elementId); - for (let k in elementAttributes){ + + for (let k in elementAttributes) { try { element.setAttribute(k, elementAttributes[k]); - } - catch(e){ + } catch(e) { console.warn(e.message); } } + if (oldElement) { head.removeChild(oldElement); } + head.appendChild(element); } @@ -37,9 +44,68 @@ function constructHeadElement(elementType, elementId, elementAttributes) { */ export function constructHead(headObject){ for (let k in headObject){ - let favicons = headObject[k]; + const favicons = headObject[k]; for (let f in favicons){ constructHeadElement(k, f, favicons[f]); } } -} \ No newline at end of file +} + +/** + * Replaces the links and emails in a given string with anchor elements. + * + * @param {string} string String to anchorize + * @param {(object)} options Options object for anchorizing + * @param {(boolean)} emails Whether or not to replace emails (default: true) + * @param {(boolean)} links Whether or not to replace links (default: true) + * @param {(string)} target Anchor target attribute (default: '_blank') + * @return {string|React.element[]} Anchorized string as usable react element, either as an array of + * elements or just a string + */ +export function anchorize(string, { emails: replaceEmail = true, links: replaceLink = true, target = '_blank' } = {}) { + if (!replaceEmail && !replaceLink) { + return string; + } + + const linkRegex = getLinkRegex(); + const strWithAnchorElems = []; + let lastMatchIndex = 0; + let regexMatch; + + while (regexMatch = linkRegex.exec(string)) { + const [ matchedStr, schemeName ] = regexMatch; + const matchedStrIsEmail = isEmail(matchedStr); + + let anchorizedMatch; + if (matchedStrIsEmail && replaceEmail) { + anchorizedMatch = ({matchedStr}); + } else if (!matchedStrIsEmail && replaceLink) { + anchorizedMatch = ({matchedStr}); + } + + // We only need to add an element to the array and update the lastMatchIndex if we actually create an anchor + if (anchorizedMatch) { + // First add the string between the end of the last anchor text and the start of the current match + const currentMatchStartIndex = linkRegex.lastIndex - matchedStr.length; + + if (lastMatchIndex !== currentMatchStartIndex) { + strWithAnchorElems.push(string.substring(lastMatchIndex, currentMatchStartIndex)); + } + + strWithAnchorElems.push(anchorizedMatch); + + lastMatchIndex = linkRegex.lastIndex; + } + } + + if (strWithAnchorElems.length) { + // Add the string between the end of the last anchor and the end of the string + if (lastMatchIndex !== string.length) { + strWithAnchorElems.push(string.substring(lastMatchIndex)); + } + + return strWithAnchorElems; + } else { + return string; + } +} diff --git a/js/utils/regex_utils.js b/js/utils/regex_utils.js index af948b2b..49412d07 100644 --- a/js/utils/regex_utils.js +++ b/js/utils/regex_utils.js @@ -1,7 +1,56 @@ 'use strict' -export function isEmail(string) { +// TODO: Create Unittests that test all functions + +// We return new regexes everytime as opposed to using a constant regex because +// regexes with the global flag maintain internal iterators that can cause problems: +// http://bjorn.tipling.com/state-and-regular-expressions-in-javascript +// http://www.2ality.com/2013/08/regexp-g.html +export function getEmailRegex() { // 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(/.*@.*\..*/); + return /.*@.*\..*/g; +} + +export function getLinkRegex() { + // You really can't win them all with urls too (unless a 500 character regex that adheres + // to a strict interpretation of urls sounds like fun!) + // https://mathiasbynens.be/demo/url-regex + // + // This was initially based off of the one angular uses for its linky + // (https://github.com/angular/angular.js/blob/master/src/ngSanitize/filter/linky.js)... + // but then it evovled into its own thing to support capturing groups for filtering the + // hostname and other technically valid urls. + // + // Capturing groups: + // 1. URL scheme + // 2. URL without scheme + // 3. Host name + // 4. Path + // 5. Fragment + // + // Passes most tests of https://mathiasbynens.be/demo/url-regex, but provides a few false + // positives for some tests that are too strict (like `foo.com`). There are a few other + // false positives, such as `http://www.foo.bar./` but c'mon, that one's not my fault. + // I'd argue we would want to match that as a link anyway. + // + // Note: This also catches emails, as otherwise it would match the `ascribe.io` in `hi@ascribe.io`, + // producing (what I think is) more surprising behaviour than the alternative. + return /\b(https?:\/\/)?((?:www\.)?((?:[^\s.,;()\/]+\.)+[^\s$_!*()$&.,;=?+\/\#]+)((?:\/|\?|\/\?)[^\s#^`{}<>?"\[\]\/\|]+)*\/?(#[^\s#%^`{}<>?"\[\]\/\|]*)?)/g; +} + +/** + * @param {string} string String to check + * @return {boolean} Whether string is an email or not + */ +export function isEmail(string) { + return !!string && string.match(getEmailRegex()); +} + +/** + * @param {string} string String to check + * @return {boolean} Whether string is an link or not + */ +export function isLink(string) { + return !!string && string.match(getLinkRegex()); } From d1a056b7d937aa9aec70a91ea2311462418d7cca Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 7 Jan 2016 18:30:27 +0100 Subject: [PATCH 045/197] Use left text align for ascribe-pre --- sass/ascribe_textarea.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sass/ascribe_textarea.scss b/sass/ascribe_textarea.scss index bcd0502a..e241e442 100644 --- a/sass/ascribe_textarea.scss +++ b/sass/ascribe_textarea.scss @@ -14,7 +14,7 @@ font-family: inherit; margin: 0; padding: 0; - text-align: justify; + text-align: left; white-space: -moz-pre-wrap; white-space: -o-pre-wrap; white-space: -pre-wrap; From fce578e854c1bd9c02a9f5600451b8465f7211f0 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 8 Jan 2016 11:44:25 +0100 Subject: [PATCH 046/197] Decorate rackt/history with history of previous locations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Could be turned into a createHistory enhancer function (https://github.com/rackt/history/blob/master/docs/Glossary.md#createhis toryenhancer), but we’ll see what the guys at rackt/history say about it… --- js/components/ascribe_app.js | 28 +++++++++++++++++++++++---- js/constants/application_constants.js | 2 ++ js/history.js | 8 ++++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index cda5637f..774395ae 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -1,11 +1,14 @@ 'use strict'; import React from 'react'; +import { History } from 'react-router'; -import Header from '../components/header'; -import Footer from '../components/footer'; +import Header from './header'; +import Footer from './footer'; import GlobalNotification from './global_notification'; +import AppConstants from '../constants/application_constants'; + let AscribeApp = React.createClass({ propTypes: { @@ -13,11 +16,28 @@ let AscribeApp = React.createClass({ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element ]), - routes: React.PropTypes.arrayOf(React.PropTypes.object) + routes: React.PropTypes.arrayOf(React.PropTypes.object), + location: React.PropTypes.object + }, + + mixins: [History], + + componentDidMount() { + this.history.locationQueue.push(this.props.location); + }, + + componentWillReceiveProps(nextProps) { + const { locationQueue } = this.history; + locationQueue.unshift(nextProps.location); + + // Limit the number of locations to keep in memory to avoid too much memory usage + if (locationQueue.length > AppConstants.locationThreshold) { + locationQueue.length = AppConstants.locationThreshold; + } }, render() { - let { children, routes } = this.props; + const { children, routes } = this.props; return (
    diff --git a/js/constants/application_constants.js b/js/constants/application_constants.js index 824ed4b2..bfb758f0 100644 --- a/js/constants/application_constants.js +++ b/js/constants/application_constants.js @@ -104,6 +104,8 @@ const constants = { 'IVARO', 'SIAE', 'JASPAR-SPDA', 'AKKA/LAA', 'LATGA-A', 'SOMAAP', 'ARTEGESTION', 'CARIER', 'BONO', 'APSAV', 'SPA', 'GESTOR', 'VISaRTA', 'RAO', 'LITA', 'DALRO', 'VeGaP', 'BUS', 'ProLitteris', 'AGADU', 'AUTORARTE', 'BUBEDRA', 'BBDA', 'BCDA', 'BURIDA', 'ADAVIS', 'BSDA'], + 'locationThreshold': 10, + 'searchThreshold': 500, 'supportedThumbnailFileFormats': [ diff --git a/js/history.js b/js/history.js index 903f2b73..0683fcb3 100644 --- a/js/history.js +++ b/js/history.js @@ -6,8 +6,12 @@ import AppConstants from './constants/application_constants'; // Remove the trailing slash if present -let baseUrl = AppConstants.baseUrl.replace(/\/$/, ''); +const baseUrl = AppConstants.baseUrl.replace(/\/$/, ''); -export default useBasename(createBrowserHistory)({ +const history = useBasename(createBrowserHistory)({ basename: baseUrl }); + +history.locationQueue = []; + +export default history; From 7e77cb58dcfe036d762690f087926e608dc7c6d6 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 8 Jan 2016 11:45:01 +0100 Subject: [PATCH 047/197] Log the previous location if a 404 is encountered --- js/components/error_not_found_page.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/js/components/error_not_found_page.js b/js/components/error_not_found_page.js index 0e111ce7..c42d2926 100644 --- a/js/components/error_not_found_page.js +++ b/js/components/error_not_found_page.js @@ -1,6 +1,7 @@ 'use strict'; import React from 'react'; +import { History } from 'react-router'; import { getLangText } from '../utils/lang_utils'; @@ -10,12 +11,25 @@ let ErrorNotFoundPage = React.createClass({ message: React.PropTypes.string }, + mixins: [History], + getDefaultProps() { return { message: getLangText("Oops, the page you are looking for doesn't exist.") }; }, + componentDidMount() { + // The previous page, if any, is the second item in the locationQueue + const { locationQueue: [ _, previousPage ] } = this.history; + + if (previousPage) { + console.logGlobal('Page not found', { + previousPath: previousPage.pathname + }); + } + }, + render() { return (
    @@ -32,4 +46,4 @@ let ErrorNotFoundPage = React.createClass({ } }); -export default ErrorNotFoundPage; \ No newline at end of file +export default ErrorNotFoundPage; From 2cedfd636e9aac237fae9b08d6c80992a0231e68 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 8 Jan 2016 14:49:39 +0100 Subject: [PATCH 048/197] Use Raven's `ignoreErrors` configuration option to ignore certain errors --- js/utils/error_utils.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/js/utils/error_utils.js b/js/utils/error_utils.js index eefe40d8..a29f59a0 100644 --- a/js/utils/error_utils.js +++ b/js/utils/error_utils.js @@ -13,22 +13,19 @@ 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, comment, ignoreSentry = AppConstants.errorMessagesToIgnore.indexOf(error.message) > -1) { +function logGlobal(error, comment, ignoreSentry) { console.error(error); - if(!ignoreSentry) { - if(comment) { - Raven.captureException(error, {extra: { comment }}); - } else { - Raven.captureException(error); - } + if (!ignoreSentry) { + Raven.captureException(error, comment ? { extra: { comment } } : undefined); } } export function initLogging() { // Initialize Raven for logging on Sentry Raven.config(AppConstants.raven.url, { - release: AppConstants.version + release: AppConstants.version, + ignoreErrors: AppConstants.errorMessagesToIgnore }).install(); window.onerror = Raven.process; From 19841ce6c4a61dcc686ab1a20a635f567a18d331 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 8 Jan 2016 14:59:45 +0100 Subject: [PATCH 049/197] Refactor EventActions and UserStore to more cleanly handle user authentication and log out events --- js/actions/event_actions.js | 5 ++-- js/components/header.js | 17 ++----------- js/components/logout_container.js | 7 ------ .../prize/portfolioreview/pr_app.js | 13 ---------- js/stores/user_store.js | 25 ++++++++++++++++--- js/third_party/intercom.js | 15 ++++++----- js/third_party/notifications.js | 11 +++++--- js/third_party/raven.js | 9 +++++-- 8 files changed, 49 insertions(+), 53 deletions(-) diff --git a/js/actions/event_actions.js b/js/actions/event_actions.js index 6d8ee12f..24889f4e 100644 --- a/js/actions/event_actions.js +++ b/js/actions/event_actions.js @@ -8,9 +8,8 @@ class EventActions { this.generateActions( 'applicationWillBoot', 'applicationDidBoot', - 'profileDidLoad', - //'userDidLogin', - //'userDidLogout', + 'userDidAuthenticate', + 'userDidLogout', 'routeDidChange' ); } diff --git a/js/components/header.js b/js/components/header.js index c16cba86..ed4a9a3d 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -59,19 +59,6 @@ let Header = React.createClass({ // 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); - - if (this.state.currentUser && this.state.currentUser.email) { - EventActions.profileDidLoad.defer(this.state.currentUser); - } - }, - - componentWillUpdate(nextProps, nextState) { - const { currentUser: { email: curEmail } = {} } = this.state; - const { currentUser: { email: nextEmail } = {} } = nextState; - - if (nextEmail && curEmail !== nextEmail) { - EventActions.profileDidLoad.defer(nextState.currentUser); - } }, componentWillUnmount() { @@ -81,7 +68,7 @@ let Header = React.createClass({ }, getLogo() { - let { whitelabel } = this.state; + const { whitelabel } = this.state; if (whitelabel.head) { constructHead(whitelabel.head); @@ -102,7 +89,7 @@ let Header = React.createClass({ ); }, - getPoweredBy(){ + getPoweredBy() { return ( { + EventActions.userDidLogout(); + + // Reset all stores back to their initial state + alt.recycle(); + altWhitelabel.recycle(); + altUser.recycle(); + altThirdParty.recycle(); + }); } onSuccessLogoutCurrentUser() { diff --git a/js/third_party/intercom.js b/js/third_party/intercom.js index 4ab2ff50..fc14ced2 100644 --- a/js/third_party/intercom.js +++ b/js/third_party/intercom.js @@ -12,25 +12,28 @@ class IntercomHandler { this.loaded = false; } - onProfileDidLoad(profile) { + onUserDidAuthenticate(user) { if (this.loaded) { return; } - /* eslint-disable */ - Intercom('boot', { - /* eslint-enable */ + window.Intercom('boot', { app_id: 'oboxh5w1', - email: profile.email, + email: user.email, subdomain: getSubdomain(), widget: { activator: '#IntercomDefaultWidget' - } + } }); console.log('Intercom loaded'); this.loaded = true; } + onUserDidLogout() { + // kill intercom (with fire) + window.Intercom('shutdown'); + this.loaded = false; + } } export default altThirdParty.createStore(IntercomHandler, 'IntercomHandler'); diff --git a/js/third_party/notifications.js b/js/third_party/notifications.js index 85379479..9e33cdaf 100644 --- a/js/third_party/notifications.js +++ b/js/third_party/notifications.js @@ -17,18 +17,19 @@ class NotificationsHandler { this.loaded = false; } - onProfileDidLoad() { + onUserDidAuthenticate() { if (this.loaded) { return; } - let subdomain = getSubdomain(); + const subdomain = getSubdomain(); if (subdomain === 'ikonotv') { NotificationActions.fetchContractAgreementListNotifications().then( (res) => { if (res.notifications && res.notifications.length > 0) { - this.loaded = true; console.log('Contractagreement notifications loaded'); + this.loaded = true; + history.pushState(null, '/contract_notifications'); } } @@ -36,6 +37,10 @@ class NotificationsHandler { } this.loaded = true; } + + onUserDidLogout() { + this.loaded = false; + } } export default altThirdParty.createStore(NotificationsHandler, 'NotificationsHandler'); diff --git a/js/third_party/raven.js b/js/third_party/raven.js index 3d6ff315..f294bc95 100644 --- a/js/third_party/raven.js +++ b/js/third_party/raven.js @@ -12,17 +12,22 @@ class RavenHandler { this.loaded = false; } - onProfileDidLoad(profile) { + onUserDidAuthenticate(user) { if (this.loaded) { return; } Raven.setUserContext({ - email: profile.email + email: user.email }); console.log('Raven loaded'); this.loaded = true; } + + onUserDidLogout() { + Raven.setUserContext(); + this.loaded = false; + } } export default altThirdParty.createStore(RavenHandler, 'RavenHandler'); From d622ddac06729a3bdbe90d98c0ba42722b92aae2 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 11 Jan 2016 12:54:15 +0100 Subject: [PATCH 050/197] Load current user and whitelabel settings in AscribeApp --- js/components/ascribe_app.js | 51 ++++++++++++++++-- .../ascribe_detail/edition_container.js | 28 +++++----- .../ascribe_detail/piece_container.js | 22 ++++---- js/components/ascribe_routes/proxy_handler.js | 40 ++++---------- .../ascribe_settings/contract_settings.js | 46 ++++++++-------- .../ascribe_settings/settings_container.js | 52 ++++++------------- js/components/coa_verify_container.js | 8 ++- js/components/error_not_found_page.js | 11 +++- js/components/login_container.js | 6 +++ js/components/logout_container.js | 9 ++++ js/components/password_reset_container.js | 50 ++++++++++-------- js/components/piece_list.js | 42 ++++++++------- js/components/register_piece.js | 25 ++++----- js/components/signup_container.js | 7 ++- 14 files changed, 216 insertions(+), 181 deletions(-) diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index cda5637f..115f93f9 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -2,10 +2,18 @@ import React from 'react'; -import Header from '../components/header'; -import Footer from '../components/footer'; +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 Header from './header'; +import Footer from './footer'; import GlobalNotification from './global_notification'; +import { mergeOptions } from '../utils/general_utils'; + let AscribeApp = React.createClass({ propTypes: { @@ -16,15 +24,48 @@ let AscribeApp = React.createClass({ routes: React.PropTypes.arrayOf(React.PropTypes.object) }, + getInitialState() { + return mergeOptions( + UserStore.getState(), + WhitelabelStore.getState() + ); + }, + + componentDidMount() { + UserStore.listen(this.onChange); + WhitelabelStore.listen(this.onChange); + + UserActions.fetchCurrentUser(); + WhitelabelActions.fetchWhitelabel(); + }, + + componentWillUnmount() { + UserStore.unlisten(this.onChange); + WhitelabelActions.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + render() { - let { children, routes } = this.props; + const { children, routes } = this.props; + const { currentUser, whitelabel } = this.state; + + // Add the current user and whitelabel settings to all child routes + const childrenWithProps = React.Children.map(children, (child) => { + return React.cloneElement(child, { + currentUser, + whitelabel + }); + }); return (
    - {/* Routes are injected here */}
    - {children} + {/* Routes are injected here */} + {childrenWithProps}