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 00000000..860c534b Binary files /dev/null and b/fonts/ascribe-font.eot differ 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 00000000..a66d8f04 Binary files /dev/null and b/fonts/ascribe-font.ttf differ diff --git a/fonts/ascribe-font.woff b/fonts/ascribe-font.woff new file mode 100755 index 00000000..7d18d089 Binary files /dev/null and b/fonts/ascribe-font.woff differ diff --git a/fonts/ascribe-logo.eot b/fonts/ascribe-logo.eot deleted file mode 100644 index 039f93f5..00000000 Binary files a/fonts/ascribe-logo.eot and /dev/null differ diff --git a/fonts/ascribe-logo.ttf b/fonts/ascribe-logo.ttf deleted file mode 100644 index 61cdce03..00000000 Binary files a/fonts/ascribe-logo.ttf and /dev/null differ diff --git a/fonts/ascribe-logo.woff b/fonts/ascribe-logo.woff deleted file mode 100644 index c563df56..00000000 Binary files a/fonts/ascribe-logo.woff and /dev/null differ diff --git a/js/actions/contract_agreement_list_actions.js b/js/actions/contract_agreement_list_actions.js index 4993b129..1eedf5b0 100644 --- a/js/actions/contract_agreement_list_actions.js +++ b/js/actions/contract_agreement_list_actions.js @@ -22,8 +22,7 @@ class ContractAgreementListActions { if (contractAgreementList.count > 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;