From c242cffdbda47a9e5ae4706c5b0b4604a13d668b Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 29 Oct 2015 17:04:31 +0100 Subject: [PATCH 01/61] Bring over changes for acl_button from Lumenus --- js/components/ascribe_buttons/acl_button.js | 81 +++++++++------------ js/utils/form_utils.js | 62 +++++++++++++--- 2 files changed, 85 insertions(+), 58 deletions(-) diff --git a/js/components/ascribe_buttons/acl_button.js b/js/components/ascribe_buttons/acl_button.js index 67f4a430..5197a744 100644 --- a/js/components/ascribe_buttons/acl_button.js +++ b/js/components/ascribe_buttons/acl_button.js @@ -16,7 +16,7 @@ import GlobalNotificationActions from '../../actions/global_notification_actions import ApiUrls from '../../constants/api_urls'; -import { getAclFormMessage } from '../../utils/form_utils'; +import { getAclFormMessage, getAclFormDataId } from '../../utils/form_utils'; import { getLangText } from '../../utils/lang_utils'; let AclButton = React.createClass({ @@ -30,32 +30,37 @@ let AclButton = React.createClass({ currentUser: React.PropTypes.object, buttonAcceptName: React.PropTypes.string, buttonAcceptClassName: React.PropTypes.string, + email: React.PropTypes.string, handleSuccess: React.PropTypes.func.isRequired, className: React.PropTypes.string }, - isPiece(){ + isPiece() { return this.props.pieceOrEditions.constructor !== Array; }, - actionProperties(){ + actionProperties() { + let message = getAclFormMessage({ + aclName: this.props.action, + entities: this.props.pieceOrEditions, + isPiece: this.isPiece(), + senderName: this.props.currentUser.username + }); - let message = getAclFormMessage(this.props.action, this.getTitlesString(), this.props.currentUser.username); - - if (this.props.action === 'acl_consign'){ + if (this.props.action === 'acl_consign') { return { title: getLangText('Consign artwork'), tooltip: getLangText('Have someone else sell the artwork'), form: ( - ), + ), handleSuccess: this.showNotification }; - } - if (this.props.action === 'acl_unconsign'){ + } else if (this.props.action === 'acl_unconsign') { return { title: getLangText('Unconsign artwork'), tooltip: getLangText('Have the owner manage his sales again'), @@ -64,10 +69,10 @@ let AclButton = React.createClass({ message={message} id={this.getFormDataId()} url={ApiUrls.ownership_unconsigns}/> - ), + ), handleSuccess: this.showNotification }; - }else if (this.props.action === 'acl_transfer') { + } else if (this.props.action === 'acl_transfer') { return { title: getLangText('Transfer artwork'), tooltip: getLangText('Transfer the ownership of the artwork'), @@ -79,32 +84,33 @@ let AclButton = React.createClass({ ), handleSuccess: this.showNotification }; - } - else if (this.props.action === 'acl_loan'){ + } else if (this.props.action === 'acl_loan') { return { title: getLangText('Loan artwork'), tooltip: getLangText('Loan your artwork for a limited period of time'), - form: ( + url={this.isPiece() ? ApiUrls.ownership_loans_pieces + : ApiUrls.ownership_loans_editions}/> ), handleSuccess: this.showNotification }; - } - else if (this.props.action === 'acl_loan_request'){ + } else if (this.props.action === 'acl_loan_request') { return { title: getLangText('Loan artwork'), tooltip: getLangText('Someone requested you to loan your artwork for a limited period of time'), - form: ( ), handleSuccess: this.showNotification }; - } - else if (this.props.action === 'acl_share'){ + } else if (this.props.action === 'acl_share') { return { title: getLangText('Share artwork'), tooltip: getLangText('Share the artwork'), @@ -112,8 +118,9 @@ let AclButton = React.createClass({ - ), + url={this.isPiece() ? ApiUrls.ownership_shares_pieces + : ApiUrls.ownership_shares_editions}/> + ), handleSuccess: this.showNotification }; } else { @@ -121,36 +128,16 @@ let AclButton = React.createClass({ } }, - showNotification(response){ + showNotification(response) { this.props.handleSuccess(); - if(response.notification) { + if (response.notification) { let notification = new GlobalNotificationModel(response.notification, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); } }, - // plz move to share form - getTitlesString(){ - if (this.isPiece()){ - return '\"' + this.props.pieceOrEditions.title + '\"'; - } - else { - return this.props.pieceOrEditions.map(function(edition) { - return '- \"' + edition.title + ', ' + getLangText('edition') + ' ' + edition.edition_number + '\"\n'; - }).join(''); - } - - }, - getFormDataId(){ - if (this.isPiece()) { - return {piece_id: this.props.pieceOrEditions.id}; - } - else { - return {bitcoin_id: this.props.pieceOrEditions.map(function(edition){ - return edition.bitcoin_id; - }).join()}; - } + return getAclFormDataId(this.isPiece(), this.props.pieceOrEditions); }, // Removes the acl_ prefix and converts to upper case @@ -162,7 +149,7 @@ let AclButton = React.createClass({ }, render() { - if (this.props.availableAcls){ + if (this.props.availableAcls) { let shouldDisplay = this.props.availableAcls[this.props.action]; let aclProps = this.actionProperties(); let buttonClassName = this.props.buttonAcceptClassName ? this.props.buttonAcceptClassName : ''; @@ -184,4 +171,4 @@ let AclButton = React.createClass({ } }); -export default AclButton; \ No newline at end of file +export default AclButton; diff --git a/js/utils/form_utils.js b/js/utils/form_utils.js index 7f9cfb07..d2d2cd29 100644 --- a/js/utils/form_utils.js +++ b/js/utils/form_utils.js @@ -2,14 +2,42 @@ 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) + * @param {(object|object[])} pieceOrEditions Piece or array of editions + * @return {(object|object[])} Data IDs of the pieceOrEditions for the form + */ +export function getAclFormDataId(isPiece, pieceOrEditions) { + if (isPiece) { + return {piece_id: pieceOrEditions.id}; + } else { + return {bitcoin_id: pieceOrEditions.map(function(edition){ + return edition.bitcoin_id; + }).join()}; + } +} + /** * Generates a message for submitting a form - * @param {string} aclName Enum name of a acl - * @param {string} entities Already computed name of entities - * @param {string} senderName Name of the sender - * @return {string} Completed message + * @param {object} options Options object for creating the message: + * @param {string} options.aclName Enum name of an acl + * @param {(object|object[])} options.entities Piece or array of Editions + * @param {boolean} options.isPiece Is the given entities parameter a piece? (False: array of editions) + * @param {string} [options.senderName] Name of the sender + * @return {string} Completed message */ -export function getAclFormMessage(aclName, entities, senderName) { +export function getAclFormMessage(options) { + if (!options || options.aclName === undefined || options.isPiece === undefined || + !(typeof options.entities === 'object' || options.entities.constructor === Array)) { + throw new Error('You must specify an acl class, entities in the correct format, and entity type'); + } + + let aclName = options.aclName; + let entityTitles = options.isPiece ? getTitlesStringOfPiece(options.entities) + : getTitlesStringOfEditions(options.entities); let message = ''; message += getLangText('Hi'); @@ -32,7 +60,7 @@ export function getAclFormMessage(aclName, entities, senderName) { } message += ':\n'; - message += entities; + message += entityTitles; if(aclName === 'acl_transfer' || aclName === 'acl_loan' || aclName === 'acl_consign') { message += getLangText('to you'); @@ -44,10 +72,22 @@ export function getAclFormMessage(aclName, entities, senderName) { throw new Error('Your specified aclName did not match a an acl class.'); } - message += '\n\n'; - message += getLangText('Truly yours,'); - message += '\n'; - message += senderName; + if (options.senderName) { + message += '\n\n'; + message += getLangText('Truly yours,'); + message += '\n'; + message += options.senderName; + } return message; -} \ No newline at end of file +} + +function getTitlesStringOfPiece(piece){ + return '\"' + piece.title + '\"'; +} + +function getTitlesStringOfEditions(editions) { + return editions.map(function(edition) { + return '- \"' + edition.title + ', ' + getLangText('edition') + ' ' + edition.edition_number + '\"\n'; + }).join(''); +} From 03e0bbd024115b545480f3452d6337ba7a5e3f06 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 30 Oct 2015 11:10:31 +0100 Subject: [PATCH 02/61] Separate form building concerns from AclButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AclButton’s form building is now delegated to AclFormFactory so other components can use the same forms with ease. Its show/hide behaviour is also now controlled with AclProxy. --- js/components/ascribe_buttons/acl_button.js | 174 ------------------ .../ascribe_buttons/acl_button_list.js | 31 ++-- .../ascribe_buttons/acls/acl_button.js | 89 +++++++++ .../ascribe_buttons/acls/consign_button.js | 21 +++ .../ascribe_buttons/acls/loan_button.js | 21 +++ .../acls/loan_request_button.js | 21 +++ .../ascribe_buttons/acls/share_button.js | 21 +++ .../ascribe_buttons/acls/transfer_button.js | 21 +++ .../ascribe_buttons/acls/unconsign_button.js | 21 +++ .../ascribe_forms/acl_form_factory.js | 134 ++++++++++++++ .../ascribe_forms/form_request_action.js | 15 +- 11 files changed, 372 insertions(+), 197 deletions(-) delete mode 100644 js/components/ascribe_buttons/acl_button.js create mode 100644 js/components/ascribe_buttons/acls/acl_button.js create mode 100644 js/components/ascribe_buttons/acls/consign_button.js create mode 100644 js/components/ascribe_buttons/acls/loan_button.js create mode 100644 js/components/ascribe_buttons/acls/loan_request_button.js create mode 100644 js/components/ascribe_buttons/acls/share_button.js create mode 100644 js/components/ascribe_buttons/acls/transfer_button.js create mode 100644 js/components/ascribe_buttons/acls/unconsign_button.js create mode 100644 js/components/ascribe_forms/acl_form_factory.js diff --git a/js/components/ascribe_buttons/acl_button.js b/js/components/ascribe_buttons/acl_button.js deleted file mode 100644 index 5197a744..00000000 --- a/js/components/ascribe_buttons/acl_button.js +++ /dev/null @@ -1,174 +0,0 @@ -'use strict'; - -import React from 'react'; - -import ConsignForm from '../ascribe_forms/form_consign'; -import UnConsignForm from '../ascribe_forms/form_unconsign'; -import TransferForm from '../ascribe_forms/form_transfer'; -import LoanForm from '../ascribe_forms/form_loan'; -import LoanRequestAnswerForm from '../ascribe_forms/form_loan_request_answer'; -import ShareForm from '../ascribe_forms/form_share_email'; -import ModalWrapper from '../ascribe_modal/modal_wrapper'; -import AppConstants from '../../constants/application_constants'; - -import GlobalNotificationModel from '../../models/global_notification_model'; -import GlobalNotificationActions from '../../actions/global_notification_actions'; - -import ApiUrls from '../../constants/api_urls'; - -import { getAclFormMessage, getAclFormDataId } from '../../utils/form_utils'; -import { getLangText } from '../../utils/lang_utils'; - -let AclButton = React.createClass({ - propTypes: { - action: React.PropTypes.oneOf(AppConstants.aclList).isRequired, - availableAcls: React.PropTypes.object.isRequired, - pieceOrEditions: React.PropTypes.oneOfType([ - React.PropTypes.object, - React.PropTypes.array - ]).isRequired, - currentUser: React.PropTypes.object, - buttonAcceptName: React.PropTypes.string, - buttonAcceptClassName: React.PropTypes.string, - email: React.PropTypes.string, - handleSuccess: React.PropTypes.func.isRequired, - className: React.PropTypes.string - }, - - isPiece() { - return this.props.pieceOrEditions.constructor !== Array; - }, - - actionProperties() { - let message = getAclFormMessage({ - aclName: this.props.action, - entities: this.props.pieceOrEditions, - isPiece: this.isPiece(), - senderName: this.props.currentUser.username - }); - - if (this.props.action === 'acl_consign') { - return { - title: getLangText('Consign artwork'), - tooltip: getLangText('Have someone else sell the artwork'), - form: ( - - ), - handleSuccess: this.showNotification - }; - } else if (this.props.action === 'acl_unconsign') { - return { - title: getLangText('Unconsign artwork'), - tooltip: getLangText('Have the owner manage his sales again'), - form: ( - - ), - handleSuccess: this.showNotification - }; - } else if (this.props.action === 'acl_transfer') { - return { - title: getLangText('Transfer artwork'), - tooltip: getLangText('Transfer the ownership of the artwork'), - form: ( - - ), - handleSuccess: this.showNotification - }; - } else if (this.props.action === 'acl_loan') { - return { - title: getLangText('Loan artwork'), - tooltip: getLangText('Loan your artwork for a limited period of time'), - form: ( - - ), - handleSuccess: this.showNotification - }; - } else if (this.props.action === 'acl_loan_request') { - return { - title: getLangText('Loan artwork'), - tooltip: getLangText('Someone requested you to loan your artwork for a limited period of time'), - form: ( - - ), - handleSuccess: this.showNotification - }; - } else if (this.props.action === 'acl_share') { - return { - title: getLangText('Share artwork'), - tooltip: getLangText('Share the artwork'), - form: ( - - ), - handleSuccess: this.showNotification - }; - } else { - throw new Error('Your specified action did not match a form.'); - } - }, - - showNotification(response) { - this.props.handleSuccess(); - if (response.notification) { - let notification = new GlobalNotificationModel(response.notification, 'success'); - GlobalNotificationActions.appendGlobalNotification(notification); - } - }, - - getFormDataId(){ - return getAclFormDataId(this.isPiece(), this.props.pieceOrEditions); - }, - - // Removes the acl_ prefix and converts to upper case - sanitizeAction() { - if (this.props.buttonAcceptName) { - return this.props.buttonAcceptName; - } - return this.props.action.split('acl_')[1].toUpperCase(); - }, - - render() { - if (this.props.availableAcls) { - let shouldDisplay = this.props.availableAcls[this.props.action]; - let aclProps = this.actionProperties(); - let buttonClassName = this.props.buttonAcceptClassName ? this.props.buttonAcceptClassName : ''; - return ( - - {this.sanitizeAction()} - - } - handleSuccess={aclProps.handleSuccess} - title={aclProps.title}> - {aclProps.form} - - ); - } - return null; - } -}); - -export default AclButton; diff --git a/js/components/ascribe_buttons/acl_button_list.js b/js/components/ascribe_buttons/acl_button_list.js index e87a6407..6aec6c77 100644 --- a/js/components/ascribe_buttons/acl_button_list.js +++ b/js/components/ascribe_buttons/acl_button_list.js @@ -5,21 +5,25 @@ import React from 'react/addons'; import UserActions from '../../actions/user_actions'; import UserStore from '../../stores/user_store'; -import AclButton from '../ascribe_buttons/acl_button'; +import ConsignButton from './acls/consign_button'; +import LoanButton from './acls/loan_button'; +import LoanRequestButton from './acls/loan_request_button'; +import ShareButton from './acls/share_button'; +import TransferButton from './acls/transfer_button'; +import UnconsignButton from './acls/unconsign_button'; import { mergeOptions } from '../../utils/general_utils'; - let AclButtonList = React.createClass({ propTypes: { className: React.PropTypes.string, editions: React.PropTypes.oneOfType([ React.PropTypes.object, React.PropTypes.array - ]), - availableAcls: React.PropTypes.object, + ]).isRequired, + availableAcls: React.PropTypes.object.isRequired, buttonsStyle: React.PropTypes.object, - handleSuccess: React.PropTypes.func, + handleSuccess: React.PropTypes.func.isRequired, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -86,33 +90,28 @@ let AclButtonList = React.createClass({ return (
- - - - - @@ -123,4 +122,4 @@ let AclButtonList = React.createClass({ } }); -export default AclButtonList; \ No newline at end of file +export default AclButtonList; diff --git a/js/components/ascribe_buttons/acls/acl_button.js b/js/components/ascribe_buttons/acls/acl_button.js new file mode 100644 index 00000000..f7149e79 --- /dev/null +++ b/js/components/ascribe_buttons/acls/acl_button.js @@ -0,0 +1,89 @@ +'use strict'; + +import React from 'react'; +import classNames from 'classnames'; + +import AclProxy from '../../acl_proxy'; + +import AclFormFactory from '../../ascribe_forms/acl_form_factory'; + +import ModalWrapper from '../../ascribe_modal/modal_wrapper'; + +import AppConstants from '../../../constants/application_constants'; + +import GlobalNotificationModel from '../../../models/global_notification_model'; +import GlobalNotificationActions from '../../../actions/global_notification_actions'; + +import ApiUrls from '../../../constants/api_urls'; + +import { getAclFormMessage, getAclFormDataId } from '../../../utils/form_utils'; +import { getLangText } from '../../../utils/lang_utils'; + +let AclButton = React.createClass({ + propTypes: { + action: React.PropTypes.oneOf(AppConstants.aclList).isRequired, + availableAcls: React.PropTypes.object.isRequired, + buttonAcceptName: React.PropTypes.string, + buttonAcceptClassName: React.PropTypes.string, + currentUser: React.PropTypes.object.isRequired, + email: React.PropTypes.string, + pieceOrEditions: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]).isRequired, + title: React.PropTypes.string, + handleSuccess: React.PropTypes.func.isRequired, + className: React.PropTypes.string + }, + + getDefaultProps() { + return { + buttonAcceptClassName: '' + }; + }, + + // Removes the acl_ prefix and converts to upper case + sanitizeAction() { + if (this.props.buttonAcceptName) { + return this.props.buttonAcceptName; + } + return this.props.action.split('acl_')[1].toUpperCase(); + }, + + render() { + const { + action, + availableAcls, + buttonAcceptClassName, + currentUser, + email, + pieceOrEditions, + handleSuccess, + title } = this.props; + + return ( + + + {this.sanitizeAction()} + + } + handleSuccess={handleSuccess} + title={title}> + + + + ); + } +}); + +export default AclButton; diff --git a/js/components/ascribe_buttons/acls/consign_button.js b/js/components/ascribe_buttons/acls/consign_button.js new file mode 100644 index 00000000..3eb88fc4 --- /dev/null +++ b/js/components/ascribe_buttons/acls/consign_button.js @@ -0,0 +1,21 @@ +'use strict'; + +import React from 'react'; + +import AclButton from './acl_button'; + +import { getLangText } from '../../../utils/lang_utils'; + +let ConsignButton = React.createClass({ + render() { + return ( + + ); + } +}); + +export default ConsignButton; diff --git a/js/components/ascribe_buttons/acls/loan_button.js b/js/components/ascribe_buttons/acls/loan_button.js new file mode 100644 index 00000000..aa0036c4 --- /dev/null +++ b/js/components/ascribe_buttons/acls/loan_button.js @@ -0,0 +1,21 @@ +'use strict'; + +import React from 'react'; + +import AclButton from './acl_button'; + +import { getLangText } from '../../../utils/lang_utils'; + +let LoanButton = React.createClass({ + render() { + return ( + + ); + } +}); + +export default LoanButton; diff --git a/js/components/ascribe_buttons/acls/loan_request_button.js b/js/components/ascribe_buttons/acls/loan_request_button.js new file mode 100644 index 00000000..85483ab1 --- /dev/null +++ b/js/components/ascribe_buttons/acls/loan_request_button.js @@ -0,0 +1,21 @@ +'use strict'; + +import React from 'react'; + +import AclButton from './acl_button'; + +import { getLangText } from '../../../utils/lang_utils'; + +let LoanButton = React.createClass({ + render() { + return ( + + ); + } +}); + +export default LoanButton; diff --git a/js/components/ascribe_buttons/acls/share_button.js b/js/components/ascribe_buttons/acls/share_button.js new file mode 100644 index 00000000..30792ef1 --- /dev/null +++ b/js/components/ascribe_buttons/acls/share_button.js @@ -0,0 +1,21 @@ +'use strict'; + +import React from 'react'; + +import AclButton from './acl_button'; + +import { getLangText } from '../../../utils/lang_utils'; + +let ShareButton = React.createClass({ + render() { + return ( + + ); + } +}); + +export default ShareButton; diff --git a/js/components/ascribe_buttons/acls/transfer_button.js b/js/components/ascribe_buttons/acls/transfer_button.js new file mode 100644 index 00000000..da8728f6 --- /dev/null +++ b/js/components/ascribe_buttons/acls/transfer_button.js @@ -0,0 +1,21 @@ +'use strict'; + +import React from 'react'; + +import AclButton from './acl_button'; + +import { getLangText } from '../../../utils/lang_utils'; + +let TransferButton = React.createClass({ + render() { + return ( + + ); + } +}); + +export default TransferButton; diff --git a/js/components/ascribe_buttons/acls/unconsign_button.js b/js/components/ascribe_buttons/acls/unconsign_button.js new file mode 100644 index 00000000..daaf488b --- /dev/null +++ b/js/components/ascribe_buttons/acls/unconsign_button.js @@ -0,0 +1,21 @@ +'use strict'; + +import React from 'react'; + +import AclButton from './acl_button'; + +import { getLangText } from '../../../utils/lang_utils'; + +let UnconsignButton = React.createClass({ + render() { + return ( + + ); + } +}); + +export default UnconsignButton; diff --git a/js/components/ascribe_forms/acl_form_factory.js b/js/components/ascribe_forms/acl_form_factory.js new file mode 100644 index 00000000..dc5ebd4e --- /dev/null +++ b/js/components/ascribe_forms/acl_form_factory.js @@ -0,0 +1,134 @@ +'use strict'; + +import React from 'react'; + +import ConsignForm from '../ascribe_forms/form_consign'; +import UnConsignForm from '../ascribe_forms/form_unconsign'; +import TransferForm from '../ascribe_forms/form_transfer'; +import LoanForm from '../ascribe_forms/form_loan'; +import LoanRequestAnswerForm from '../ascribe_forms/form_loan_request_answer'; +import ShareForm from '../ascribe_forms/form_share_email'; + +import AppConstants from '../../constants/application_constants'; +import ApiUrls from '../../constants/api_urls'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import { getAclFormMessage, getAclFormDataId } from '../../utils/form_utils'; + +let AclFormFactory = React.createClass({ + propTypes: { + action: React.PropTypes.oneOf(AppConstants.aclList).isRequired, + currentUser: React.PropTypes.object.isRequired, + email: React.PropTypes.string, + message: React.PropTypes.string, + pieceOrEditions: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]).isRequired, + handleSuccess: React.PropTypes.func, + showNotification: React.PropTypes.bool + }, + + getDefaultProps() { + return { + showNotification: false + }; + }, + + isPiece() { + return this.props.pieceOrEditions.constructor !== Array; + }, + + getFormDataId() { + return getAclFormDataId(this.isPiece(), this.props.pieceOrEditions); + }, + + showSuccessNotification(response) { + if (typeof this.props.handleSuccess === 'function') { + this.props.handleSuccess(); + } + + if (response.notification) { + const notification = new GlobalNotificationModel(response.notification, 'success'); + GlobalNotificationActions.appendGlobalNotification(notification); + } + }, + + render() { + const { + action, + pieceOrEditions, + currentUser, + email, + message, + handleSuccess, + showNotification } = this.props; + + const formMessage = message || getAclFormMessage({ + aclName: action, + entities: pieceOrEditions, + isPiece: this.isPiece(), + senderName: currentUser.username + }); + + if (action === 'acl_consign') { + return ( + + ); + } else if (action === 'acl_unconsign') { + return ( + + ); + } else if (action === 'acl_transfer') { + return ( + + ); + } else if (action === 'acl_loan') { + return ( + + ); + } else if (action === 'acl_loan_request') { + return ( + + ); + } else if (action === 'acl_share') { + return ( + + ); + } else { + throw new Error('Your specified action did not match a form.'); + } + } +}); + +export default AclFormFactory; diff --git a/js/components/ascribe_forms/form_request_action.js b/js/components/ascribe_forms/form_request_action.js index b0f3b6c6..b3b6e3a4 100644 --- a/js/components/ascribe_forms/form_request_action.js +++ b/js/components/ascribe_forms/form_request_action.js @@ -2,10 +2,13 @@ import React from 'react'; -import AclButton from './../ascribe_buttons/acl_button'; -import ActionPanel from '../ascribe_panel/action_panel'; import Form from './form'; +import LoanRequestButton from '../ascribe_buttons/acls/loan_request_button'; +import UnconsignButton from '../ascribe_buttons/acls/unconsign_button'; + +import ActionPanel from '../ascribe_panel/action_panel'; + import NotificationActions from '../../actions/notification_actions'; import GlobalNotificationModel from '../../models/global_notification_model'; @@ -100,9 +103,8 @@ let RequestActionForm = React.createClass({ getAcceptButtonForm(urls) { if(this.props.notifications.action === 'unconsign') { return ( - Date: Fri, 30 Oct 2015 11:16:44 +0100 Subject: [PATCH 03/61] Remove unnecessary import from form utils --- js/utils/form_utils.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/utils/form_utils.js b/js/utils/form_utils.js index d2d2cd29..c15eb067 100644 --- a/js/utils/form_utils.js +++ b/js/utils/form_utils.js @@ -2,8 +2,6 @@ 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) From 67fbfbd470392ed82cbf76785a089f8f4880fe12 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 30 Oct 2015 11:46:01 +0100 Subject: [PATCH 04/61] Update RequestActionForm to use form utils --- .../ascribe_forms/form_request_action.js | 50 ++++++++----------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/js/components/ascribe_forms/form_request_action.js b/js/components/ascribe_forms/form_request_action.js index b3b6e3a4..ddf832b4 100644 --- a/js/components/ascribe_forms/form_request_action.js +++ b/js/components/ascribe_forms/form_request_action.js @@ -16,9 +16,9 @@ import GlobalNotificationActions from '../../actions/global_notification_actions import ApiUrls from '../../constants/api_urls'; +import { getAclFormDataId } from '../../utils/form_utils'; import { getLangText } from '../../utils/lang_utils.js'; - let RequestActionForm = React.createClass({ propTypes: { pieceOrEditions: React.PropTypes.oneOfType([ @@ -30,26 +30,26 @@ let RequestActionForm = React.createClass({ handleSuccess: React.PropTypes.func }, - isPiece(){ + isPiece() { return this.props.pieceOrEditions.constructor !== Array; }, getUrls() { let urls = {}; - if (this.props.notifications.action === 'consign'){ + if (this.props.notifications.action === 'consign') { urls.accept = ApiUrls.ownership_consigns_confirm; urls.deny = ApiUrls.ownership_consigns_deny; - } else if (this.props.notifications.action === 'unconsign'){ + } else if (this.props.notifications.action === 'unconsign') { urls.accept = ApiUrls.ownership_unconsigns; urls.deny = ApiUrls.ownership_unconsigns_deny; - } else if (this.props.notifications.action === 'loan' && !this.isPiece()){ + } else if (this.props.notifications.action === 'loan' && !this.isPiece()) { urls.accept = ApiUrls.ownership_loans_confirm; urls.deny = ApiUrls.ownership_loans_deny; - } else if (this.props.notifications.action === 'loan' && this.isPiece()){ + } else if (this.props.notifications.action === 'loan' && this.isPiece()) { urls.accept = ApiUrls.ownership_loans_pieces_confirm; urls.deny = ApiUrls.ownership_loans_pieces_deny; - } else if (this.props.notifications.action === 'loan_request' && this.isPiece()){ + } else if (this.props.notifications.action === 'loan_request' && this.isPiece()) { urls.accept = ApiUrls.ownership_loans_pieces_request_confirm; urls.deny = ApiUrls.ownership_loans_pieces_request_deny; } @@ -57,37 +57,29 @@ let RequestActionForm = React.createClass({ return urls; }, - getFormData(){ - if (this.isPiece()) { - return {piece_id: this.props.pieceOrEditions.id}; - } - else { - return {bitcoin_id: this.props.pieceOrEditions.map(function(edition){ - return edition.bitcoin_id; - }).join()}; - } + getFormData() { + return getAclFormDataId(this.isPiece(), this.props.pieceOrEditions); }, showNotification(option, action, owner) { return () => { - let message = getLangText('You have successfully') + ' ' + option + ' the ' + action + ' request ' + getLangText('from') + ' ' + owner; + const message = getLangText('You have successfully') + ' ' + option + ' the ' + action + ' request ' + getLangText('from') + ' ' + owner; - let notifications = new GlobalNotificationModel(message, 'success'); + const notifications = new GlobalNotificationModel(message, 'success'); GlobalNotificationActions.appendGlobalNotification(notifications); this.handleSuccess(); - }; }, handleSuccess() { - if (this.isPiece()){ + if (this.isPiece()) { NotificationActions.fetchPieceListNotifications(); - } - else { + } else { NotificationActions.fetchEditionListNotifications(); } - if(this.props.handleSuccess) { + + if (typeof this.props.handleSuccess === 'function') { this.props.handleSuccess(); } }, @@ -101,7 +93,7 @@ let RequestActionForm = React.createClass({ }, getAcceptButtonForm(urls) { - if(this.props.notifications.action === 'unconsign') { + if (this.props.notifications.action === 'unconsign') { return ( ); - } else if(this.props.notifications.action === 'loan_request') { + } else if (this.props.notifications.action === 'loan_request') { return ( @@ -157,7 +149,7 @@ let RequestActionForm = React.createClass({ {acceptButtonForm} @@ -169,7 +161,7 @@ let RequestActionForm = React.createClass({ return ( + buttons={this.getButtonForm()} /> ); } }); From d23331d9b9e46f6862d0212ef1c0278001ac885f Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 30 Oct 2015 17:43:20 +0100 Subject: [PATCH 05/61] Remove ReactS3FineUploader's dependency on react-router's location MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReactS3FineUploader used to check the current url’s query params to determine which method it should use to upload, but this decision means the component is tightly coupled with react-router and history.js. A major pain point is having to propagate the location prop all the way down to this component even when it’s not necessary. Now, ReactS3FineUploader’s parent elements can either parse the current query params themselves or, if they have a location from react-router, simply use the location. Added a few utils to help parse url params. --- .../further_details_fileuploader.js | 8 +- .../ascribe_forms/form_create_contract.js | 8 +- .../ascribe_forms/form_register_piece.js | 9 +- .../ascribe_forms/input_fineuploader.js | 10 +-- .../contract_settings_update_button.js | 8 +- .../file_drag_and_drop.js | 34 ++++---- .../file_drag_and_drop_dialog.js | 56 +++++++------ .../react_s3_fine_uploader.js | 74 +++++++---------- js/fetchers/edition_list_fetcher.js | 2 +- js/fetchers/piece_list_fetcher.js | 2 +- js/utils/fetch_api_utils.js | 57 +------------ js/utils/requests.js | 6 +- js/utils/url_utils.js | 83 +++++++++++++++++++ package.json | 3 + 14 files changed, 190 insertions(+), 170 deletions(-) create mode 100644 js/utils/url_utils.js diff --git a/js/components/ascribe_detail/further_details_fileuploader.js b/js/components/ascribe_detail/further_details_fileuploader.js index c5ef8a1c..33caf9b0 100644 --- a/js/components/ascribe_detail/further_details_fileuploader.js +++ b/js/components/ascribe_detail/further_details_fileuploader.js @@ -20,8 +20,7 @@ let FurtherDetailsFileuploader = React.createClass({ submitFile: React.PropTypes.func, isReadyForFormSubmission: React.PropTypes.func, editable: React.PropTypes.bool, - multiple: React.PropTypes.bool, - location: React.PropTypes.object + multiple: React.PropTypes.bool }, getDefaultProps() { @@ -89,11 +88,10 @@ let FurtherDetailsFileuploader = React.createClass({ }} areAssetsDownloadable={true} areAssetsEditable={this.props.editable} - multiple={this.props.multiple} - location={this.props.location}/> + multiple={this.props.multiple} /> ); } }); -export default FurtherDetailsFileuploader; \ No newline at end of file +export default FurtherDetailsFileuploader; diff --git a/js/components/ascribe_forms/form_create_contract.js b/js/components/ascribe_forms/form_create_contract.js index fe00cebc..aac4c5ea 100644 --- a/js/components/ascribe_forms/form_create_contract.js +++ b/js/components/ascribe_forms/form_create_contract.js @@ -28,8 +28,7 @@ let CreateContractForm = React.createClass({ fileClassToUpload: React.PropTypes.shape({ singular: React.PropTypes.string, plural: React.PropTypes.string - }), - location: React.PropTypes.object + }) }, getInitialState() { @@ -87,8 +86,7 @@ let CreateContractForm = React.createClass({ areAssetsEditable={true} setIsUploadReady={this.setIsUploadReady} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} - fileClassToUpload={this.props.fileClassToUpload} - location={this.props.location}/> + fileClassToUpload={this.props.fileClassToUpload} /> + uploadMethod={this.props.location.query.method} /> + uploadMethod={this.props.uploadMethod} + fileClassToUpload={this.props.fileClassToUpload} /> ); } }); -export default InputFineUploader; \ No newline at end of file +export default InputFineUploader; diff --git a/js/components/ascribe_settings/contract_settings_update_button.js b/js/components/ascribe_settings/contract_settings_update_button.js index f3bab156..ffd5ef4b 100644 --- a/js/components/ascribe_settings/contract_settings_update_button.js +++ b/js/components/ascribe_settings/contract_settings_update_button.js @@ -20,8 +20,7 @@ import { getLangText } from '../../utils/lang_utils'; let ContractSettingsUpdateButton = React.createClass({ propTypes: { - contract: React.PropTypes.object, - location: React.PropTypes.object + contract: React.PropTypes.object }, submitFile(file) { @@ -90,10 +89,9 @@ let ContractSettingsUpdateButton = React.createClass({ plural: getLangText('UPDATE') }} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} - submitFile={this.submitFile} - location={this.props.location}/> + submitFile={this.submitFile} /> ); } }); -export default ContractSettingsUpdateButton; \ No newline at end of file +export default ContractSettingsUpdateButton; 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 38ec459a..430dcab3 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 @@ -27,6 +27,7 @@ let FileDragAndDrop = React.createClass({ areAssetsEditable: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool, + uploadMethod: React.PropTypes.string, // triggers a FileDragAndDrop-global spinner hashingProgress: React.PropTypes.number, @@ -41,8 +42,7 @@ let FileDragAndDrop = React.createClass({ plural: React.PropTypes.string }), - allowedExtensions: React.PropTypes.string, - location: React.PropTypes.object + allowedExtensions: React.PropTypes.string }, handleDragOver(event) { @@ -137,19 +137,19 @@ let FileDragAndDrop = React.createClass({ }, render: function () { - let { filesToUpload, - dropzoneInactive, - className, - hashingProgress, - handleCancelHashing, - multiple, - enableLocalHashing, - fileClassToUpload, - areAssetsDownloadable, - areAssetsEditable, - allowedExtensions, - location - } = this.props; + 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 or canceled let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0; @@ -185,8 +185,8 @@ let FileDragAndDrop = React.createClass({ hasFiles={hasFiles} onClick={this.handleOnClick} enableLocalHashing={enableLocalHashing} - fileClassToUpload={fileClassToUpload} - location={location}/> + uploadMethod={uploadMethod} + fileClassToUpload={fileClassToUpload} /> {getLangText('Drag %s here', fileClass)}

,

{getLangText('or')}

@@ -37,26 +35,31 @@ let FileDragAndDropDialog = React.createClass({ }, render() { - const queryParams = this.props.location.query; + const { + hasFiles, + multipleFiles, + enableLocalHashing, + uploadMethod, + fileClassToUpload, + onClick } = this.props; - if(this.props.hasFiles) { + if (hasFiles) { return null; } else { - if(this.props.enableLocalHashing && !queryParams.method) { + if (enableLocalHashing && !uploadMethod) { + const currentQueryParams = getCurrentQueryParams(); - let queryParamsHash = Object.assign({}, queryParams); + const queryParamsHash = Object.assign({}, currentQueryParams); queryParamsHash.method = 'hash'; - let queryParamsUpload = Object.assign({}, queryParams); + const queryParamsUpload = Object.assign({}, currentQueryParams); queryParamsUpload.method = 'upload'; - let { location } = this.props; - return (

{getLangText('Would you rather')}

{getLangText('Hash your work')} @@ -64,9 +67,9 @@ let FileDragAndDropDialog = React.createClass({ or - + {getLangText('Upload and hash your work')} @@ -75,26 +78,27 @@ let FileDragAndDropDialog = React.createClass({
); } else { - if(this.props.multipleFiles) { + if (multipleFiles) { return ( - {this.getDragDialog(this.props.fileClassToUpload.plural)} + {this.getDragDialog(fileClassToUpload.plural)} - {getLangText('choose %s to upload', this.props.fileClassToUpload.plural)} + onClick={onClick}> + {getLangText('choose %s to upload', fileClassToUpload.plural)} ); } else { - let dialog = queryParams.method === 'hash' ? getLangText('choose a %s to hash', this.props.fileClassToUpload.singular) : getLangText('choose a %s to upload', this.props.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(this.props.fileClassToUpload.singular)} + {this.getDragDialog(fileClassToUpload.singular)} + onClick={onClick}> {dialog} @@ -105,4 +109,4 @@ let FileDragAndDropDialog = React.createClass({ } }); -export default FileDragAndDropDialog; \ No newline at end of file +export default FileDragAndDropDialog; diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index e8cc8bfa..61dbcbcc 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -18,7 +18,6 @@ import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } import { getCookie } from '../../utils/fetch_api_utils'; import { getLangText } from '../../utils/lang_utils'; - let ReactS3FineUploader = React.createClass({ propTypes: { keyRoutine: React.PropTypes.shape({ @@ -107,11 +106,14 @@ let ReactS3FineUploader = React.createClass({ // 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 essentially enables that behavior + // + // 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: React.PropTypes.bool, - - // automatically injected by React-Router - query: React.PropTypes.object, + uploadMethod: React.PropTypes.string, // A class of a file the user has to upload // Needs to be defined both in singular as well as in plural @@ -126,9 +128,7 @@ let ReactS3FineUploader = React.createClass({ fileInputElement: React.PropTypes.oneOfType([ React.PropTypes.func, React.PropTypes.element - ]), - - location: React.PropTypes.object + ]) }, getDefaultProps() { @@ -192,11 +192,11 @@ let ReactS3FineUploader = React.createClass({ filesToUpload: [], uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()), csrfToken: getCookie(AppConstants.csrftoken), - + // -1: aborted // -2: uninitialized hashingProgress: -2, - + // this is for logging chunks: {} }; @@ -354,7 +354,6 @@ let ReactS3FineUploader = React.createClass({ /* FineUploader specific callback function handlers */ onUploadChunk(id, name, chunkData) { - let chunks = this.state.chunks; chunks[id + '-' + chunkData.startByte + '-' + chunkData.endByte] = { @@ -370,10 +369,9 @@ let ReactS3FineUploader = React.createClass({ }, onUploadChunkSuccess(id, chunkData, responseJson, xhr) { - let chunks = this.state.chunks; let chunkKey = id + '-' + chunkData.startByte + '-' + chunkData.endByte; - + if(chunks[chunkKey]) { chunks[chunkKey].completed = true; chunks[chunkKey].responseJson = responseJson; @@ -414,7 +412,7 @@ let ReactS3FineUploader = React.createClass({ } else { console.warn('You didn\'t define submitFile in 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, @@ -597,7 +595,6 @@ let ReactS3FineUploader = React.createClass({ } else { throw new Error(getLangText('File upload could not be paused.')); } - }, handleResumeFile(fileId) { @@ -647,16 +644,14 @@ let ReactS3FineUploader = React.createClass({ // md5 hash of a file locally and just upload a txt file containing that hash. // // In the view this only happens when the user is allowed to do local hashing as well - // as when the correct query parameter is present in the url ('hash' and not 'upload') - let queryParams = this.props.location.query; - if(this.props.enableLocalHashing && queryParams && queryParams.method === 'hash') { - - let convertedFilePromises = []; + // as when the correct method prop is present ('hash' and not 'upload') + if (this.props.enableLocalHashing && this.props.uploadMethod === 'hash') { + const convertedFilePromises = []; let overallFileSize = 0; + // "files" is not a classical Javascript array but a Javascript FileList, therefore // we can not use map to convert values for(let i = 0; i < files.length; i++) { - // for calculating the overall progress of all submitted files // we'll need to calculate the overall sum of all files' sizes overallFileSize += files[i].size; @@ -668,7 +663,6 @@ let ReactS3FineUploader = React.createClass({ // we're using promises to handle that let hashedFilePromise = computeHashOfFile(files[i]); convertedFilePromises.push(hashedFilePromise); - } // To react after the computation of all files, we define the resolvement @@ -676,7 +670,6 @@ let ReactS3FineUploader = React.createClass({ // with their txt representative Q.all(convertedFilePromises) .progress(({index, value: {progress, reject}}) => { - // hashing progress has been aborted from outside // To get out of the executing, we need to call reject from the // inside of the promise's execution. @@ -696,18 +689,14 @@ let ReactS3FineUploader = React.createClass({ // currently hashing files let overallHashingProgress = 0; for(let i = 0; i < files.length; i++) { - let filesSliceOfOverall = files[i].size / overallFileSize; overallHashingProgress += filesSliceOfOverall * files[i].progress; - } // Multiply by 100, since react-progressbar expects decimal numbers this.setState({ hashingProgress: overallHashingProgress * 100}); - }) .then((convertedFiles) => { - // clear hashing progress, since its done this.setState({ hashingProgress: -2}); @@ -823,20 +812,18 @@ let ReactS3FineUploader = React.createClass({ changeSet.status = { $set: status }; let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet }); - + this.setState({ filesToUpload }); }, isDropzoneInactive() { - let filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1); - let queryParams = this.props.location.query; + const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1); - if((this.props.enableLocalHashing && !queryParams.method) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) { + if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) { return true; } else { return false; } - }, getAllowedExtensions() { @@ -850,17 +837,16 @@ let ReactS3FineUploader = React.createClass({ }, render() { - let { - multiple, - areAssetsDownloadable, - areAssetsEditable, - onInactive, - enableLocalHashing, - fileClassToUpload, - validation, - fileInputElement, - location - } = this.props; + const { + multiple, + areAssetsDownloadable, + areAssetsEditable, + onInactive, + enableLocalHashing, + uploadMethod, + fileClassToUpload, + validation, + fileInputElement } = this.props; // Here we initialize the template that has been either provided from the outside // or the default input that is FileDragAndDrop. @@ -870,8 +856,8 @@ let ReactS3FineUploader = React.createClass({ areAssetsEditable, onInactive, enableLocalHashing, + uploadMethod, fileClassToUpload, - location, onDrop: this.handleUploadFile, filesToUpload: this.state.filesToUpload, handleDeleteFile: this.handleDeleteFile, diff --git a/js/fetchers/edition_list_fetcher.js b/js/fetchers/edition_list_fetcher.js index b416c595..93e4553d 100644 --- a/js/fetchers/edition_list_fetcher.js +++ b/js/fetchers/edition_list_fetcher.js @@ -2,8 +2,8 @@ import requests from '../utils/requests'; -import { generateOrderingQueryParams } from '../utils/fetch_api_utils'; import { mergeOptions } from '../utils/general_utils'; +import { generateOrderingQueryParams } from '../utils/url_utils'; let EditionListFetcher = { /** diff --git a/js/fetchers/piece_list_fetcher.js b/js/fetchers/piece_list_fetcher.js index 8e58402a..6bd4eb3a 100644 --- a/js/fetchers/piece_list_fetcher.js +++ b/js/fetchers/piece_list_fetcher.js @@ -3,7 +3,7 @@ import requests from '../utils/requests'; import { mergeOptions } from '../utils/general_utils'; -import { generateOrderingQueryParams } from '../utils/fetch_api_utils'; +import { generateOrderingQueryParams } from '../utils/url_utils'; let PieceListFetcher = { /** diff --git a/js/utils/fetch_api_utils.js b/js/utils/fetch_api_utils.js index 3ed964ba..cb676fce 100644 --- a/js/utils/fetch_api_utils.js +++ b/js/utils/fetch_api_utils.js @@ -2,63 +2,10 @@ import Q from 'q'; -import { sanitize } from './general_utils'; import AppConstants from '../constants/application_constants'; // TODO: Create Unittests that test all functions - /** - * Takes a key-value object of this form: - * - * { - * 'page': 1, - * 'pageSize': 10 - * } - * - * and converts it to a query-parameter, which you can append to your URL. - * The return looks like this: - * - * ?page=1&page_size=10 - * - * CamelCase gets converted to snake_case! - * - */ -export function argsToQueryParams(obj) { - - obj = sanitize(obj); - - return Object - .keys(obj) - .map((key, i) => { - let s = ''; - - if(i === 0) { - s += '?'; - } else { - s += '&'; - } - - let snakeCaseKey = key.replace(/[A-Z]/, (match) => '_' + match.toLowerCase()); - - return s + snakeCaseKey + '=' + encodeURIComponent(obj[key]); - }) - .join(''); -} - -/** - * Takes a string and a boolean and generates a string query parameter for - * an API call. - */ -export function generateOrderingQueryParams(orderBy, orderAsc) { - let interpolation = ''; - - if(!orderAsc) { - interpolation += '-'; - } - - return interpolation + orderBy; -} - export function status(response) { if (response.status >= 200 && response.status < 300) { return response; @@ -68,7 +15,7 @@ export function status(response) { export function getCookie(name) { let parts = document.cookie.split(';'); - + for(let i = 0; i < parts.length; i++) { if(parts[i].indexOf(AppConstants.csrftoken + '=') > -1) { return parts[i].split('=').pop(); @@ -111,4 +58,4 @@ export function fetchImageAsBlob(url) { xhr.send(); }); -} \ No newline at end of file +} diff --git a/js/utils/requests.js b/js/utils/requests.js index 7e9c9a58..bf203751 100644 --- a/js/utils/requests.js +++ b/js/utils/requests.js @@ -2,11 +2,11 @@ import Q from 'q'; -import { argsToQueryParams, getCookie } from '../utils/fetch_api_utils'; - import AppConstants from '../constants/application_constants'; -import {excludePropFromObject} from '../utils/general_utils'; +import { getCookie } from '../utils/fetch_api_utils'; +import { excludePropFromObject } from '../utils/general_utils'; +import { argsToQueryParams } from '../utils/url_utils'; class Requests { _merge(defaults, options) { diff --git a/js/utils/url_utils.js b/js/utils/url_utils.js new file mode 100644 index 00000000..cc875981 --- /dev/null +++ b/js/utils/url_utils.js @@ -0,0 +1,83 @@ +'use strict' + +import camelCase from 'camelcase'; +import snakeCase from 'snake-case'; +import qs from 'qs'; + +import { sanitize } from './general_utils'; + +// TODO: Create Unittests that test all functions + +/** + * Takes a key-value dictionary of this form: + * + * { + * 'page': 1, + * 'pageSize': 10 + * } + * + * and converts it to a query-parameter, which you can append to your URL. + * The return looks like this: + * + * ?page=1&page_size=10 + * + * CamelCase gets converted to snake_case! + * + * @param {object} obj Query params dictionary + * @return {string} Query params string + */ +export function argsToQueryParams(obj) { + const sanitizedObj = sanitize(obj); + const queryParamObj = {}; + + Object + .keys(sanitizedObj) + .forEach((key) => { + queryParamObj[snakeCase(key)] = sanitizedObj[key]; + }); + + // Use bracket arrayFormat as history.js and react-router use it + return '?' + qs.stringify(queryParamObj, { arrayFormat: 'brackets' }); +} + +/** + * Get the current url's query params as an key-val dictionary. + * snake_case gets converted to CamelCase! + * @return {object} Query params dictionary + */ +export function getCurrentQueryParams() { + return queryParamsToArgs(window.location.search.substring(1)); +} + +/** + * Convert the given query param string into a key-val dictionary. + * snake_case gets converted to CamelCase! + * @param {string} queryParamString Query params string + * @return {object} Query params dictionary + */ +export function queryParamsToArgs(queryParamString) { + const qsQueryParamObj = qs.parse(queryParamString); + const camelCaseParamObj = {}; + + Object + .keys(qsQueryParamObj) + .forEach((key) => { + camelCaseParamObj[camelCase(key)] = qsQueryParamObj[key]; + }); + + return camelCaseParamObj; +} + +/** + * Takes a string and a boolean and generates a string query parameter for + * an API call. + */ +export function generateOrderingQueryParams(orderBy, orderAsc) { + let interpolation = ''; + + if(!orderAsc) { + interpolation += '-'; + } + + return interpolation + orderBy; +} diff --git a/package.json b/package.json index 2b770fd3..4e7cd6a9 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "browser-sync": "^2.7.5", "browserify": "^9.0.8", "browserify-shim": "^3.8.10", + "camelcase": "^1.2.1", "classnames": "^1.2.2", "compression": "^1.4.4", "envify": "^3.4.0", @@ -73,6 +74,7 @@ "object-assign": "^2.0.0", "opn": "^3.0.2", "q": "^1.4.1", + "qs": "^5.2.0", "raven-js": "^1.1.19", "react": "0.13.2", "react-bootstrap": "0.25.1", @@ -83,6 +85,7 @@ "react-textarea-autosize": "^2.5.2", "reactify": "^1.1.0", "shmui": "^0.1.0", + "snake-case": "^1.1.1", "spark-md5": "~1.0.0", "uglifyjs": "^2.4.10", "vinyl-buffer": "^1.0.0", From 1e328b722b8009a6ec3e9c8a2f4e7998041d5ac4 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 30 Oct 2015 17:46:51 +0100 Subject: [PATCH 06/61] Sanitize utility should not modify given object Mutating arguments and then returning them is redundant and confusing behaviour (why pass it back if they already have it? Am I getting a new copy since it returns something?). --- js/utils/general_utils.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index 7c13f9b5..cd73ba45 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -1,29 +1,23 @@ 'use strict'; +import _ from 'lodash'; + /** - * Takes an object and deletes all keys that are - * - * tagged as false by the passed in filter function + * Takes an object and returns a shallow copy without any keys + * that fail the passed in filter function. + * Does not modify the passed in object. * * @param {object} obj regular javascript object * @return {object} regular javascript object without null values or empty strings */ export function sanitize(obj, filterFn) { - if(!filterFn) { + if (!filterFn) { // By matching null with a double equal, we can match undefined and null // http://stackoverflow.com/a/15992131 filterFn = (val) => val == null || val === ''; } - Object - .keys(obj) - .map((key) => { - if(filterFn(obj[key])) { - delete obj[key]; - } - }); - - return obj; + return _.omit(obj, filterFn); } /** From 147c852b022adb0a5214d8a28924c7e12c4bb3c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Mon, 2 Nov 2015 10:32:48 +0100 Subject: [PATCH 07/61] Replace getFullYear() with getUTCFullYear() --- .../ascribe_accordion_list/accordion_list_item_wallet.js | 2 +- js/components/ascribe_detail/edition.js | 2 +- js/components/ascribe_detail/piece_container.js | 2 +- .../ascribe_accordion_list/accordion_list_item_prize.js | 2 +- .../prize/components/ascribe_detail/prize_piece_container.js | 2 +- .../wallet/components/ascribe_detail/wallet_piece_container.js | 2 +- .../cyland/cyland_accordion_list/cyland_accordion_list_item.js | 2 +- .../ikonotv_accordion_list/ikonotv_accordion_list_item.js | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) 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 185f6e05..8899c67e 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -129,7 +129,7 @@ let AccordionListItemWallet = React.createClass({ piece={this.props.content} subsubheading={
- {new Date(this.props.content.date_created).getFullYear()} + {new Date(this.props.content.date_created).getUTCFullYear()} {this.getLicences()}
} buttons={ diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 64cbf714..ae53728f 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -85,7 +85,7 @@ let Edition = React.createClass({

{this.props.edition.title}

- +

{this.state.piece.title}

- + {this.state.piece.num_editions > 0 ? : null}
diff --git a/js/components/whitelabel/prize/components/ascribe_accordion_list/accordion_list_item_prize.js b/js/components/whitelabel/prize/components/ascribe_accordion_list/accordion_list_item_prize.js index caef504b..8eac81d1 100644 --- a/js/components/whitelabel/prize/components/ascribe_accordion_list/accordion_list_item_prize.js +++ b/js/components/whitelabel/prize/components/ascribe_accordion_list/accordion_list_item_prize.js @@ -182,7 +182,7 @@ let AccordionListItemPrize = React.createClass({ artistName={artistName} subsubheading={
- {new Date(this.props.content.date_created).getFullYear()} + {new Date(this.props.content.date_created).getUTCFullYear()}
} buttons={this.getPrizeButtons()} badge={this.getPrizeBadge()}> diff --git a/js/components/whitelabel/prize/components/ascribe_detail/prize_piece_container.js b/js/components/whitelabel/prize/components/ascribe_detail/prize_piece_container.js index 07e84b0e..6bd47c18 100644 --- a/js/components/whitelabel/prize/components/ascribe_detail/prize_piece_container.js +++ b/js/components/whitelabel/prize/components/ascribe_detail/prize_piece_container.js @@ -141,7 +141,7 @@ let PieceContainer = React.createClass({

{this.state.piece.title}

- + {artistEmail} {this.getActions()}
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 5644b5b0..e765bd7b 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 @@ -39,7 +39,7 @@ let WalletPieceContainer = React.createClass({

{this.props.piece.title}

- +
} diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js b/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js index 755e550b..0b4ec543 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js @@ -100,7 +100,7 @@ let CylandAccordionListItem = React.createClass({ piece={this.props.content} subsubheading={
- {new Date(this.props.content.date_created).getFullYear()} + {new Date(this.props.content.date_created).getUTCFullYear()}
} buttons={this.getSubmitButtons()}> {this.props.children} diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js index 7445eb36..e1187b7c 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js @@ -106,7 +106,7 @@ let IkonotvAccordionListItem = React.createClass({ piece={this.props.content} subsubheading={
- {new Date(this.props.content.date_created).getFullYear()} + {new Date(this.props.content.date_created).getUTCFullYear()}
} buttons={this.getSubmitButtons()}> {this.props.children} From a513af984d0d7e98d419b8ff376e3411d2d01360 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 2 Nov 2015 10:41:59 +0100 Subject: [PATCH 08/61] Update cyland for FineUploader changes --- .../cyland/cyland_detail/cyland_piece_container.js | 4 +--- .../cyland/cyland_forms/cyland_additional_data_form.js | 8 +++----- .../wallet/components/cyland/cyland_register_piece.js | 3 +-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js b/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js index 79d63abf..7e784981 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js @@ -33,7 +33,6 @@ import { mergeOptions } from '../../../../../../utils/general_utils'; let CylandPieceContainer = React.createClass({ propTypes: { - location: React.PropTypes.object, params: React.PropTypes.object }, @@ -107,8 +106,7 @@ let CylandPieceContainer = React.createClass({ + isInline={true} /> ); diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js b/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js index 63863b2d..0adfcb40 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js @@ -26,8 +26,7 @@ let CylandAdditionalDataForm = React.createClass({ handleSuccess: React.PropTypes.func, piece: React.PropTypes.object.isRequired, disabled: React.PropTypes.bool, - isInline: React.PropTypes.bool, - location: React.PropTypes.object + isInline: React.PropTypes.bool }, getDefaultProps() { @@ -143,8 +142,7 @@ let CylandAdditionalDataForm = React.createClass({ isReadyForFormSubmission={formSubmissionValidation.fileOptional} pieceId={piece.id} otherData={piece.other_data} - multiple={true} - location={this.props.location}/> + multiple={true} /> ); } else { @@ -157,4 +155,4 @@ let CylandAdditionalDataForm = React.createClass({ } }); -export default CylandAdditionalDataForm; \ No newline at end of file +export default CylandAdditionalDataForm; 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 ca755cf4..1903c7a2 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js @@ -210,8 +210,7 @@ let CylandRegisterPiece = React.createClass({ 1} handleSuccess={this.handleAdditionalDataSuccess} - piece={this.state.piece} - location={this.props.location}/> + piece={this.state.piece} /> From 5f5461c10ddd11362a79708913e3d937cdd74970 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 2 Nov 2015 10:42:15 +0100 Subject: [PATCH 09/61] Remove warning for missing prop from FurtherDetailsFileUploader --- js/components/ascribe_detail/further_details_fileuploader.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/components/ascribe_detail/further_details_fileuploader.js b/js/components/ascribe_detail/further_details_fileuploader.js index 33caf9b0..9a1f091c 100644 --- a/js/components/ascribe_detail/further_details_fileuploader.js +++ b/js/components/ascribe_detail/further_details_fileuploader.js @@ -43,6 +43,7 @@ let FurtherDetailsFileuploader = React.createClass({ return ( Date: Mon, 2 Nov 2015 12:10:41 +0100 Subject: [PATCH 10/61] Check for a new csrf token on componentWillReceiveProps instead of componentWillUpdate this.setState() should not be used in componentWillUpdate(): https://facebook.github.io/react/docs/component-specs.html#updating-comp onentwillupdate --- js/components/ascribe_uploader/react_s3_fine_uploader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index 61dbcbcc..685c2b2f 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -202,7 +202,7 @@ let ReactS3FineUploader = React.createClass({ }; }, - componentWillUpdate() { + componentWillReceiveProps() { // since the csrf header is defined in this component's props, // everytime the csrf cookie is changed we'll need to reinitalize // fineuploader and update the actual csrf token From 6c8016e094138db91f7e602ea2381eb7a1e762ac Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 2 Nov 2015 15:19:52 +0100 Subject: [PATCH 11/61] Remove misleading editions prop to pieceOrEditions --- js/components/ascribe_buttons/acl_button_list.js | 14 +++++++------- .../ascribe_detail/edition_action_panel.js | 4 ++-- js/components/ascribe_detail/piece_container.js | 2 +- .../piece_list_bulk_modal.js | 4 ++-- .../ascribe_detail/wallet_action_panel.js | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/js/components/ascribe_buttons/acl_button_list.js b/js/components/ascribe_buttons/acl_button_list.js index 6aec6c77..83495363 100644 --- a/js/components/ascribe_buttons/acl_button_list.js +++ b/js/components/ascribe_buttons/acl_button_list.js @@ -17,7 +17,7 @@ import { mergeOptions } from '../../utils/general_utils'; let AclButtonList = React.createClass({ propTypes: { className: React.PropTypes.string, - editions: React.PropTypes.oneOfType([ + pieceOrEditions: React.PropTypes.oneOfType([ React.PropTypes.object, React.PropTypes.array ]).isRequired, @@ -82,7 +82,7 @@ let AclButtonList = React.createClass({ const { className, buttonsStyle, availableAcls, - editions, + pieceOrEditions, handleSuccess } = this.props; const { currentUser } = this.state; @@ -92,27 +92,27 @@ let AclButtonList = React.createClass({ {this.renderChildren()} diff --git a/js/components/ascribe_detail/edition_action_panel.js b/js/components/ascribe_detail/edition_action_panel.js index 7b075ce0..a2ad1e58 100644 --- a/js/components/ascribe_detail/edition_action_panel.js +++ b/js/components/ascribe_detail/edition_action_panel.js @@ -107,7 +107,7 @@ let EditionActionPanel = React.createClass({ Date: Mon, 2 Nov 2015 15:20:02 +0100 Subject: [PATCH 12/61] Remove unnecessary default props --- js/components/ascribe_buttons/acls/acl_button.js | 6 ------ js/components/ascribe_forms/acl_form_factory.js | 6 ------ 2 files changed, 12 deletions(-) diff --git a/js/components/ascribe_buttons/acls/acl_button.js b/js/components/ascribe_buttons/acls/acl_button.js index f7149e79..d81a19f4 100644 --- a/js/components/ascribe_buttons/acls/acl_button.js +++ b/js/components/ascribe_buttons/acls/acl_button.js @@ -36,12 +36,6 @@ let AclButton = React.createClass({ className: React.PropTypes.string }, - getDefaultProps() { - return { - buttonAcceptClassName: '' - }; - }, - // Removes the acl_ prefix and converts to upper case sanitizeAction() { if (this.props.buttonAcceptName) { diff --git a/js/components/ascribe_forms/acl_form_factory.js b/js/components/ascribe_forms/acl_form_factory.js index dc5ebd4e..d5494c2d 100644 --- a/js/components/ascribe_forms/acl_form_factory.js +++ b/js/components/ascribe_forms/acl_form_factory.js @@ -31,12 +31,6 @@ let AclFormFactory = React.createClass({ showNotification: React.PropTypes.bool }, - getDefaultProps() { - return { - showNotification: false - }; - }, - isPiece() { return this.props.pieceOrEditions.constructor !== Array; }, From 7746241a592d9f0876e21c2e2167fd4288870a5e Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 2 Nov 2015 15:21:27 +0100 Subject: [PATCH 13/61] Fix getLangText() when using multiple placeholders --- js/utils/lang_utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/utils/lang_utils.js b/js/utils/lang_utils.js index f2e2fa14..ee2d292c 100644 --- a/js/utils/lang_utils.js +++ b/js/utils/lang_utils.js @@ -22,15 +22,15 @@ export function getLangText(s, ...args) { let lang = getLang(); try { if(lang in languages) { - return formatText(languages[lang][s], args); + return formatText(languages[lang][s], ...args); } else { // just use the english language - return formatText(languages['en-US'][s], args); + return formatText(languages['en-US'][s], ...args); } } catch(err) { //if(!(s in languages[lang])) { //console.warn('Language-string is not in constants file. Add: "' + s + '" to the "' + lang + '" language file. Defaulting to keyname'); - return formatText(s, args); + return formatText(s, ...args); //} else { // console.error(err); //} From a0ebc7dc58ca948f477b6d6e3e1323f00f43813f Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 2 Nov 2015 15:21:33 +0100 Subject: [PATCH 14/61] Use string formatting for RequestActionForm's notification message --- js/components/ascribe_forms/form_request_action.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/js/components/ascribe_forms/form_request_action.js b/js/components/ascribe_forms/form_request_action.js index ddf832b4..d3d1bc71 100644 --- a/js/components/ascribe_forms/form_request_action.js +++ b/js/components/ascribe_forms/form_request_action.js @@ -63,8 +63,7 @@ let RequestActionForm = React.createClass({ showNotification(option, action, owner) { return () => { - const message = getLangText('You have successfully') + ' ' + option + ' the ' + action + ' request ' + getLangText('from') + ' ' + owner; - + const message = getLangText('You have successfully %s the %s request from %s', getLangText(option), getLangText(action), owner); const notifications = new GlobalNotificationModel(message, 'success'); GlobalNotificationActions.appendGlobalNotification(notifications); @@ -118,7 +117,7 @@ let RequestActionForm = React.createClass({ url={urls.accept} getFormData={this.getFormData} handleSuccess={ - this.showNotification(getLangText('accepted'), this.props.notifications.action, this.props.notifications.by) + this.showNotification('accepted', this.props.notifications.action, this.props.notifications.by) } isInline={true} className='inline pull-right'> @@ -143,7 +142,7 @@ let RequestActionForm = React.createClass({ isInline={true} getFormData={this.getFormData} handleSuccess={ - this.showNotification(getLangText('denied'), this.props.notifications.action, this.props.notifications.by) + this.showNotification('denied', this.props.notifications.action, this.props.notifications.by) } className='inline pull-right'>