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 47c0fb77..5d3e033f 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 @@ -125,7 +125,6 @@ let AccordionListItemEditionWidget = React.createClass({ ); } else { let editionMapping = piece && piece.first_edition ? piece.first_edition.num_editions_available + '/' + piece.num_editions : ''; - return ( } diff --git a/js/components/ascribe_buttons/acl_button_list.js b/js/components/ascribe_buttons/acl_button_list.js index b9139979..e87a6407 100644 --- a/js/components/ascribe_buttons/acl_button_list.js +++ b/js/components/ascribe_buttons/acl_button_list.js @@ -1,12 +1,15 @@ 'use strict'; -import React from 'react'; +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 { mergeOptions } from '../../utils/general_utils'; + + let AclButtonList = React.createClass({ propTypes: { className: React.PropTypes.string, @@ -15,6 +18,7 @@ let AclButtonList = React.createClass({ React.PropTypes.array ]), availableAcls: React.PropTypes.object, + buttonsStyle: React.PropTypes.object, handleSuccess: React.PropTypes.func, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), @@ -23,56 +27,97 @@ let AclButtonList = React.createClass({ }, getInitialState() { - return UserStore.getState(); + return mergeOptions( + UserStore.getState(), + { + buttonListSize: 0 + } + ); }, componentDidMount() { UserStore.listen(this.onChange); UserActions.fetchCurrentUser(); + + window.addEventListener('resize', this.handleResize); + window.dispatchEvent(new Event('resize')); + }, + + componentDidUpdate(prevProps) { + if(prevProps.availableAcls && prevProps.availableAcls !== this.props.availableAcls) { + window.dispatchEvent(new Event('resize')); + } }, componentWillUnmount() { UserStore.unlisten(this.onChange); + + window.removeEventListener('resize', this.handleResize); + }, + + handleResize() { + this.setState({ + buttonListSize: this.refs.buttonList.getDOMNode().offsetWidth + }); }, onChange(state) { this.setState(state); }, + renderChildren() { + const { children } = this.props; + const { buttonListSize } = this.state; + + return React.Children.map(children, (child) => { + return React.addons.cloneWithProps(child, { buttonListSize }); + }); + }, + render() { + const { className, + buttonsStyle, + availableAcls, + editions, + handleSuccess } = this.props; + + const { currentUser } = this.state; + return ( -
- - - - - - {this.props.children} +
+ + + + + + + {this.renderChildren()} +
); } diff --git a/js/components/ascribe_buttons/acl_information.js b/js/components/ascribe_buttons/acl_information.js new file mode 100644 index 00000000..8d412e02 --- /dev/null +++ b/js/components/ascribe_buttons/acl_information.js @@ -0,0 +1,133 @@ +'use strict'; + +import React from 'react'; +import classnames from 'classnames'; + +import { InformationTexts } from '../../constants/information_text'; +import { replaceSubstringAtIndex, sanitize, intersectLists } from '../../utils/general_utils'; +import { getLangText } from '../../utils/lang_utils'; + + +let AclInformation = React.createClass({ + propTypes: { + verbs: React.PropTypes.arrayOf(React.PropTypes.string), + aim: React.PropTypes.string.isRequired, + aclObject: React.PropTypes.object, + + // Must be inserted from the outside + buttonListSize: React.PropTypes.number.isRequired + }, + + getDefaultProps() { + return { + buttonListSize: 400 + }; + }, + + getInitialState() { + return { isVisible: false }; + }, + + onOff() { + if(!this.state.isVisible) { + this.setState({ isVisible: true }); + } + else { + this.setState({ isVisible: false }); + } + }, + + getInfoText(title, info, example){ + let aim = this.props.aim; + + if(aim) { + if(aim === 'form') { + return ( +

+ + {replaceSubstringAtIndex(info.slice(2), 's ', ' ')} + + + {' ' + example} + +

+ ); + } + else if(aim === 'button') { + return ( +

+ + {title} + + + {info + ' '} + + + {example} + +

+ ); + } + } + else { + console.log('Aim is required when you want to place information text'); + } + }, + + produceInformationBlock() { + const { titles, informationSentences, exampleSentences } = InformationTexts; + const { verbs, aim } = this.props; + + const availableInformations = intersectLists(verbs, Object.keys(titles)); + + // sorting is not needed, as `this.props.verbs` takes care of sorting already + // So we assume a user of `AclInformationButton` puts an ordered version of + // `verbs` into `propTypes` + let verbsToDisplay = []; + + + if(aim === 'form' && availableInformations.length > 0) { + verbsToDisplay = verbsToDisplay.concat(verbs); + } else if(aim === 'button' && this.props.aclObject) { + const { aclObject } = this.props; + const sanitizedAclObject = sanitize(aclObject, (val) => !val); + verbsToDisplay = verbsToDisplay.concat(intersectLists(verbs, Object.keys(sanitizedAclObject))); + } + + return verbsToDisplay.map((verb) => { + return this.getInfoText(getLangText(titles[verb]), getLangText(informationSentences[verb]), getLangText(exampleSentences[verb])); + }); + }, + + getButton() { + return this.props.aim === 'button' ? + ; + btnDelete = ; } else if(availableAcls.acl_unshare){ @@ -57,7 +57,7 @@ let DeleteButton = React.createClass({ title = getLangText('Remove Piece from Collection'); } - btnDelete = ; + btnDelete = ; } else { return null; diff --git a/js/components/ascribe_detail/detail_property.js b/js/components/ascribe_detail/detail_property.js index 828ed81a..9ea37285 100644 --- a/js/components/ascribe_detail/detail_property.js +++ b/js/components/ascribe_detail/detail_property.js @@ -2,6 +2,7 @@ import React from 'react'; + let DetailProperty = React.createClass({ propTypes: { label: React.PropTypes.string, @@ -12,20 +13,29 @@ let DetailProperty = React.createClass({ separator: React.PropTypes.string, labelClassName: React.PropTypes.string, valueClassName: React.PropTypes.string, - ellipsis: React.PropTypes.bool + ellipsis: React.PropTypes.bool, + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]) }, getDefaultProps() { return { separator: '', - labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-2 col-xs-height col-bottom ascribe-detail-property-label', + labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-2 col-xs-height ascribe-detail-property-label', valueClassName: 'col-xs-9 col-sm-9 col-md-10 col-lg-10 col-xs-height col-bottom ascribe-detail-property-value' }; }, render() { - let value = this.props.value; let styles = {}; + const { labelClassName, + label, + separator, + valueClassName, + children, + value } = this.props; if(this.props.ellipsis) { styles = { @@ -35,30 +45,16 @@ let DetailProperty = React.createClass({ }; } - - if (this.props.children){ - value = ( -
-
- { this.props.value } -
-
- { this.props.children } -
-
); - } return (
-
- { this.props.label } { this.props.separator} +
+ {label} {separator}
- {value} + {children || value}
diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index bcd2902c..ccab60a0 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -145,7 +145,6 @@ let Edition = React.createClass({ url={ApiUrls.note_public_edition} currentUser={this.state.currentUser}/> - - {this.getStatus()} - + + +
); diff --git a/js/components/ascribe_detail/edition_action_panel.js b/js/components/ascribe_detail/edition_action_panel.js index bd423e4c..6beda543 100644 --- a/js/components/ascribe_detail/edition_action_panel.js +++ b/js/components/ascribe_detail/edition_action_panel.js @@ -22,6 +22,8 @@ import DeleteButton from '../ascribe_buttons/delete_button'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; +import AclInformation from '../ascribe_buttons/acl_information'; + import AclProxy from '../acl_proxy'; import ApiUrls from '../../constants/api_urls'; @@ -103,7 +105,7 @@ let EditionActionPanel = React.createClass({ @@ -122,7 +124,7 @@ let EditionActionPanel = React.createClass({ type="text" value={edition.bitcoin_id} /> - @@ -158,6 +160,10 @@ let EditionActionPanel = React.createClass({ + diff --git a/js/components/ascribe_detail/piece_container.js b/js/components/ascribe_detail/piece_container.js index 678dda39..f8a2369f 100644 --- a/js/components/ascribe_detail/piece_container.js +++ b/js/components/ascribe_detail/piece_container.js @@ -27,6 +27,8 @@ import CreateEditionsForm from '../ascribe_forms/create_editions_form'; import CreateEditionsButton from '../ascribe_buttons/create_editions_button'; import DeleteButton from '../ascribe_buttons/delete_button'; +import AclInformation from '../ascribe_buttons/acl_information'; + import ListRequestActions from '../ascribe_forms/list_form_request_actions'; import GlobalNotificationModel from '../../models/global_notification_model'; @@ -188,24 +190,29 @@ let PieceContainer = React.createClass({ currentUser={this.state.currentUser} handleSuccess={this.loadPiece} notifications={this.state.piece.notifications}/>); - } - else { + } else { return ( - - - - + + + + + + + ); } }, diff --git a/js/components/ascribe_forms/form.js b/js/components/ascribe_forms/form.js index c5f60b76..fe15f537 100644 --- a/js/components/ascribe_forms/form.js +++ b/js/components/ascribe_forms/form.js @@ -288,10 +288,8 @@ let Form = React.createClass({ {this.renderChildren()} {this.getButtons()} - ); } }); - export default Form; diff --git a/js/components/ascribe_forms/form_consign.js b/js/components/ascribe_forms/form_consign.js index f57e0045..5f6e2fc8 100644 --- a/js/components/ascribe_forms/form_consign.js +++ b/js/components/ascribe_forms/form_consign.js @@ -10,7 +10,7 @@ import InputTextAreaToggable from './input_textarea_toggable'; import AscribeSpinner from '../ascribe_spinner'; import { getLangText } from '../../utils/lang_utils.js'; - +import AclInformation from '../ascribe_buttons/acl_information'; let ConsignForm = React.createClass({ propTypes: { @@ -47,6 +47,7 @@ let ConsignForm = React.createClass({

}> + diff --git a/js/components/ascribe_forms/form_delete_edition.js b/js/components/ascribe_forms/form_delete_edition.js index c13e0fa7..6ae8ddd6 100644 --- a/js/components/ascribe_forms/form_delete_edition.js +++ b/js/components/ascribe_forms/form_delete_edition.js @@ -8,7 +8,7 @@ import ApiUrls from '../../constants/api_urls'; import AscribeSpinner from '../ascribe_spinner'; import { getLangText } from '../../utils/lang_utils'; - +import AclInformation from '../ascribe_buttons/acl_information'; let EditionDeleteForm = React.createClass({ @@ -60,6 +60,7 @@ let EditionDeleteForm = React.createClass({

}> +

{getLangText('Are you sure you would like to permanently delete this edition')}?

{getLangText('This is an irrevocable action%s', '.')}

diff --git a/js/components/ascribe_forms/form_delete_piece.js b/js/components/ascribe_forms/form_delete_piece.js index 4b0c9e39..ee066d3f 100644 --- a/js/components/ascribe_forms/form_delete_piece.js +++ b/js/components/ascribe_forms/form_delete_piece.js @@ -4,6 +4,8 @@ import React from 'react'; import Form from '../ascribe_forms/form'; +import AclInformation from '../ascribe_buttons/acl_information'; + import ApiUrls from '../../constants/api_urls'; import AscribeSpinner from '../ascribe_spinner'; @@ -51,6 +53,7 @@ let PieceDeleteForm = React.createClass({

}> +

{getLangText('Are you sure you would like to permanently delete this piece')}?

{getLangText('This is an irrevocable action%s', '.')}

diff --git a/js/components/ascribe_forms/form_loan.js b/js/components/ascribe_forms/form_loan.js index 919b6118..d6102f14 100644 --- a/js/components/ascribe_forms/form_loan.js +++ b/js/components/ascribe_forms/form_loan.js @@ -19,7 +19,7 @@ import AscribeSpinner from '../ascribe_spinner'; import { mergeOptions } from '../../utils/general_utils'; import { getLangText } from '../../utils/lang_utils'; - +import AclInformation from '../ascribe_buttons/acl_information'; let LoanForm = React.createClass({ propTypes: { @@ -232,6 +232,7 @@ let LoanForm = React.createClass({

{this.props.loanHeading}

+

}> + diff --git a/js/components/ascribe_forms/form_transfer.js b/js/components/ascribe_forms/form_transfer.js index 010c4829..3fb95ff6 100644 --- a/js/components/ascribe_forms/form_transfer.js +++ b/js/components/ascribe_forms/form_transfer.js @@ -9,6 +9,8 @@ import Form from './form'; import Property from './property'; import InputTextAreaToggable from './input_textarea_toggable'; +import AclInformation from '../ascribe_buttons/acl_information'; + import AscribeSpinner from '../ascribe_spinner'; import { getLangText } from '../../utils/lang_utils.js'; @@ -52,6 +54,7 @@ let TransferForm = React.createClass({

}> + diff --git a/js/components/ascribe_modal/modal_wrapper.js b/js/components/ascribe_modal/modal_wrapper.js index f00eee9e..5c3ce742 100644 --- a/js/components/ascribe_modal/modal_wrapper.js +++ b/js/components/ascribe_modal/modal_wrapper.js @@ -65,7 +65,7 @@ let ModalWrapper = React.createClass({ {this.props.title} -
+
{this.renderChildren()}
diff --git a/js/components/header.js b/js/components/header.js index 042d2b1c..bcdabd31 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -201,7 +201,7 @@ let Header = React.createClass({ {this.getPoweredBy()} diff --git a/js/constants/information_text.js b/js/constants/information_text.js new file mode 100644 index 00000000..442e481e --- /dev/null +++ b/js/constants/information_text.js @@ -0,0 +1,33 @@ +'use strict'; + +export const InformationTexts = { + 'titles': { + 'acl_consign': 'CONSIGN', + 'acl_loan': 'LOAN', + 'acl_share': 'SHARE', + 'acl_delete': 'DELETE', + 'acl_create_editions': 'CREATE EDITIONS', + 'acl_unconsign': 'UNCONSIGN', + 'acl_request_unconsign': 'REQUEST UNCONSIGN' + }, + 'informationSentences': { + 'acl_consign': ' - Lets someone represent you in dealing with the work, under the terms you agree to.', + 'acl_loan': ' - Lets someone use or put the Work on display for a limited amount of time.', + 'acl_share': ' - Lets someone view the Work or Edition, but does not give rights to publish or display it.', + 'acl_delete': ' - Removes the Work from your Wallet. Note that the previous registration and transfer ' + + 'history will still exist on the blockchain and cannot be deleted.', + 'acl_create_editions': ' Lets the artist set a fixed number of editions of a work which can then be transferred, guaranteeing each edition is authentic and from the artist.', + 'acl_unconsign': 'Ends the consignment agreement between the owner and a consignee.', + 'acl_request_unconsign': 'Lets the owner ask the consignee to confirm that they will no longer manage the work.' + }, + 'exampleSentences': { + 'acl_consign': '(e.g. an artist Consigns 10 Editions of her new Work to a gallery ' + + 'so the gallery can sell them on her behalf, under the terms the artist and the gallery have agreed to)', + 'acl_loan': '(e.g. a collector Loans a Work to a gallery for one month for display in the gallery\'s show)', + 'acl_share': '(e.g. a photographer Shares proofs of a graduation photo with the graduate\'s grandparents)', + 'acl_delete': '(e.g. an artist uploaded the wrong file and doesn\'t want it cluttering his Wallet, so he Deletes it)', + 'acl_create_editions': '(e.g. A company commissions a visual artists to create three limited edition prints for a giveaway)', + 'acl_unconsign': '(e.g. An artist regains full control over their work and releases the consignee of any rights or responsibilities)', + 'acl_request_unconsign': '(e.g. An artist submits an unconsign request to a gallery after his exhibition ends, as per their agreement)' + } +}; \ No newline at end of file diff --git a/js/utils/acl_utils.js b/js/utils/acl_utils.js index f0075f55..fc3987c1 100644 --- a/js/utils/acl_utils.js +++ b/js/utils/acl_utils.js @@ -1,10 +1,6 @@ 'use strict'; -import { sanitize } from './general_utils'; - -function intersectAcls(a, b) { - return a.filter((val) => b.indexOf(val) > -1); -} +import { sanitize, intersectLists } from './general_utils'; export function getAvailableAcls(editions, filterFn) { let availableAcls = []; @@ -44,7 +40,7 @@ export function getAvailableAcls(editions, filterFn) { } if(editionsCopy.length >= 2) { for(let i = 1; i < editionsCopy.length; i++) { - availableAcls = intersectAcls(availableAcls, editionsCopy[i].acl); + availableAcls = intersectLists(availableAcls, editionsCopy[i].acl); } } diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index 390ee749..7c13f9b5 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -223,6 +223,16 @@ export function truncateTextAtCharIndex(text, charIndex, replacement = '...') { return truncatedText; } +/** + * @param index, int, the starting index of the substring to be replaced + * @param character, substring to be replaced + * @returns {string} + */ +export function replaceSubstringAtIndex(baseString, substrToReplace, stringToBePut) { + let index = baseString.indexOf(substrToReplace); + return baseString.substr(0, index) + stringToBePut + baseString.substr(index + substrToReplace.length); +} + /** * Extracts the user's subdomain from the browser's window. * If no subdomain is found (for example on a naked domain), the default "www" is just assumed. @@ -234,3 +244,12 @@ export function getSubdomain() { return tokens.length > 2 ? tokens[0] : 'www'; } +/** + * Takes two lists and returns their intersection as a list + * @param {Array} a + * @param {Array} b + * @return {[Array]} Intersected list of a and b + */ +export function intersectLists(a, b) { + return a.filter((val) => b.indexOf(val) > -1); +} diff --git a/sass/ascribe_acl_information.scss b/sass/ascribe_acl_information.scss new file mode 100644 index 00000000..9abec899 --- /dev/null +++ b/sass/ascribe_acl_information.scss @@ -0,0 +1,25 @@ +.acl-information-dropdown-list { + text-align: justify; + padding: .5em .5em .5em 0; + + p { + margin: 0 .5em 1em 0; + line-height: 1.2; + } + + span { + font-size: 11px; + } + + .title { + color: $ascribe-dark-blue; + } + + .info { + color: #212121; + } + + .example { + color: #616161; + } +} \ No newline at end of file diff --git a/sass/ascribe_edition.scss b/sass/ascribe_edition.scss index 9fa30387..195e79e0 100644 --- a/sass/ascribe_edition.scss +++ b/sass/ascribe_edition.scss @@ -17,8 +17,4 @@ border: 1px solid #CCC; display: table-cell; vertical-align: middle; -} - -.ascribe-button-list { - margin-top: 1em; -} +} \ No newline at end of file diff --git a/sass/lib/buttons.scss b/sass/lib/buttons.scss index e69de29b..68a124f9 100644 --- a/sass/lib/buttons.scss +++ b/sass/lib/buttons.scss @@ -0,0 +1,9 @@ +.btn-transparent { + color: black; + background-color: transparent; + + &:hover, &:active, &:focus { + color:#424242; + outline: none; + } +} \ No newline at end of file diff --git a/sass/lib/modals.scss b/sass/lib/modals.scss new file mode 100644 index 00000000..20a720c1 --- /dev/null +++ b/sass/lib/modals.scss @@ -0,0 +1,8 @@ +.modal-body { + padding-top:0; +} + +.modal-header { + padding: 15px 15px 0 15px; + border-bottom: none; +} \ No newline at end of file diff --git a/sass/main.scss b/sass/main.scss index 4b9e0f71..337dd32e 100644 --- a/sass/main.scss +++ b/sass/main.scss @@ -35,6 +35,9 @@ $BASE_URL: '<%= BASE_URL %>'; @import 'ascribe_form'; @import 'ascribe_panel'; @import 'ascribe_collapsible'; +@import 'ascribe_acl_information'; +@import 'lib/buttons'; +@import 'lib/modals'; @import 'ascribe_custom_style'; @import 'ascribe_spinner'; @@ -155,6 +158,7 @@ hr { } .ascribe-detail-property-label { + vertical-align: top; font-size: .8em; } diff --git a/sass/variables.scss b/sass/variables.scss index ccd48864..ac93a2fb 100644 --- a/sass/variables.scss +++ b/sass/variables.scss @@ -613,7 +613,7 @@ $modal-header-border-color: #e5e5e5 !default; $modal-footer-border-color: $modal-header-border-color !default; $modal-lg: 900px !default; -$modal-md: 600px !default; +$modal-md: 500px !default; $modal-sm: 300px !default;