diff --git a/.scss-lint.yml b/.scss-lint.yml new file mode 100644 index 00000000..61b1c624 --- /dev/null +++ b/.scss-lint.yml @@ -0,0 +1,224 @@ +linters: + BangFormat: + enabled: true + space_before_bang: true + space_after_bang: false + + BemDepth: + enabled: false + max_elements: 1 + + BorderZero: + enabled: true + convention: zero # or `none` + + ColorKeyword: + enabled: true + + ColorVariable: + enabled: true + + Comment: + enabled: true + + DebugStatement: + enabled: true + + DeclarationOrder: + enabled: true + + DisableLinterReason: + enabled: false + + DuplicateProperty: + enabled: true + + ElsePlacement: + enabled: true + style: same_line # or 'new_line' + + EmptyLineBetweenBlocks: + enabled: true + ignore_single_line_blocks: true + + EmptyRule: + enabled: true + + ExtendDirective: + enabled: false + + FinalNewline: + enabled: false + present: true + + HexLength: + enabled: true + style: short # or 'long' + + HexNotation: + enabled: true + style: lowercase # or 'uppercase' + + HexValidation: + enabled: true + + IdSelector: + enabled: true + + ImportantRule: + enabled: true + + ImportPath: + enabled: true + leading_underscore: false + filename_extension: false + + Indentation: + enabled: true + allow_non_nested_indentation: false + character: space # or 'tab' + width: 4 + + LeadingZero: + enabled: true + style: exclude_zero # or 'include_zero' + + MergeableSelector: + enabled: true + force_nesting: true + + NameFormat: + enabled: true + allow_leading_underscore: true + convention: hyphenated_lowercase # or 'camel_case', or 'snake_case', or a regex pattern + + NestingDepth: + enabled: true + max_depth: 3 + ignore_parent_selectors: false + + PlaceholderInExtend: + enabled: true + + PropertyCount: + enabled: false + include_nested: false + max_properties: 10 + + PropertySortOrder: + enabled: false + ignore_unspecified: false + min_properties: 2 + separate_groups: false + + PropertySpelling: + enabled: true + extra_properties: [] + + PropertyUnits: + enabled: true + global: [ + 'ch', 'em', 'ex', 'rem', # Font-relative lengths + 'cm', 'in', 'mm', 'pc', 'pt', 'px', 'q', # Absolute lengths + 'vh', 'vw', 'vmin', 'vmax', # Viewport-percentage lengths + 'deg', 'grad', 'rad', 'turn', # Angle + 'ms', 's', # Duration + 'Hz', 'kHz', # Frequency + 'dpi', 'dpcm', 'dppx', # Resolution + '%'] # Other + properties: {} + + QualifyingElement: + enabled: true + allow_element_with_attribute: false + allow_element_with_class: false + allow_element_with_id: false + + SelectorDepth: + enabled: true + max_depth: 3 + + SelectorFormat: + enabled: true + convention: hyphenated_lowercase # or 'strict_BEM', or 'hyphenated_BEM', or 'snake_case', or 'camel_case', or a regex pattern + + Shorthand: + enabled: true + allowed_shorthands: [1, 2, 3] + + SingleLinePerProperty: + enabled: true + allow_single_line_rule_sets: true + + SingleLinePerSelector: + enabled: true + + SpaceAfterComma: + enabled: true + + SpaceAfterPropertyColon: + enabled: true + style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned' + + SpaceAfterPropertyName: + enabled: true + + SpaceAfterVariableName: + enabled: true + + SpaceAroundOperator: + enabled: true + style: one_space # or 'no_space' + + SpaceBeforeBrace: + enabled: true + style: space # or 'new_line' + allow_single_line_padding: false + + SpaceBetweenParens: + enabled: true + spaces: 0 + + StringQuotes: + enabled: true + style: single_quotes # or double_quotes + + TrailingSemicolon: + enabled: true + + TrailingWhitespace: + enabled: true + + TrailingZero: + enabled: false + + TransitionAll: + enabled: false + + UnnecessaryMantissa: + enabled: true + + UnnecessaryParentReference: + enabled: true + + UrlFormat: + enabled: true + + UrlQuotes: + enabled: true + + VariableForProperty: + enabled: false + properties: [] + + VendorPrefix: + enabled: false + identifier_list: base + additional_identifiers: [] + excluded_identifiers: [] + + ZeroUnit: + enabled: true + + Compass::*: + enabled: false diff --git a/README.md b/README.md index 270c4a5f..a3258576 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ Additionally, to work on the white labeling functionality, you need to edit your ``` -Code Conventions -================ +JavaScript Code Conventions +=========================== For this project, we're using: * 4 Spaces @@ -42,6 +42,15 @@ For this project, we're using: * We don't use camel case for file naming but in everything Javascript related * We use `let` instead of `var`: [SA Post](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword) + +SCSS Code Conventions +===================== +Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor. + +Some interesting links: +* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom) + + Testing =============== We're using Facebook's jest to do testing as it integrates nicely with react.js as well. @@ -127,4 +136,4 @@ Moar stuff - [24ways.org: JavaScript Modules the ES6 Way](http://24ways.org/2014/javascript-modules-the-es6-way/) - [Babel: Learn ES6](https://babeljs.io/docs/learn-es6/) - [egghead's awesome reactjs and flux tutorials](https://egghead.io/) -- [Crockford's genious Javascript: The Good Parts (Tim has a copy)](http://www.amazon.de/JavaScript-Parts-Working-Shallow-Grain/dp/0596517742) \ No newline at end of file +- [Crockford's genious Javascript: The Good Parts (Tim has a copy)](http://www.amazon.de/JavaScript-Parts-Working-Shallow-Grain/dp/0596517742) diff --git a/js/actions/contract_agreement_list_actions.js b/js/actions/contract_agreement_list_actions.js new file mode 100644 index 00000000..589c1f51 --- /dev/null +++ b/js/actions/contract_agreement_list_actions.js @@ -0,0 +1,113 @@ +'use strict'; + +import alt from '../alt'; +import Q from 'q'; + +import OwnershipFetcher from '../fetchers/ownership_fetcher'; +import ContractListActions from './contract_list_actions'; + +class ContractAgreementListActions { + constructor() { + this.generateActions( + 'updateContractAgreementList', + 'flushContractAgreementList' + ); + } + + fetchContractAgreementList(issuer, accepted, pending) { + this.actions.updateContractAgreementList(null); + return Q.Promise((resolve, reject) => { + OwnershipFetcher.fetchContractAgreementList(issuer, accepted, pending) + .then((contractAgreementList) => { + if (contractAgreementList.count > 0) { + this.actions.updateContractAgreementList(contractAgreementList.results); + resolve(contractAgreementList.results); + } + else{ + resolve(null); + } + }) + .catch((err) => { + console.logGlobal(err); + reject(err); + }); + } + ); + } + + fetchAvailableContractAgreementList(issuer, createContractAgreement) { + 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) { + this.actions.updateContractAgreementList(acceptedContractAgreementList.results); + } else { + // otherwise, we're looking for contract agreements that are still pending + // + // Normally nesting promises, but for this conditional one, it makes sense to not + // overcomplicate the method + OwnershipFetcher.fetchContractAgreementList(issuer, null, true) + .then((pendingContractAgreementList) => { + if(pendingContractAgreementList.count > 0) { + this.actions.updateContractAgreementList(pendingContractAgreementList.results); + } else { + // 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); + } + } + }) + .catch((err) => { + console.logGlobal(err); + reject(err); + }); + } + }) + .catch((err) => { + console.logGlobal(err); + reject(err); + }); + } + ); + } + + createContractAgreementFromPublicContract(issuer) { + ContractListActions.fetchContractList(null, null, issuer) + .then((publicContract) => { + // create an agreement with the public contract if there is one + if (publicContract && publicContract.length > 0) { + return this.actions.createContractAgreement(null, publicContract[0]); + } + else { + /* + contractAgreementList in the store is already set to null; + */ + } + }).then((publicContracAgreement) => { + if (publicContracAgreement) { + this.actions.updateContractAgreementList([publicContracAgreement]); + } + }).catch((err) => { + console.logGlobal(err); + }); + } + + createContractAgreement(issuer, contract){ + return Q.Promise((resolve, reject) => { + OwnershipFetcher.createContractAgreement(issuer, contract).then( + (contractAgreement) => { + resolve(contractAgreement); + } + ).catch((err) => { + console.logGlobal(err); + reject(err); + }); + }); + } +} + +export default alt.createActions(ContractAgreementListActions); diff --git a/js/actions/contract_list_actions.js b/js/actions/contract_list_actions.js new file mode 100644 index 00000000..5b874caf --- /dev/null +++ b/js/actions/contract_list_actions.js @@ -0,0 +1,58 @@ +'use strict'; + +import alt from '../alt'; +import OwnershipFetcher from '../fetchers/ownership_fetcher'; +import Q from 'q'; + +class ContractListActions { + constructor() { + this.generateActions( + 'updateContractList', + 'flushContractList' + ); + } + + fetchContractList(isActive, isPublic, issuer) { + return Q.Promise((resolve, reject) => { + OwnershipFetcher.fetchContractList(isActive, isPublic, issuer) + .then((contracts) => { + this.actions.updateContractList(contracts.results); + resolve(contracts.results); + }) + .catch((err) => { + console.logGlobal(err); + this.actions.updateContractList([]); + reject(err); + }); + }); + } + + + changeContract(contract){ + return Q.Promise((resolve, reject) => { + OwnershipFetcher.changeContract(contract) + .then((res) => { + resolve(res); + }) + .catch((err)=> { + console.logGlobal(err); + reject(err); + }); + }); + } + + removeContract(contractId){ + return Q.Promise( (resolve, reject) => { + OwnershipFetcher.deleteContract(contractId) + .then((res) => { + resolve(res); + }) + .catch( (err) => { + console.logGlobal(err); + reject(err); + }); + }); + } +} + +export default alt.createActions(ContractListActions); diff --git a/js/actions/loan_contract_actions.js b/js/actions/loan_contract_actions.js deleted file mode 100644 index cc7e5a5b..00000000 --- a/js/actions/loan_contract_actions.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -import alt from '../alt'; -import OwnershipFetcher from '../fetchers/ownership_fetcher'; - - -class LoanContractActions { - constructor() { - this.generateActions( - 'updateLoanContract', - 'flushLoanContract' - ); - } - - fetchLoanContract(email) { - if(email.match(/.+\@.+\..+/)) { - OwnershipFetcher.fetchLoanContract(email) - .then((contracts) => { - if (contracts && contracts.length > 0) { - this.actions.updateLoanContract({ - contractKey: contracts[0].s3Key, - contractUrl: contracts[0].s3Url, - contractEmail: email - }); - } - else { - this.actions.updateLoanContract({ - contractKey: null, - contractUrl: null, - contractEmail: null - }); - } - }) - .catch((err) => { - console.logGlobal(err); - this.actions.updateLoanContract({ - contractKey: null, - contractUrl: null, - contractEmail: null - }); - }); - } else { - /* No email was entered - Ignore and keep going*/ - } - } -} - -export default alt.createActions(LoanContractActions); diff --git a/js/actions/loan_contract_list_actions.js b/js/actions/loan_contract_list_actions.js deleted file mode 100644 index bc5cef82..00000000 --- a/js/actions/loan_contract_list_actions.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -import alt from '../alt'; -import OwnershipFetcher from '../fetchers/ownership_fetcher'; - - -class LoanContractListActions { - constructor() { - this.generateActions( - 'updateLoanContractList', - 'flushLoanContractList' - ); - } - - fetchLoanContractList() { - OwnershipFetcher.fetchLoanContractList() - .then((contracts) => { - this.actions.updateLoanContractList(contracts); - }) - .catch((err) => { - console.logGlobal(err); - this.actions.updateLoanContractList([]); - }); - } -} - -export default alt.createActions(LoanContractListActions); diff --git a/js/actions/notification_actions.js b/js/actions/notification_actions.js new file mode 100644 index 00000000..9318c922 --- /dev/null +++ b/js/actions/notification_actions.js @@ -0,0 +1,68 @@ +'use strict'; + +import alt from '../alt'; +import Q from 'q'; + +import NotificationFetcher from '../fetchers/notification_fetcher'; + +class NotificationActions { + constructor() { + this.generateActions( + 'updatePieceListNotifications', + 'updateEditionListNotifications', + 'updateEditionNotifications', + 'updatePieceNotifications', + 'updateContractAgreementListNotifications' + ); + } + + fetchPieceListNotifications() { + NotificationFetcher + .fetchPieceListNotifications() + .then((res) => { + this.actions.updatePieceListNotifications(res); + }) + .catch((err) => console.logGlobal(err)); + } + + fetchPieceNotifications(pieceId) { + NotificationFetcher + .fetchPieceNotifications(pieceId) + .then((res) => { + this.actions.updatePieceNotifications(res); + }) + .catch((err) => console.logGlobal(err)); + } + + fetchEditionListNotifications() { + NotificationFetcher + .fetchEditionListNotifications() + .then((res) => { + this.actions.updateEditionListNotifications(res); + }) + .catch((err) => console.logGlobal(err)); + } + + fetchEditionNotifications(editionId) { + NotificationFetcher + .fetchEditionNotifications(editionId) + .then((res) => { + this.actions.updateEditionNotifications(res); + }) + .catch((err) => console.logGlobal(err)); + } + + fetchContractAgreementListNotifications() { + return Q.Promise((resolve, reject) => { + NotificationFetcher + .fetchContractAgreementListNotifications() + .then((res) => { + this.actions.updateContractAgreementListNotifications(res); + resolve(res); + }) + .catch((err) => console.logGlobal(err)); + }); + } +} + +export default alt.createActions(NotificationActions); diff --git a/js/actions/piece_list_actions.js b/js/actions/piece_list_actions.js index 2fd15c04..ae5ac090 100644 --- a/js/actions/piece_list_actions.js +++ b/js/actions/piece_list_actions.js @@ -57,7 +57,7 @@ class PieceListActions { PieceListFetcher .fetchRequestActions() .then((res) => { - this.actions.updatePieceListRequestActions(res.piece_ids); + this.actions.updatePieceListRequestActions(res); }) .catch((err) => console.logGlobal(err)); } diff --git a/js/app.js b/js/app.js index addd0494..30a57d2b 100644 --- a/js/app.js +++ b/js/app.js @@ -26,6 +26,7 @@ import EventActions from './actions/event_actions'; import GoogleAnalyticsHandler from './third_party/ga'; import RavenHandler from './third_party/raven'; import IntercomHandler from './third_party/intercom'; +import NotificationsHandler from './third_party/notifications'; /* eslint-enable */ initLogging(); @@ -71,8 +72,10 @@ class AppGateway { subdomain = settings.subdomain; } + window.document.body.classList.add('client--' + subdomain); + EventActions.applicationWillBoot(settings); - Router.run(getRoutes(type, subdomain), Router.HistoryLocation, (App) => { + window.appRouter = Router.run(getRoutes(type, subdomain), Router.HistoryLocation, (App) => { React.render( , document.getElementById('main') diff --git a/js/components/acl_proxy.js b/js/components/acl_proxy.js index be0d8466..4fc90a9b 100644 --- a/js/components/acl_proxy.js +++ b/js/components/acl_proxy.js @@ -20,21 +20,28 @@ let AclProxy = React.createClass({ show: React.PropTypes.bool }, - render() { - if(this.props.show) { + getChildren() { + if (React.Children.count(this.props.children) > 1){ + /* + This might ruin styles for header items in the navbar etc + */ return ( {this.props.children} ); + } + /* can only do this when there is only 1 child, but will preserve styles */ + return this.props.children; + }, + + render() { + if(this.props.show) { + return this.getChildren(); } else { if(this.props.aclObject) { if(this.props.aclObject[this.props.aclName]) { - return ( - - {this.props.children} - - ); + return this.getChildren(); } else { /* if(typeof this.props.aclObject[this.props.aclName] === 'undefined') { console.warn('The aclName you\'re filtering for was not present (or undefined) in the aclObject.'); diff --git a/js/components/ascribe_accordion_list/accordion_list.js b/js/components/ascribe_accordion_list/accordion_list.js index 471ba9d5..fe300702 100644 --- a/js/components/ascribe_accordion_list/accordion_list.js +++ b/js/components/ascribe_accordion_list/accordion_list.js @@ -21,7 +21,7 @@ let AccordionList = React.createClass({ ); } else if(this.props.count === 0) { return ( -
+

{getLangText('We could not find any works related to you...')}

{getLangText('To register one, click')} {getLangText('here')}!

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 e4ba0d2b..709160b9 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 @@ -6,7 +6,6 @@ import classNames from 'classnames'; import EditionListActions from '../../actions/edition_list_actions'; import EditionListStore from '../../stores/edition_list_store'; -import PieceListActions from '../../actions/piece_list_actions'; import PieceListStore from '../../stores/piece_list_store'; import Button from 'react-bootstrap/lib/Button'; @@ -16,6 +15,7 @@ import CreateEditionsButton from '../ascribe_buttons/create_editions_button'; import { mergeOptions } from '../../utils/general_utils'; import { getLangText } from '../../utils/lang_utils'; + let AccordionListItemEditionWidget = React.createClass({ propTypes: { className: React.PropTypes.string, diff --git a/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js b/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js index d1ab2112..350d61a8 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js @@ -160,7 +160,7 @@ let AccordionListItemTableEditions = React.createClass({ let content = item.acl; return { 'content': content, - 'requestAction': item.request_action + 'notifications': item.notifications }; }, 'acl', getLangText('Actions'), 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 f7bca334..dde5c43d 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -61,12 +61,12 @@ let AccordionListItemWallet = React.createClass({ }, getGlyphicon(){ - if (this.props.content.requestAction && this.props.content.requestAction.length > 0) { + if ((this.props.content.notifications && this.props.content.notifications.length > 0)){ return ( {getLangText('You have actions pending in one of your editions')}}> + overlay={{getLangText('You have actions pending')}}> ); } diff --git a/js/components/ascribe_buttons/acl_button.js b/js/components/ascribe_buttons/acl_button.js index 084de194..e3c7fa1c 100644 --- a/js/components/ascribe_buttons/acl_button.js +++ b/js/components/ascribe_buttons/acl_button.js @@ -162,21 +162,24 @@ let AclButton = React.createClass({ }, render() { - let shouldDisplay = this.props.availableAcls[this.props.action]; - let aclProps = this.actionProperties(); - let buttonClassName = this.props.buttonAcceptClassName ? this.props.buttonAcceptClassName : ''; - return ( - - {this.sanitizeAction()} - - } - handleSuccess={aclProps.handleSuccess} - title={aclProps.title}> - {aclProps.form} - - ); + if (this.props.availableAcls){ + let shouldDisplay = this.props.availableAcls[this.props.action]; + let aclProps = this.actionProperties(); + let buttonClassName = this.props.buttonAcceptClassName ? this.props.buttonAcceptClassName : ''; + return ( + + {this.sanitizeAction()} + + } + handleSuccess={aclProps.handleSuccess} + title={aclProps.title}> + {aclProps.form} + + ); + } + return null; } }); diff --git a/js/components/ascribe_collapsible/collapsible_paragraph.js b/js/components/ascribe_collapsible/collapsible_paragraph.js index 8b3b3cf4..e146b42b 100644 --- a/js/components/ascribe_collapsible/collapsible_paragraph.js +++ b/js/components/ascribe_collapsible/collapsible_paragraph.js @@ -17,7 +17,7 @@ const CollapsibleParagraph = React.createClass({ getDefaultProps() { return { - show: false + show: true }; }, @@ -38,14 +38,14 @@ const CollapsibleParagraph = React.createClass({ if(this.props.show) { return (
-
+
{text} {this.props.title}
+ className="ascribe-collapsible-content"> {this.props.children}
diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 696e5057..97f9fb3d 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -25,7 +25,7 @@ import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph import Form from './../ascribe_forms/form'; import Property from './../ascribe_forms/property'; import EditionDetailProperty from './detail_property'; - +import LicenseDetail from './license_detail'; import EditionFurtherDetails from './further_details'; import ListRequestActions from './../ascribe_forms/list_form_request_actions'; @@ -88,10 +88,8 @@ let Edition = React.createClass({ }, handleDeleteSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + this.refreshCollection(); - EditionListActions.refreshEditionList({pieceId: this.props.edition.parent}); EditionListActions.closeAllEditionLists(); EditionListActions.clearAllEditionSelections(); @@ -101,6 +99,12 @@ let Edition = React.createClass({ this.transitionTo('pieces'); }, + refreshCollection() { + PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, + this.state.orderBy, this.state.orderAsc, this.state.filterBy); + EditionListActions.refreshEditionList({pieceId: this.props.edition.parent}); + }, + render() { return ( @@ -118,6 +122,7 @@ let Edition = React.createClass({
@@ -152,8 +157,9 @@ let Edition = React.createClass({ + show={!!(this.state.currentUser.username + || this.props.edition.acl.acl_edit + || this.props.edition.public_note)}> {return {'bitcoin_id': this.props.edition.bitcoin_id}; }} label={getLangText('Personal note (private)')} @@ -205,14 +211,22 @@ let EditionSummary = React.createClass({ edition: React.PropTypes.object, handleSuccess: React.PropTypes.func, currentUser: React.PropTypes.object, - handleDeleteSuccess: React.PropTypes.func + handleDeleteSuccess: React.PropTypes.func, + refreshCollection: React.PropTypes.func }, getTransferWithdrawData(){ return {'bitcoin_id': this.props.edition.bitcoin_id}; }, + + handleSuccess() { + this.props.refreshCollection(); + this.props.handleSuccess(); + }, + showNotification(response){ this.props.handleSuccess(); + if (response){ let notification = new GlobalNotificationModel(response.notification, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); @@ -234,13 +248,15 @@ let EditionSummary = React.createClass({ getActions(){ let actions = null; - if (this.props.edition.request_action && this.props.edition.request_action.length > 0){ + if (this.props.edition && + this.props.edition.notifications && + this.props.edition.notifications.length > 0){ actions = ( ); + notifications={this.props.edition.notifications}/>); } else { @@ -275,7 +291,7 @@ let EditionSummary = React.createClass({ className="text-center ascribe-button-list" availableAcls={this.props.edition.acl} editions={[this.props.edition]} - handleSuccess={this.props.handleSuccess}> + handleSuccess={this.handleSuccess}> {withdrawButton} + {this.getStatus()} {this.getActions()}
); - } }); diff --git a/js/components/ascribe_detail/edition_container.js b/js/components/ascribe_detail/edition_container.js index 2194123d..0f726ae5 100644 --- a/js/components/ascribe_detail/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -9,6 +9,8 @@ import Edition from './edition'; import AppConstants from '../../constants/application_constants'; + + /** * This is the component that implements resource/data specific functionality */ @@ -34,6 +36,15 @@ let EditionContainer = React.createClass({ EditionActions.fetchOne(this.props.params.editionId); }, + // This is done to update the container when the user clicks on the prev or next + // button to update the URL parameter (and therefore to switch pieces) + componentWillReceiveProps(nextProps) { + if(this.props.params.editionId !== nextProps.params.editionId) { + EditionActions.updateEdition({}); + EditionActions.fetchOne(nextProps.params.editionId); + } + }, + componentWillUnmount() { // Every time we're leaving the edition detail page, // just reset the edition that is saved in the edition store diff --git a/js/components/ascribe_detail/further_details.js b/js/components/ascribe_detail/further_details.js index 9fc5bf15..db241f3d 100644 --- a/js/components/ascribe_detail/further_details.js +++ b/js/components/ascribe_detail/further_details.js @@ -15,7 +15,7 @@ import GlobalNotificationActions from '../../actions/global_notification_actions import FurtherDetailsFileuploader from './further_details_fileuploader'; -import { isReadyForFormSubmission } from '../ascribe_uploader/react_s3_fine_uploader_utils'; +import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; let FurtherDetails = React.createClass({ propTypes: { @@ -38,9 +38,9 @@ let FurtherDetails = React.createClass({ GlobalNotificationActions.appendGlobalNotification(notification); }, - submitKey(key){ + submitFile(file){ this.setState({ - otherDataKey: key + otherDataKey: file.key }); }, @@ -78,9 +78,9 @@ let FurtherDetails = React.createClass({ extraData={this.props.extraData} />
+ { this.props.license.code.toUpperCase() + ': ' + this.props.license.name} + + } + /> + ); + } +}); + +export default LicenseDetail; diff --git a/js/components/ascribe_detail/note.js b/js/components/ascribe_detail/note.js index 0f32e5da..c739b937 100644 --- a/js/components/ascribe_detail/note.js +++ b/js/components/ascribe_detail/note.js @@ -39,19 +39,18 @@ let Note = React.createClass({ }, render() { - if (!!this.props.currentUser.username && this.props.show) { + if ((!!this.props.currentUser.username && this.props.editable || !this.props.editable ) && this.props.show) { return ( + handleSuccess={this.showNotification} + disabled={!this.props.editable}> + label={this.props.label}> @@ -63,4 +62,4 @@ let Note = React.createClass({ } }); -export default Note \ No newline at end of file +export default Note; \ No newline at end of file diff --git a/js/components/ascribe_detail/piece_container.js b/js/components/ascribe_detail/piece_container.js index 5726fd16..c2eb1759 100644 --- a/js/components/ascribe_detail/piece_container.js +++ b/js/components/ascribe_detail/piece_container.js @@ -19,6 +19,7 @@ import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph import FurtherDetails from './further_details'; import DetailProperty from './detail_property'; +import LicenseDetail from './license_detail'; import HistoryIterator from './history_iterator'; import AclButtonList from './../ascribe_buttons/acl_button_list'; @@ -172,17 +173,16 @@ let PieceContainer = React.createClass({ return {'id': this.state.piece.id}; }, - getActions(){ + getActions() { if (this.state.piece && - this.state.piece.request_action && - this.state.piece.request_action.length > 0) { + this.state.piece.notifications && + this.state.piece.notifications.length > 0) { return ( - ); + notifications={this.state.piece.notifications}/>); } else { return ( @@ -225,6 +225,7 @@ let PieceContainer = React.createClass({
+
} buttons={this.getActions()}> @@ -238,12 +239,11 @@ let PieceContainer = React.createClass({ + show={!!(this.state.currentUser.username || this.state.piece.public_note)}> 0) { - return ( - - {getLangText('Learn more')} - - }> - - ); - } - return null; - }, - - render() { - return ( - - {getLangText('Send loan request')} - } - spinner={ - - - - }> -
-

{getLangText('Contract form')}

-
- - - - - - - {this.getContracts()} - - {getLangText('Appendix')} - - - - ); - } -}); - -export default ContractForm; \ No newline at end of file diff --git a/js/components/ascribe_forms/create_editions_form.js b/js/components/ascribe_forms/create_editions_form.js index a078feeb..cd5a22d3 100644 --- a/js/components/ascribe_forms/create_editions_form.js +++ b/js/components/ascribe_forms/create_editions_form.js @@ -40,6 +40,12 @@ let CreateEditionsForm = React.createClass({ url={ApiUrls.editions} getFormData={this.getFormData} handleSuccess={this.handleSuccess} + buttons={ + } spinner={ } + spinner={ + + + + }> +
+

{getLangText('Contract form')}

+
+ + + + {this.getContracts()} + + {getLangText('Appendix')} + {/* We're using disabled on a form here as PropertyCollapsible currently + does not support the disabled + overrideForm functionality */} + + + + ); + } + return ( +
+

+ {getLangText('No contracts uploaded yet, please go to the ')} + {getLangText('settings page')} + {getLangText(' and create them.')} +

+
+ ); + } +}); + +export default ContractAgreementForm; \ No newline at end of file diff --git a/js/components/ascribe_forms/form_copyright_association.js b/js/components/ascribe_forms/form_copyright_association.js new file mode 100644 index 00000000..da14e76d --- /dev/null +++ b/js/components/ascribe_forms/form_copyright_association.js @@ -0,0 +1,80 @@ +'use strict'; + +import React from 'react'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import Form from './form'; +import Property from './property'; + +import ApiUrls from '../../constants/api_urls'; +import AppConstants from '../../constants/application_constants'; + +import { getLangText } from '../../utils/lang_utils'; + +let CopyrightAssociationForm = React.createClass({ + propTypes: { + currentUser: React.PropTypes.object + }, + + handleSubmitSuccess(){ + let notification = getLangText('Copyright association updated'); + notification = new GlobalNotificationModel(notification, 'success', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + getProfileFormData(){ + return {email: this.props.currentUser.email}; + }, + + render() { + let selectedState; + let selectDefaultValue = ' -- ' + getLangText('select an association') + ' -- '; + + if (this.props.currentUser && this.props.currentUser.profile + && this.props.currentUser.profile.copyright_association) { + selectedState = AppConstants.copyrightAssociations.indexOf(this.props.currentUser.profile.copyright_association); + selectedState = selectedState !== -1 ? AppConstants.copyrightAssociations[selectedState] : selectDefaultValue; + } + + if (this.props.currentUser && this.props.currentUser.email){ + return ( +
+ + + +
+
+ ); + } + return null; + } +}); + +export default CopyrightAssociationForm; \ No newline at end of file diff --git a/js/components/ascribe_forms/form_create_contract.js b/js/components/ascribe_forms/form_create_contract.js new file mode 100644 index 00000000..b19cb050 --- /dev/null +++ b/js/components/ascribe_forms/form_create_contract.js @@ -0,0 +1,111 @@ +'use strict'; + +import React from 'react'; + +import Form from '../ascribe_forms/form'; +import Property from '../ascribe_forms/property'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import ContractListActions from '../../actions/contract_list_actions'; + +import AppConstants from '../../constants/application_constants'; +import ApiUrls from '../../constants/api_urls'; + +import InputFineUploader from './input_fineuploader'; + +import { getLangText } from '../../utils/lang_utils'; +import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; + + +let CreateContractForm = React.createClass({ + propTypes: { + isPublic: React.PropTypes.bool, + + // A class of a file the user has to upload + // Needs to be defined both in singular as well as in plural + fileClassToUpload: React.PropTypes.shape({ + singular: React.PropTypes.string, + plural: React.PropTypes.string + }) + }, + + getInitialState() { + return { + isUploadReady: false, + contractName: '' + }; + }, + + setIsUploadReady(isReady) { + this.setState({ + isUploadReady: isReady + }); + }, + + handleCreateSuccess(response) { + ContractListActions.fetchContractList(true); + let notification = new GlobalNotificationModel(getLangText('Contract %s successfully created', response.name), 'success', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + this.refs.form.reset(); + }, + + submitFileName(fileName) { + this.setState({ + contractName: fileName + }); + + this.refs.form.submit(); + }, + + render() { + return ( +
+ + + + + +
+ ); + } +}); + +export default CreateContractForm; \ No newline at end of file diff --git a/js/components/ascribe_forms/form_loan.js b/js/components/ascribe_forms/form_loan.js index 60dad486..ef2fbd13 100644 --- a/js/components/ascribe_forms/form_loan.js +++ b/js/components/ascribe_forms/form_loan.js @@ -12,11 +12,12 @@ import InputTextAreaToggable from './input_textarea_toggable'; import InputDate from './input_date'; import InputCheckbox from './input_checkbox'; -import LoanContractStore from '../../stores/loan_contract_store'; -import LoanContractActions from '../../actions/loan_contract_actions'; +import ContractAgreementListStore from '../../stores/contract_agreement_list_store'; +import ContractAgreementListActions from '../../actions/contract_agreement_list_actions'; import AppConstants from '../../constants/application_constants'; +import { mergeOptions } from '../../utils/general_utils'; import { getLangText } from '../../utils/lang_utils'; @@ -34,6 +35,7 @@ let LoanForm = React.createClass({ url: React.PropTypes.string, id: React.PropTypes.object, message: React.PropTypes.string, + createPublicContractAgreement: React.PropTypes.bool, handleSuccess: React.PropTypes.func }, @@ -43,62 +45,117 @@ let LoanForm = React.createClass({ showPersonalMessage: true, showEndDate: true, showStartDate: true, - showPassword: true + showPassword: true, + createPublicContractAgreement: true }; }, getInitialState() { - return LoanContractStore.getState(); + return ContractAgreementListStore.getState(); }, componentDidMount() { - LoanContractStore.listen(this.onChange); - LoanContractActions.flushLoanContract.defer(); + 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() { - LoanContractStore.unlisten(this.onChange); + ContractAgreementListStore.unlisten(this.onChange); }, onChange(state) { this.setState(state); }, + getContractAgreementsOrCreatePublic(email){ + ContractAgreementListActions.flushContractAgreementList.defer(); + if (email) { + // fetch the available contractagreements (pending/accepted) + ContractAgreementListActions.fetchAvailableContractAgreementList(email, true); + } + }, + getFormData(){ - return this.props.id; + return mergeOptions( + this.props.id, + this.getContractAgreementId() + ); }, handleOnChange(event) { // event.target.value is the submitted email of the loanee - if(event && event.target && event.target.value && event.target.value.match(/.*@.*/)) { - LoanContractActions.fetchLoanContract(event.target.value); + if(event && event.target && event.target.value && event.target.value.match(/.*@.*\..*/)) { + this.getContractAgreementsOrCreatePublic(event.target.value); } else { - LoanContractActions.flushLoanContract(); + ContractAgreementListActions.flushContractAgreementList(); } }, + getContractAgreementId() { + if (this.state.contractAgreementList && this.state.contractAgreementList.length > 0) { + return {'contract_agreement_id': this.state.contractAgreementList[0].id}; + } + return {}; + }, + getContractCheckbox() { - if(this.state.contractKey && this.state.contractUrl) { + 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) - return ( - - - - {getLangText('I agree to the')}  - - {getLangText('terms of')} {this.state.contractEmail} - - - - - ); + let contractAgreement = this.state.contractAgreementList[0]; + let contract = contractAgreement.contract; + + if(contractAgreement.datetime_accepted) { + return ( + + ); + } else { + return ( + + + + {getLangText('I agree to the')}  + + {getLangText('terms of ')} {contract.issuer} + + + + + ); + } } else { return ( 0) { + let appendix = this.state.contractAgreementList[0].appendix; + if (appendix && appendix.default) { + return ( + +
{appendix.default}
+
+ ); + } + } + return null; + }, + getButtons() { if(this.props.loanHeading) { return ( @@ -157,8 +230,8 @@ let LoanForm = React.createClass({ + {this.getContractCheckbox()} + {this.getAppendix()} - {this.getContractCheckbox()} {this.props.children} ); diff --git a/js/components/ascribe_forms/form_loan_request_answer.js b/js/components/ascribe_forms/form_loan_request_answer.js index 2ebdb439..1bfe90db 100644 --- a/js/components/ascribe_forms/form_loan_request_answer.js +++ b/js/components/ascribe_forms/form_loan_request_answer.js @@ -18,7 +18,7 @@ let LoanRequestAnswerForm = React.createClass({ url: React.PropTypes.string, id: React.PropTypes.object, message: React.PropTypes.string, - handleSuccess: React.PropTypes.func.required + handleSuccess: React.PropTypes.func.isRequired }, getDefaultProps() { diff --git a/js/components/ascribe_forms/form_login.js b/js/components/ascribe_forms/form_login.js index 86a20119..79bced6c 100644 --- a/js/components/ascribe_forms/form_login.js +++ b/js/components/ascribe_forms/form_login.js @@ -27,7 +27,7 @@ let LoginForm = React.createClass({ onLogin: React.PropTypes.func }, - mixins: [Router.Navigation], + mixins: [Router.Navigation, Router.State], getDefaultProps() { return { @@ -95,6 +95,7 @@ let LoginForm = React.createClass({ }, render() { + let email = this.getQuery().email || null; return (
+ getFormData={this.getFormData} + disabled={!this.props.editable}> + label={this.props.title}> diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index 541840c0..118b3968 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -10,10 +10,11 @@ import Property from './property'; import InputFineUploader from './input_fineuploader'; import ApiUrls from '../../constants/api_urls'; +import AppConstants from '../../constants/application_constants'; import { getLangText } from '../../utils/lang_utils'; import { mergeOptions } from '../../utils/general_utils'; -import { isReadyForFormSubmission } from '../ascribe_uploader/react_s3_fine_uploader_utils'; +import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; let RegisterPieceForm = React.createClass({ @@ -99,11 +100,19 @@ let RegisterPieceForm = React.createClass({ name="digital_work_key" ignoreFocus={true}> { let message = getLangText('You have successfully') + ' ' + option + ' the ' + action + ' request ' + getLangText('from') + ' ' + owner; - let notification = new GlobalNotificationModel(message, 'success'); - GlobalNotificationActions.appendGlobalNotification(notification); + let notifications = new GlobalNotificationModel(message, 'success'); + GlobalNotificationActions.appendGlobalNotification(notifications); + + this.handleSuccess(); - if(this.props.handleSuccess) { - this.props.handleSuccess(); - } }; }, - getContent() { - let pieceOrEditionStr = this.isPiece() ? getLangText('this work%s', '.') : getLangText('this edition%s', '.'); - let message = this.props.requestUser + ' ' + getLangText('requests you') + ' ' + this.props.requestAction + ' ' + pieceOrEditionStr; - if (this.props.requestAction === 'loan_request'){ - message = this.props.requestUser + ' ' + getLangText('requests you to loan') + ' ' + pieceOrEditionStr; + handleSuccess() { + if (this.isPiece()){ + NotificationActions.fetchPieceListNotifications(); } + else { + NotificationActions.fetchEditionListNotifications(); + } + if(this.props.handleSuccess) { + this.props.handleSuccess(); + } + }, + + getContent() { return ( - {message} + {this.props.notifications.action_str + ' by ' + this.props.notifications.by} ); }, getAcceptButtonForm(urls) { - if(this.props.requestAction === 'unconsign') { + if(this.props.notifications.action === 'unconsign') { return ( + handleSuccess={this.handleSuccess} /> ); - } else if(this.props.requestAction === 'loan_request') { + } else if(this.props.notifications.action === 'loan_request') { return ( + handleSuccess={this.handleSuccess} /> ); } else { return ( @@ -118,7 +125,7 @@ let RequestActionForm = React.createClass({ url={urls.accept} getFormData={this.getFormData} handleSuccess={ - this.showNotification(getLangText('accepted'), this.props.requestAction, this.props.requestUser) + this.showNotification(getLangText('accepted'), this.props.notifications.action, this.props.notifications.by) } isInline={true} className='inline pull-right'> @@ -143,7 +150,7 @@ let RequestActionForm = React.createClass({ isInline={true} getFormData={this.getFormData} handleSuccess={ - this.showNotification(getLangText('denied'), this.props.requestAction, this.props.requestUser) + this.showNotification(getLangText('denied'), this.props.notifications.action, this.props.notifications.by) } className='inline pull-right'> +
+ + }/> + ); + }, this); + } + return content; + }, + + render() { + return ( + +
+ + + +
+
+
+                    Usage: curl <url> -H 'Authorization: Bearer <token>'
+                
+ {this.getApplications()} +
+ ); + } +}); + +export default APISettings; \ No newline at end of file diff --git a/js/components/ascribe_settings/bitcoin_wallet_settings.js b/js/components/ascribe_settings/bitcoin_wallet_settings.js new file mode 100644 index 00000000..ea528709 --- /dev/null +++ b/js/components/ascribe_settings/bitcoin_wallet_settings.js @@ -0,0 +1,71 @@ +'use strict'; + +import React from 'react'; + +import WalletSettingsStore from '../../stores/wallet_settings_store'; +import WalletSettingsActions from '../../actions/wallet_settings_actions'; + +import Form from '../ascribe_forms/form'; +import Property from '../ascribe_forms/property'; + +import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph'; + +import AppConstants from '../../constants/application_constants'; + +import { getLangText } from '../../utils/lang_utils'; + + +let BitcoinWalletSettings = React.createClass({ + propTypes: { + defaultExpanded: React.PropTypes.bool + }, + + getInitialState() { + return WalletSettingsStore.getState(); + }, + + componentDidMount() { + WalletSettingsStore.listen(this.onChange); + WalletSettingsActions.fetchWalletSettings(); + }, + + componentWillUnmount() { + WalletSettingsStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + let content = ; + + if (this.state.walletSettings.btc_public_key) { + content = ( +
+ +
{this.state.walletSettings.btc_public_key}
+
+ +
{this.state.walletSettings.btc_root_address}
+
+
+
); + } + return ( + + {content} + + ); + } +}); + +export default BitcoinWalletSettings; \ No newline at end of file diff --git a/js/components/ascribe_settings/contract_settings.js b/js/components/ascribe_settings/contract_settings.js new file mode 100644 index 00000000..7196032b --- /dev/null +++ b/js/components/ascribe_settings/contract_settings.js @@ -0,0 +1,186 @@ +'use strict'; + +import React from 'react'; + +import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph'; +import CreateContractForm from '../ascribe_forms/form_create_contract'; + +import ContractListStore from '../../stores/contract_list_store'; +import ContractListActions from '../../actions/contract_list_actions'; + +import UserStore from '../../stores/user_store'; +import UserActions from '../../actions/user_actions'; + +import WhitelabelStore from '../../stores/whitelabel_store'; +import WhitelabelActions from '../../actions/whitelabel_actions'; + +import ActionPanel from '../ascribe_panel/action_panel'; +import ContractSettingsUpdateButton from './contract_settings_update_button'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import AclProxy from '../acl_proxy'; + +import { getLangText } from '../../utils/lang_utils'; +import { mergeOptions, truncateTextAtCharIndex } from '../../utils/general_utils'; + + +let ContractSettings = React.createClass({ + getInitialState(){ + return mergeOptions( + ContractListStore.getState(), + UserStore.getState() + ); + }, + + componentDidMount() { + ContractListStore.listen(this.onChange); + UserStore.listen(this.onChange); + WhitelabelStore.listen(this.onChange); + + WhitelabelActions.fetchWhitelabel(); + UserActions.fetchCurrentUser(); + ContractListActions.fetchContractList(true); + }, + + componentWillUnmount() { + WhitelabelStore.unlisten(this.onChange); + UserStore.unlisten(this.onChange); + ContractListStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + removeContract(contract) { + return () => { + ContractListActions.removeContract(contract.id) + .then((response) => { + ContractListActions.fetchContractList(true); + let notification = new GlobalNotificationModel(response.notification, 'success', 4000); + GlobalNotificationActions.appendGlobalNotification(notification); + }) + .catch((err) => { + let notification = new GlobalNotificationModel(err, 'danger', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + }); + }; + }, + + getPublicContracts(){ + return this.state.contractList.filter((contract) => contract.is_public); + }, + + getPrivateContracts(){ + return this.state.contractList.filter((contract) => !contract.is_public); + }, + + render() { + let publicContracts = this.getPublicContracts(); + let privateContracts = this.getPrivateContracts(); + let createPublicContractForm = null; + + if(publicContracts.length === 0) { + createPublicContractForm = ( + + ); + } + + return ( +
+ + +
+ {createPublicContractForm} + {publicContracts.map((contract, i) => { + return ( + + + + + + {getLangText('PREVIEW')} + + +
+ } + leftColumnWidth="40%" + rightColumnWidth="60%"/> + ); + })} +
+ + +
+ + {privateContracts.map((contract, i) => { + return ( + + + + + + {getLangText('PREVIEW')} + + +
+ } + leftColumnWidth="60%" + rightColumnWidth="40%"/> + ); + })} + +
+ + + ); + } +}); + +export default ContractSettings; \ No newline at end of file diff --git a/js/components/ascribe_settings/contract_settings_update_button.js b/js/components/ascribe_settings/contract_settings_update_button.js new file mode 100644 index 00000000..f2e54c50 --- /dev/null +++ b/js/components/ascribe_settings/contract_settings_update_button.js @@ -0,0 +1,98 @@ +'use strict'; + +import React from 'react'; + +import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader'; +import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button'; + +import AppConstants from '../../constants/application_constants'; +import ApiUrls from '../../constants/api_urls'; + +import ContractListActions from '../../actions/contract_list_actions'; + +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 { getCookie } from '../../utils/fetch_api_utils'; +import { getLangText } from '../../utils/lang_utils'; + + +let ContractSettingsUpdateButton = React.createClass({ + propTypes: { + contract: React.PropTypes.object + }, + + submitFile(file) { + let contract = this.props.contract; + + // override the blob with the key's value + contract.blob = file.key; + + // send it to the server + ContractListActions + .changeContract(contract) + .then((res) => { + + // Display feedback to the user + let notification = new GlobalNotificationModel(getLangText('Contract %s successfully updated', res.name), 'success', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + + // and refresh the contract list to get the updated contracs + return ContractListActions.fetchContractList(true); + }) + .then(() => { + // Also, reset the fineuploader component so that the user can again 'update' his contract + this.refs.fineuploader.reset(); + }) + .catch((err) => { + console.logGlobal(err); + let notification = new GlobalNotificationModel(getLangText('Contract could not be updated'), 'success', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + }); + }, + + render() { + return ( + {/* So that ReactS3FineUploader is not complaining */}} + signature={{ + endpoint: AppConstants.serverUrl + 's3/signature/', + customHeaders: { + 'X-CSRFToken': getCookie(AppConstants.csrftoken) + } + }} + deleteFile={{ + enabled: true, + method: 'DELETE', + endpoint: AppConstants.serverUrl + 's3/delete', + customHeaders: { + 'X-CSRFToken': getCookie(AppConstants.csrftoken) + } + }} + fileClassToUpload={{ + singular: getLangText('UPDATE'), + plural: getLangText('UPDATE') + }} + isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} + submitFile={this.submitFile} + /> + ); + } +}); + +export default ContractSettingsUpdateButton; \ No newline at end of file diff --git a/js/components/ascribe_settings/settings_container.js b/js/components/ascribe_settings/settings_container.js new file mode 100644 index 00000000..2b9ae2a1 --- /dev/null +++ b/js/components/ascribe_settings/settings_container.js @@ -0,0 +1,84 @@ +'use strict'; + +import React from 'react'; +import Router from 'react-router'; + +import UserStore from '../../stores/user_store'; +import UserActions from '../../actions/user_actions'; + +import WhitelabelStore from '../../stores/whitelabel_store'; +import WhitelabelActions from '../../actions/whitelabel_actions'; + +import AccountSettings from './account_settings'; +import BitcoinWalletSettings from './bitcoin_wallet_settings'; +import APISettings from './api_settings'; + +import AclProxy from '../acl_proxy'; + +import { mergeOptions } from '../../utils/general_utils'; + + +let SettingsContainer = React.createClass({ + propTypes: { + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element]) + }, + + mixins: [Router.Navigation], + + getInitialState() { + return mergeOptions( + UserStore.getState(), + WhitelabelStore.getState() + ); + }, + + componentDidMount() { + UserStore.listen(this.onChange); + WhitelabelStore.listen(this.onChange); + + WhitelabelActions.fetchWhitelabel(); + UserActions.fetchCurrentUser(); + }, + + componentWillUnmount() { + WhitelabelStore.unlisten(this.onChange); + UserStore.unlisten(this.onChange); + }, + + loadUser(){ + UserActions.fetchCurrentUser(); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + if (this.state.currentUser && this.state.currentUser.username) { + return ( +
+ + {this.props.children} + + + + + + +
+ ); + } + return null; + } +}); + +export default SettingsContainer; diff --git a/js/components/ascribe_slides_container/slides_container.js b/js/components/ascribe_slides_container/slides_container.js index 84dff61c..53092a38 100644 --- a/js/components/ascribe_slides_container/slides_container.js +++ b/js/components/ascribe_slides_container/slides_container.js @@ -178,7 +178,7 @@ let SlidesContainer = React.createClass({ let breadcrumbs = []; ReactAddons.Children.map(this.props.children, (child, i) => { - if(i >= this.state.startFrom && child.props['data-slide-title']) { + if(child && i >= this.state.startFrom && child.props['data-slide-title']) { breadcrumbs.push(child.props['data-slide-title']); } }); @@ -229,7 +229,7 @@ let SlidesContainer = React.createClass({ // since the default parameter of startFrom is -1, we do not need to check // if its actually present in the url bar, as it will just not match - if(i >= this.state.startFrom) { + if(child && i >= this.state.startFrom) { return ReactAddons.addons.cloneWithProps(child, { className: 'ascribe-slide', style: { diff --git a/js/components/ascribe_table/table_item_acl_filtered.js b/js/components/ascribe_table/table_item_acl_filtered.js index c850ab59..22a28130 100644 --- a/js/components/ascribe_table/table_item_acl_filtered.js +++ b/js/components/ascribe_table/table_item_acl_filtered.js @@ -6,15 +6,15 @@ import React from 'react'; let TableItemAclFiltered = React.createClass({ propTypes: { content: React.PropTypes.object, - requestAction: React.PropTypes.string + notifications: React.PropTypes.string }, render() { var availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete']; - if (this.props.requestAction && this.props.requestAction.length > 0){ + if (this.props.notifications && this.props.notifications.length > 0){ return ( - {this.props.requestAction[0].action + ' request pending'} + {this.props.notifications[0].action_str} ); } diff --git a/js/components/ascribe_uploader/file_drag_and_drop.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js similarity index 72% rename from js/components/ascribe_uploader/file_drag_and_drop.js rename to js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js index 312b504d..4c9211c5 100644 --- a/js/components/ascribe_uploader/file_drag_and_drop.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js @@ -6,20 +6,14 @@ import ProgressBar from 'react-bootstrap/lib/ProgressBar'; import FileDragAndDropDialog from './file_drag_and_drop_dialog'; import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator'; -import { getLangText } from '../../utils/lang_utils'; +import { getLangText } from '../../../utils/lang_utils'; + // Taken from: https://github.com/fedosejev/react-file-drag-and-drop let FileDragAndDrop = React.createClass({ propTypes: { - className: React.PropTypes.string, - onDragStart: React.PropTypes.func, onDrop: React.PropTypes.func.isRequired, - onDrag: React.PropTypes.func, - onDragEnter: React.PropTypes.func, - onLeave: React.PropTypes.func, - onDragLeave: React.PropTypes.func, onDragOver: React.PropTypes.func, - onDragEnd: React.PropTypes.func, onInactive: React.PropTypes.func, filesToUpload: React.PropTypes.array, handleDeleteFile: React.PropTypes.func, @@ -37,37 +31,16 @@ let FileDragAndDrop = React.createClass({ hashingProgress: React.PropTypes.number, // sets the value of this.state.hashingProgress in reactfineuploader // to -1 which is code for: aborted - handleCancelHashing: React.PropTypes.func - }, + handleCancelHashing: React.PropTypes.func, - handleDragStart(event) { - if (typeof this.props.onDragStart === 'function') { - this.props.onDragStart(event); - } - }, + // A class of a file the user has to upload + // Needs to be defined both in singular as well as in plural + fileClassToUpload: React.PropTypes.shape({ + singular: React.PropTypes.string, + plural: React.PropTypes.string + }), - handleDrag(event) { - if (typeof this.props.onDrag === 'function') { - this.props.onDrag(event); - } - }, - - handleDragEnd(event) { - if (typeof this.props.onDragEnd === 'function') { - this.props.onDragEnd(event); - } - }, - - handleDragEnter(event) { - if (typeof this.props.onDragEnter === 'function') { - this.props.onDragEnter(event); - } - }, - - handleDragLeave(event) { - if (typeof this.props.onDragLeave === 'function') { - this.props.onDragLeave(event); - } + allowedExtensions: React.PropTypes.string }, handleDragOver(event) { @@ -159,14 +132,27 @@ let FileDragAndDrop = React.createClass({ }, render: function () { + let { filesToUpload, + dropzoneInactive, + className, + hashingProgress, + handleCancelHashing, + multiple, + enableLocalHashing, + fileClassToUpload, + areAssetsDownloadable, + areAssetsEditable, + allowedExtensions + } = this.props; + // has files only is true if there are files that do not have the status deleted or canceled - let hasFiles = this.props.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0; - let className = hasFiles ? 'has-files ' : ''; - className += this.props.dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone'; - className += this.props.className ? ' ' + this.props.className : ''; + let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0; + let updatedClassName = hasFiles ? 'has-files ' : ''; + updatedClassName += dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone'; + updatedClassName += ' file-drag-and-drop'; // if !== -2: triggers a FileDragAndDrop-global spinner - if(this.props.hashingProgress !== -2) { + if(hashingProgress !== -2) { return (
@@ -184,29 +170,26 @@ let FileDragAndDrop = React.createClass({ } else { return (
+ onDrop={this.handleDrop}> + enableLocalHashing={enableLocalHashing} + fileClassToUpload={fileClassToUpload}/> + areAssetsDownloadable={areAssetsDownloadable} + areAssetsEditable={areAssetsEditable}/> + onChange={this.handleDrop} + accept={allowedExtensions}/>
); } diff --git a/js/components/ascribe_uploader/file_drag_and_drop_dialog.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js similarity index 71% rename from js/components/ascribe_uploader/file_drag_and_drop_dialog.js rename to js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js index 0cb14be7..f74eb713 100644 --- a/js/components/ascribe_uploader/file_drag_and_drop_dialog.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js @@ -3,7 +3,7 @@ import React from 'react'; import Router from 'react-router'; -import { getLangText } from '../../utils/lang_utils'; +import { getLangText } from '../../../utils/lang_utils'; let Link = Router.Link; @@ -12,7 +12,14 @@ let FileDragAndDropDialog = React.createClass({ hasFiles: React.PropTypes.bool, multipleFiles: React.PropTypes.bool, onClick: React.PropTypes.func, - enableLocalHashing: React.PropTypes.bool + enableLocalHashing: React.PropTypes.bool, + + // A class of a file the user has to upload + // Needs to be defined both in singular as well as in plural + fileClassToUpload: React.PropTypes.shape({ + singular: React.PropTypes.string, + plural: React.PropTypes.string + }) }, mixins: [Router.State], @@ -56,29 +63,29 @@ let FileDragAndDropDialog = React.createClass({ } else { if(this.props.multipleFiles) { return ( -
-

{getLangText('Drag files here')}

+ +

{getLangText('Drag %s here', this.props.fileClassToUpload.plural)}

{getLangText('or')}

- {getLangText('choose files to upload')} + {getLangText('choose %s to upload', this.props.fileClassToUpload.plural)} -
+ ); } else { - let dialog = queryParams.method === 'hash' ? getLangText('choose a file to hash') : getLangText('choose a file to upload'); + let dialog = queryParams.method === 'hash' ? getLangText('choose a %s to hash', this.props.fileClassToUpload.singular) : getLangText('choose a %s to upload', this.props.fileClassToUpload.singular); return ( -
-

{getLangText('Drag a file here')}

+ +

{getLangText('Drag a %s here', this.props.fileClassToUpload.singular)}

{getLangText('or')}

{dialog} -
+ ); } } diff --git a/js/components/ascribe_uploader/file_drag_and_drop_preview.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview.js similarity index 96% rename from js/components/ascribe_uploader/file_drag_and_drop_preview.js rename to js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview.js index 05c4a688..86d4135e 100644 --- a/js/components/ascribe_uploader/file_drag_and_drop_preview.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview.js @@ -4,7 +4,9 @@ import React from 'react'; import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image'; import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other'; -import { getLangText } from '../../utils/lang_utils.js'; + + +import { getLangText } from '../../../utils/lang_utils'; let FileDragAndDropPreview = React.createClass({ @@ -43,6 +45,7 @@ let FileDragAndDropPreview = React.createClass({ handleDownloadFile() { if(this.props.file.s3Url) { + // This simply opens a new browser tab with the url provided open(this.props.file.s3Url); } }, diff --git a/js/components/ascribe_uploader/file_drag_and_drop_preview_image.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_image.js similarity index 95% rename from js/components/ascribe_uploader/file_drag_and_drop_preview_image.js rename to js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_image.js index 8e599e27..7b4977c8 100644 --- a/js/components/ascribe_uploader/file_drag_and_drop_preview_image.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_image.js @@ -3,8 +3,8 @@ import React from 'react'; import ProgressBar from 'react-bootstrap/lib/ProgressBar'; -import AppConstants from '../../constants/application_constants'; -import { getLangText } from '../../utils/lang_utils.js'; +import AppConstants from '../../../constants/application_constants'; +import { getLangText } from '../../../utils/lang_utils'; let FileDragAndDropPreviewImage = React.createClass({ propTypes: { diff --git a/js/components/ascribe_uploader/file_drag_and_drop_preview_iterator.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_iterator.js similarity index 96% rename from js/components/ascribe_uploader/file_drag_and_drop_preview_iterator.js rename to js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_iterator.js index ec1dd181..2352407a 100644 --- a/js/components/ascribe_uploader/file_drag_and_drop_preview_iterator.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_iterator.js @@ -5,7 +5,7 @@ import React from 'react'; import FileDragAndDropPreview from './file_drag_and_drop_preview'; import FileDragAndDropPreviewProgress from './file_drag_and_drop_preview_progress'; -import { displayValidFilesFilter } from './react_s3_fine_uploader_utils'; +import { displayValidFilesFilter } from '../react_s3_fine_uploader_utils'; let FileDragAndDropPreviewIterator = React.createClass({ diff --git a/js/components/ascribe_uploader/file_drag_and_drop_preview_other.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_other.js similarity index 93% rename from js/components/ascribe_uploader/file_drag_and_drop_preview_other.js rename to js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_other.js index 2528a9d7..0716b72f 100644 --- a/js/components/ascribe_uploader/file_drag_and_drop_preview_other.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_other.js @@ -3,8 +3,8 @@ import React from 'react'; import ProgressBar from 'react-bootstrap/lib/ProgressBar'; -import AppConstants from '../../constants/application_constants'; -import { getLangText } from '../../utils/lang_utils.js'; +import AppConstants from '../../../constants/application_constants'; +import { getLangText } from '../../../utils/lang_utils'; let FileDragAndDropPreviewOther = React.createClass({ propTypes: { @@ -61,7 +61,7 @@ let FileDragAndDropPreviewOther = React.createClass({
{actionSymbol} - {'.' + this.props.type} +

{'.' + this.props.type}

diff --git a/js/components/ascribe_uploader/file_drag_and_drop_preview_progress.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_progress.js similarity index 88% rename from js/components/ascribe_uploader/file_drag_and_drop_preview_progress.js rename to js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_progress.js index d1b34f41..1f1fd421 100644 --- a/js/components/ascribe_uploader/file_drag_and_drop_preview_progress.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_progress.js @@ -4,7 +4,8 @@ import React from 'react'; import ProgressBar from 'react-bootstrap/lib/ProgressBar'; -import { displayValidProgressFilesFilter } from './react_s3_fine_uploader_utils'; +import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils'; +import { getLangText } from '../../../utils/lang_utils'; let FileDragAndDropPreviewProgress = React.createClass({ @@ -54,7 +55,7 @@ let FileDragAndDropPreviewProgress = React.createClass({ return ( ); diff --git a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js new file mode 100644 index 00000000..1547272e --- /dev/null +++ b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js @@ -0,0 +1,103 @@ +'use strict'; + +import React from 'react'; + +import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils'; +import { getLangText } from '../../../utils/lang_utils'; + + +let UploadButton = React.createClass({ + propTypes: { + onDrop: React.PropTypes.func.isRequired, + filesToUpload: React.PropTypes.array, + multiple: React.PropTypes.bool, + + // For simplification purposes we're just going to use this prop as a + // label for the upload button + fileClassToUpload: React.PropTypes.shape({ + singular: React.PropTypes.string, + plural: React.PropTypes.string + }), + + allowedExtensions: React.PropTypes.string + }, + + handleDrop(event) { + event.preventDefault(); + event.stopPropagation(); + let files = event.target.files; + + if(typeof this.props.onDrop === 'function' && files) { + this.props.onDrop(files); + } + + }, + + getUploadingFiles() { + return this.props.filesToUpload.filter((file) => file.status === 'uploading'); + }, + + handleOnClick() { + let uploadingFiles = this.getUploadingFiles(); + + // We only want the button to be clickable if there are no files currently uploading + if(uploadingFiles.length === 0) { + // 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 + }); + + evt.stopPropagation(); + this.refs.fileinput.getDOMNode().dispatchEvent(evt); + } + }, + + getButtonLabel() { + let { filesToUpload, fileClassToUpload } = this.props; + + // filter invalid files that might have been deleted or canceled... + filesToUpload = filesToUpload.filter(displayValidProgressFilesFilter); + + // Depending on wether there is an upload going on or not we + // display the progress + if(filesToUpload.length > 0) { + return getLangText('Upload progress') + ': ' + Math.ceil(filesToUpload[0].progress) + '%'; + } else { + return fileClassToUpload.singular; + } + }, + + render() { + let { + multiple, + fileClassToUpload, + allowedExtensions + } = this.props; + + return ( + + ); + } +}); + +export default UploadButton; \ No newline at end of file diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index 39ab42fc..c7a5f9a7 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -7,7 +7,7 @@ import Q from 'q'; import S3Fetcher from '../../fetchers/s3_fetcher'; -import FileDragAndDrop from './file_drag_and_drop'; +import FileDragAndDrop from './ascribe_file_drag_and_drop/file_drag_and_drop'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; @@ -15,12 +15,12 @@ import GlobalNotificationActions from '../../actions/global_notification_actions import AppConstants from '../../constants/application_constants'; import { computeHashOfFile } from '../../utils/file_utils'; -import { displayValidFilesFilter } from './react_s3_fine_uploader_utils'; +import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils'; import { getCookie } from '../../utils/fetch_api_utils'; import { getLangText } from '../../utils/lang_utils'; -var ReactS3FineUploader = React.createClass({ +let ReactS3FineUploader = React.createClass({ propTypes: { keyRoutine: React.PropTypes.shape({ url: React.PropTypes.string, @@ -37,7 +37,7 @@ var ReactS3FineUploader = React.createClass({ React.PropTypes.number ]) }), - submitKey: React.PropTypes.func, + submitFile: React.PropTypes.func, autoUpload: React.PropTypes.bool, debug: React.PropTypes.bool, objectProperties: React.PropTypes.shape({ @@ -84,7 +84,8 @@ var ReactS3FineUploader = React.createClass({ }), validation: React.PropTypes.shape({ itemLimit: React.PropTypes.number, - sizeLimit: React.PropTypes.string + sizeLimit: React.PropTypes.string, + allowedExtensions: React.PropTypes.arrayOf(React.PropTypes.string) }), messages: React.PropTypes.shape({ unsupportedBrowser: React.PropTypes.string @@ -111,7 +112,22 @@ var ReactS3FineUploader = React.createClass({ enableLocalHashing: React.PropTypes.bool, // automatically injected by React-Router - query: React.PropTypes.object + query: React.PropTypes.object, + + // A class of a file the user has to upload + // Needs to be defined both in singular as well as in plural + fileClassToUpload: React.PropTypes.shape({ + singular: React.PropTypes.string, + plural: React.PropTypes.string + }), + + // Uploading functionality of react fineuploader is disconnected from its UI + // layer, which means that literally every (properly adjusted) react element + // can handle the UI handling. + fileInputElement: React.PropTypes.oneOfType([ + React.PropTypes.func, + React.PropTypes.element + ]) }, mixins: [Router.State], @@ -163,7 +179,12 @@ var ReactS3FineUploader = React.createClass({ return name; }, multiple: false, - defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.') + defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.'), + fileClassToUpload: { + singular: getLangText('file'), + plural: getLangText('files') + }, + fileInputElement: FileDragAndDrop }; }, @@ -313,6 +334,9 @@ var ReactS3FineUploader = React.createClass({ } else if(res.digitalwork) { file.s3Url = res.digitalwork.url_safe; file.s3UrlSafe = res.digitalwork.url_safe; + } else if(res.contractblob) { + file.s3Url = res.contractblob.url_safe; + file.s3UrlSafe = res.contractblob.url_safe; } else { throw new Error(getLangText('Could not find a url to download.')); } @@ -384,12 +408,12 @@ var ReactS3FineUploader = React.createClass({ // Only after the blob has been created server-side, we can make the form submittable. this.createBlob(files[id]) .then(() => { - // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey + // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile // are optional, we'll only trigger them when they're actually defined - if(this.props.submitKey) { - this.props.submitKey(files[id].key); + if(this.props.submitFile) { + this.props.submitFile(files[id]); } else { - console.warn('You didn\'t define submitKey in as a prop in react-s3-fine-uploader'); + console.warn('You didn\'t define submitFile in as a prop in react-s3-fine-uploader'); } // for explanation, check comment of if statement above @@ -424,7 +448,7 @@ var ReactS3FineUploader = React.createClass({ }); this.state.uploader.cancelAll(); - let notification = new GlobalNotificationModel(this.props.defaultErrorMessage, 'danger', 5000); + let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000); GlobalNotificationActions.appendGlobalNotification(notification); }, @@ -449,7 +473,7 @@ var ReactS3FineUploader = React.createClass({ let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); - // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey + // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile // are optional, we'll only trigger them when they're actually defined if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) { @@ -516,7 +540,7 @@ var ReactS3FineUploader = React.createClass({ GlobalNotificationActions.appendGlobalNotification(notification); } - // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey + // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile // are optional, we'll only trigger them when they're actually defined if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { // also, lets check if after the completion of this upload, @@ -541,7 +565,7 @@ var ReactS3FineUploader = React.createClass({ this.setStatusOfFile(fileId, 'deleted'); // In some instances (when the file was already uploaded and is just displayed to the user - // - for example in the loan contract or additional files dialog) + // - for example in the contract or additional files dialog) // fineuploader does not register an id on the file (we do, don't be confused by this!). // Since you can only delete a file by its id, we have to implement this method ourselves // @@ -816,27 +840,48 @@ var ReactS3FineUploader = React.createClass({ }, + getAllowedExtensions() { + let { validation } = this.props; + + if(validation && validation.allowedExtensions && validation.allowedExtensions.length > 0) { + return transformAllowedExtensionsToInputAcceptProp(validation.allowedExtensions); + } else { + return null; + } + }, + render() { - return ( -
- -
- ); + let { + multiple, + areAssetsDownloadable, + areAssetsEditable, + onInactive, + enableLocalHashing, + fileClassToUpload, + validation, + fileInputElement + } = this.props; + + // Here we initialize the template that has been either provided from the outside + // or the default input that is FileDragAndDrop. + return React.createElement(fileInputElement, { + onDrop: this.handleUploadFile, + filesToUpload: this.state.filesToUpload, + handleDeleteFile: this.handleDeleteFile, + handleCancelFile: this.handleCancelFile, + handlePauseFile: this.handlePauseFile, + handleResumeFile: this.handleResumeFile, + handleCancelHashing: this.handleCancelHashing, + multiple: multiple, + areAssetsDownloadable: areAssetsDownloadable, + areAssetsEditable: areAssetsEditable, + onInactive: onInactive, + dropzoneInactive: this.isDropzoneInactive(), + hashingProgress: this.state.hashingProgress, + enableLocalHashing: enableLocalHashing, + fileClassToUpload: fileClassToUpload, + allowedExtensions: this.getAllowedExtensions() + }); } }); diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js b/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js index b5376e74..cd1dbce2 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader_utils.js @@ -1,5 +1,38 @@ 'use strict'; +export const formSubmissionValidation = { + /** + * Returns a boolean if there has been at least one file uploaded + * successfully without it being deleted or canceled. + * @param {array of files} files provided by react fine uploader + * @return {boolean} + */ + atLeastOneUploadedFile(files) { + files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled'); + if (files.length > 0 && files[0].status === 'upload successful') { + return true; + } else { + return false; + } + }, + + /** + * File submission for the form is optional, but if the user decides to submit a file + * the form is not ready until there are no more files currently uploading. + * @param {array of files} files files provided by react fine uploader + * @return {boolean} [description] + */ + fileOptional(files) { + let uploadingFiles = files.filter((file) => file.status === 'submitting'); + + if (uploadingFiles.length === 0) { + return true; + } else { + return false; + } + } +}; + /** * Filter function for filtering all deleted and canceled files * @param {object} file A file from filesToUpload that has status as a prop. @@ -9,20 +42,6 @@ export function displayValidFilesFilter(file) { return file.status !== 'deleted' && file.status !== 'canceled'; } -/** - * Returns a boolean if there has been at least one file uploaded - * successfully without it being deleted or canceled. - * @param {array of files} files provided by react fine uploader - * @return {Boolean} - */ -export function isReadyForFormSubmission(files) { - files = files.filter(displayValidFilesFilter); - if (files.length > 0 && files[0].status === 'upload successful') { - return true; - } else { - return false; - } -} /** * Filter function for which files to integrate in the progress process @@ -32,3 +51,23 @@ export function isReadyForFormSubmission(files) { export function displayValidProgressFilesFilter(file) { return file.status !== 'deleted' && file.status !== 'canceled' && file.status !== 'online'; } + + +/** + * Fineuploader allows to specify the file extensions that are allowed to upload. + * For our self defined input, we can reuse those declarations to restrict which files + * the user can pick from his hard drive. + * + * Takes an array of file extensions (['pdf', 'png', ...]) and transforms them into a string + * that can be passed into an html5 input via its 'accept' prop. + * @param {array} allowedExtensions Array of strings without a dot prefixed + * @return {string} Joined string (comma-separated) of the passed-in array + */ +export function transformAllowedExtensionsToInputAcceptProp(allowedExtensions) { + // add a dot in front of the extension + let prefixedAllowedExtensions = allowedExtensions.map((ext) => '.' + ext); + + // generate a comma separated list to add them to the DOM element + // See: http://stackoverflow.com/questions/4328947/limit-file-format-when-using-input-type-file + return prefixedAllowedExtensions.join(', '); +} diff --git a/js/components/coa_verify_container.js b/js/components/coa_verify_container.js index 96987323..d65fdb83 100644 --- a/js/components/coa_verify_container.js +++ b/js/components/coa_verify_container.js @@ -84,10 +84,11 @@ let CoaVerifyForm = React.createClass({ + label="Signature" + editable={true} + overrideForm={true}> diff --git a/js/components/contract_notification.js b/js/components/contract_notification.js new file mode 100644 index 00000000..cd6ceb53 --- /dev/null +++ b/js/components/contract_notification.js @@ -0,0 +1,36 @@ +'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/global_action.js b/js/components/global_action.js new file mode 100644 index 00000000..80df0c75 --- /dev/null +++ b/js/components/global_action.js @@ -0,0 +1,43 @@ +'use strict'; + +import React from 'react'; + +let GlobalAction = React.createClass({ + propTypes: { + requestActions: React.PropTypes.object + }, + + render() { + let pieceActions = null; + if (this.props.requestActions && this.props.requestActions.pieces){ + pieceActions = this.props.requestActions.pieces.map((item) => { + return ( +
+ {item} +
); + }); + } + let editionActions = null; + if (this.props.requestActions && this.props.requestActions.editions){ + editionActions = Object.keys(this.props.requestActions.editions).map((pieceId) => { + return this.props.requestActions.editions[pieceId].map((item) => { + return ( +
+ {item} +
); + }); + }); + } + + if (pieceActions || editionActions) { + return ( +
+ {pieceActions} + {editionActions} +
); + } + return null; + } +}); + +export default GlobalAction; \ No newline at end of file diff --git a/js/components/header.js b/js/components/header.js index 0122cb2f..1f1e83df 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -2,13 +2,6 @@ import React from 'react'; import Router from 'react-router'; -import Favico from 'favico.js'; -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 EventActions from '../actions/event_actions'; import Nav from 'react-bootstrap/lib/Nav'; import Navbar from 'react-bootstrap/lib/Navbar'; @@ -18,6 +11,17 @@ import MenuItem from 'react-bootstrap/lib/MenuItem'; import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink'; import NavItemLink from 'react-router-bootstrap/lib/NavItemLink'; +import AclProxy from './acl_proxy'; + +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 EventActions from '../actions/event_actions'; + +import HeaderNotifications from './header_notification'; + import HeaderNotificationDebug from './header_notification_debug'; import NavRoutesLinks from './nav_routes_links'; @@ -25,6 +29,7 @@ import NavRoutesLinks from './nav_routes_links'; import { mergeOptions } from '../utils/general_utils'; import { getLangText } from '../utils/lang_utils'; +let setFavicon = require('favicon-setter'); let Header = React.createClass({ propTypes: { @@ -41,7 +46,10 @@ let Header = React.createClass({ }, getInitialState() { - return mergeOptions(WhitelabelStore.getState(), UserStore.getState()); + return mergeOptions( + WhitelabelStore.getState(), + UserStore.getState() + ); }, componentDidMount() { @@ -56,59 +64,104 @@ let Header = React.createClass({ WhitelabelStore.unlisten(this.onChange); }, getLogo(){ - let logo = ( + if (this.state.whitelabel && this.state.whitelabel.logo){ + let logoPath = this.state.whitelabel.logo; + let logo = ; + console.log('should change browser icon'); + console.log(logoPath); + try { + setFavicon(logoPath); + } + catch (e){ + console.log(e.message()); + } + return logo; + } + return ( ascribe ); - if (this.state.whitelabel && this.state.whitelabel.logo){ - let logoPath = this.state.whitelabel.logo; - logo = ; - let favicon = new Favico(); - let image = new Image(); - image.src = logoPath; - console.log('should change browser icon'); - console.log(logoPath); - favicon.image(image); - console.log(image); - console.log(favicon); - } - return logo; }, getPoweredBy(){ - if (this.state.whitelabel && this.state.whitelabel.logo) { - return ( -
  • - - {getLangText('powered by')} - ascribe - - -
  • - ); - } - return null; + return ( + +
  • + + {getLangText('powered by')} + ascribe + + +
  • +
    + ); }, + onChange(state) { this.setState(state); if(this.state.currentUser && this.state.currentUser.email) { EventActions.profileDidLoad.defer(this.state.currentUser); } }, + + onMenuItemClick() { + /* + This is a hack to make the dropdown close after clicking on an item + The function just need to be defined + + from https://github.com/react-bootstrap/react-bootstrap/issues/368: + + @jvillasante - Have you tried to use onSelect with the DropdownButton? + I don't have a working example that is exactly like yours, + but I just noticed that the Dropdown closes when I've attached an event handler to OnSelect: + + + + onSelected: function(e) { + // doesn't need to have functionality (necessarily) ... just wired up + } + Internally, a call to DropdownButton.setDropDownState(false) is made which will hide the dropdown menu. + So, you should be able to call that directly on the DropdownButton instance as well if needed. + + NOW, THAT DIDN'T WORK - the onSelect routine isnt triggered in all cases + Hence, we do this manually + */ + this.refs.dropdownbutton.setDropdownState(false); + }, + render() { let account; let signup; let navRoutesLinks; if (this.state.currentUser.username){ account = ( - - {getLangText('Account Settings')} + + + {getLangText('Account Settings')} + + + + {getLangText('Contract Settings')} + + {getLangText('Log out')} - + ); - navRoutesLinks = ; + navRoutesLinks = ; } else { account = {getLangText('LOGIN')}; @@ -132,6 +185,7 @@ let Header = React.createClass({ {account} {signup} + {navRoutesLinks} diff --git a/js/components/header_notification.js b/js/components/header_notification.js new file mode 100644 index 00000000..67252af8 --- /dev/null +++ b/js/components/header_notification.js @@ -0,0 +1,218 @@ +'use strict'; + +import React from 'react'; +import Router from 'react-router'; +import DropdownButton from 'react-bootstrap/lib/DropdownButton'; +import Glyphicon from 'react-bootstrap/lib/Glyphicon'; +import MenuItem from 'react-bootstrap/lib/MenuItem'; + +import Nav from 'react-bootstrap/lib/Nav'; + +import NotificationActions from '../actions/notification_actions'; +import NotificationStore from '../stores/notification_store'; + +import { mergeOptions } from '../utils/general_utils'; +import { getLangText } from '../utils/lang_utils'; + +let Link = Router.Link; + + +let HeaderNotifications = React.createClass({ + + getInitialState() { + return mergeOptions( + NotificationStore.getState() + ); + }, + + componentDidMount() { + NotificationStore.listen(this.onChange); + NotificationActions.fetchPieceListNotifications(); + NotificationActions.fetchEditionListNotifications(); + }, + + componentWillUnmount() { + NotificationStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + onMenuItemClick(event) { + /* + This is a hack to make the dropdown close after clicking on an item + The function just need to be defined + + from https://github.com/react-bootstrap/react-bootstrap/issues/368: + + @jvillasante - Have you tried to use onSelect with the DropdownButton? + I don't have a working example that is exactly like yours, + but I just noticed that the Dropdown closes when I've attached an event handler to OnSelect: + + + + onSelected: function(e) { + // doesn't need to have functionality (necessarily) ... just wired up + } + Internally, a call to DropdownButton.setDropDownState(false) is made which will hide the dropdown menu. + So, you should be able to call that directly on the DropdownButton instance as well if needed. + + NOW, THAT DIDN'T WORK - the onSelect routine isnt triggered in all cases + Hence, we do this manually + */ + this.refs.dropdownbutton.setDropdownState(false); + }, + + getPieceNotifications(){ + if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) { + return ( +
    +
    + Artworks ({this.state.pieceListNotifications.length}) +
    + {this.state.pieceListNotifications.map((pieceNotification, i) => { + return ( + + + + ); + } + )} +
    + ); + } + return null; + }, + + getEditionNotifications(){ + if (this.state.editionListNotifications && this.state.editionListNotifications.length > 0) { + return ( +
    +
    + Editions ({this.state.editionListNotifications.length}) +
    + {this.state.editionListNotifications.map((editionNotification, i) => { + return ( + + + + ); + } + )} +
    + ); + } + return null; + }, + + render() { + if ((this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) || + (this.state.editionListNotifications && this.state.editionListNotifications.length > 0)){ + let numNotifications = 0; + if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) { + numNotifications += this.state.pieceListNotifications.length; + } + if (this.state.editionListNotifications && this.state.editionListNotifications.length > 0) { + numNotifications += this.state.editionListNotifications.length; + } + return ( + + ); + } + return null; + } +}); + +let NotificationListItem = React.createClass({ + propTypes: { + notification: React.PropTypes.array, + pieceOrEdition: React.PropTypes.object, + onClick: React.PropTypes.func + }, + + isPiece() { + return !(this.props.pieceOrEdition && this.props.pieceOrEdition.parent); + }, + + getLinkData() { + + if (this.isPiece()) { + return { + to: 'piece', + params: { + pieceId: this.props.pieceOrEdition.id + } + }; + } else { + return { + to: 'edition', + params: { + editionId: this.props.pieceOrEdition.bitcoin_id + } + }; + } + + }, + + onClick(event){ + this.props.onClick(event); + }, + + getNotificationText(){ + let numNotifications = null; + if (this.props.notification.length > 1){ + numNotifications =
    + {this.props.notification.length - 1} more...
    ; + } + return ( +
    + {this.props.notification[0].action_str} + {numNotifications} +
    ); + }, + + render() { + if (this.props.pieceOrEdition) { + return ( + +
    +
    +
    + +
    +
    +
    +

    {this.props.pieceOrEdition.title}

    +
    by {this.props.pieceOrEdition.artist_name}
    + {this.getNotificationText()} +
    +
    + ); + } + return null; + } +}); + +export default HeaderNotifications; diff --git a/js/components/logout_container.js b/js/components/logout_container.js index 096fa490..c7769867 100644 --- a/js/components/logout_container.js +++ b/js/components/logout_container.js @@ -19,7 +19,7 @@ let LogoutContainer = React.createClass({ Alt.flush(); // kill intercom (with fire) window.Intercom('shutdown'); - this.transitionTo(baseUrl); + this.replaceWith(baseUrl); }) .catch((err) => { console.logGlobal(err); diff --git a/js/components/nav_routes_links.js b/js/components/nav_routes_links.js index 9a266ba8..d3342cb8 100644 --- a/js/components/nav_routes_links.js +++ b/js/components/nav_routes_links.js @@ -3,53 +3,80 @@ import React from 'react'; import Nav from 'react-bootstrap/lib/Nav'; -import DropdownButton from 'react-bootstrap/lib/DropdownButton'; -import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink'; -import NavItemLink from 'react-router-bootstrap/lib/NavItemLink'; + +import NavRoutesLinksLink from './nav_routes_links_link'; + +import AclProxy from './acl_proxy'; import { sanitizeList } from '../utils/general_utils'; let NavRoutesLinks = React.createClass({ propTypes: { - routes: React.PropTypes.element + routes: React.PropTypes.element, + userAcl: React.PropTypes.object }, - extractLinksFromRoutes(node, i) { + /** + * This method generales a bunch of react-bootstrap specific links + * from the routes we defined in one of the specific routes.js file + * + * We can define a headerTitle as well as a aclName and according to that the + * link will be created for a specific user + * @param {ReactElement} node Starts at the very top of a routes files root + * @param {object} userAcl ACL object we use throughout the whole app + * @param {number} i Depth of the route in comparison to the root + * @return {Array} Array of ReactElements that can be displayed to the user + */ + extractLinksFromRoutes(node, userAcl, i) { if(!node) { return; } - node = node.props; + let links = node.props.children.map((child, j) => { + let childrenFn = null; + let { aclName, headerTitle, name, children } = child.props; - let links = node.children.map((child, j) => { + // If the node has children that could be rendered, then we want + // to execute this function again with the child as the root + // + // Otherwise we'll just pass childrenFn as false + if(child.props.children && child.props.children.length > 0) { + childrenFn = this.extractLinksFromRoutes(child, userAcl, i++); + } - // check if this a candidate for a link generation - if(child.props.headerTitle && typeof child.props.headerTitle === 'string') { - - // also check if it is a candidate for generating a dropdown menu - if(child.props.children && child.props.children.length > 0) { + // We validate if the user has set the title correctly, + // otherwise we're not going to render his route + if(headerTitle && typeof headerTitle === 'string') { + // if there is an aclName present on the route definition, + // we evaluate it against the user's acl + if(aclName && typeof aclName !== 'undefined') { return ( - - {this.extractLinksFromRoutes(child, i++)} - - ); - } else if(i === 1) { - // if the node's child is actually a node of level one (a child of a node), we're - // returning a DropdownButton matching MenuItemLink - return ( - {child.props.headerTitle} - ); - } else if(i === 0) { - return ( - {child.props.headerTitle} + + + ); } else { - return null; + return ( + + ); } } else { return null; } + }); // remove all nulls from the list of generated links @@ -57,9 +84,11 @@ let NavRoutesLinks = React.createClass({ }, render() { + let {routes, userAcl} = this.props; + return ( ); } diff --git a/js/components/nav_routes_links_link.js b/js/components/nav_routes_links_link.js new file mode 100644 index 00000000..15aff405 --- /dev/null +++ b/js/components/nav_routes_links_link.js @@ -0,0 +1,51 @@ +'use strict'; + +import React from 'react'; + +import DropdownButton from 'react-bootstrap/lib/DropdownButton'; +import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink'; +import NavItemLink from 'react-router-bootstrap/lib/NavItemLink'; + +let NavRoutesLinksLink = React.createClass({ + propTypes: { + headerTitle: React.PropTypes.string, + routeName: React.PropTypes.string, + + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]), + + depth: React.PropTypes.number + }, + + render() { + let { children, headerTitle, depth, routeName } = this.props; + + // if the route has children, we're returning a DropdownButton that will get filled + // with MenuItemLinks + if(children) { + return ( + + {children} + + ); + } else { + if(depth === 1) { + // if the node's child is actually a node of level one (a child of a node), we're + // returning a DropdownButton matching MenuItemLink + return ( + {headerTitle} + ); + } else if(depth === 0) { + return ( + {headerTitle} + ); + } else { + return null; + } + } + } +}); + +export default NavRoutesLinksLink; \ No newline at end of file diff --git a/js/components/piece_list.js b/js/components/piece_list.js index 466f3ad3..35dcaba0 100644 --- a/js/components/piece_list.js +++ b/js/components/piece_list.js @@ -15,12 +15,16 @@ import AccordionListItemTableEditions from './ascribe_accordion_list/accordion_l import Pagination from './ascribe_pagination/pagination'; +import PieceListFilterDisplay from './piece_list_filter_display'; + import PieceListBulkModal from './ascribe_piece_list_bulk_modal/piece_list_bulk_modal'; import PieceListToolbar from './ascribe_piece_list_toolbar/piece_list_toolbar'; import AppConstants from '../constants/application_constants'; import { mergeOptions } from '../utils/general_utils'; +import { getLangText } from '../utils/lang_utils'; + let PieceList = React.createClass({ propTypes: { @@ -30,7 +34,6 @@ let PieceList = React.createClass({ filterParams: React.PropTypes.array, orderParams: React.PropTypes.array, orderBy: React.PropTypes.string - }, mixins: [Router.Navigation, Router.State], @@ -39,13 +42,14 @@ let PieceList = React.createClass({ return { accordionListItemType: AccordionListItemWallet, orderParams: ['artist_name', 'title'], - filterParams: [ - 'acl_transfer', - 'acl_consign', - { - key: 'acl_create_editions', - label: 'create editions' - }] + filterParams: [{ + label: getLangText('Show works I can'), + items: [ + 'acl_transfer', + 'acl_consign', + 'acl_create_editions' + ] + }] }; }, getInitialState() { @@ -60,18 +64,18 @@ let PieceList = React.createClass({ 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) - .then(() => PieceListActions.fetchPieceRequestActions()); + orderBy, this.state.orderAsc, this.state.filterBy); } }, componentDidUpdate() { if (this.props.redirectTo && this.state.unfilteredPieceListCount === 0) { // FIXME: hack to redirect out of the dispatch cycle - window.setTimeout(() => this.transitionTo(this.props.redirectTo), 0); + window.setTimeout(() => this.transitionTo(this.props.redirectTo, this.getQuery())); } }, @@ -90,8 +94,8 @@ let PieceList = React.createClass({ // 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.state.orderBy, this.state.orderAsc, + this.state.filterBy); }; }, @@ -147,6 +151,7 @@ let PieceList = React.createClass({ render() { let loadingElement = (); let AccordionListItemType = this.props.accordionListItemType; + return (
    + { + return { + label: filterParam.label, + items: filterParam.items.map((item) => { + if(typeof item !== 'string' && typeof item.key === 'string' && typeof item.label === 'string') { + return { + key: item.key, + label: item.label, + value: filterBy[item.key] || false + }; + } else { + return { + key: item, + label: item.split('acl_')[1].replace(/_/g, ' '), + value: filterBy[item] || false + }; + } + }) + }; + }); + }, + + /** + * Takes the list of filters generated in transformFilterParamsItemsToBools and + * transforms them into human readable text. + * @param {Object} filtersWithLabel An object of the shape {key: , label: , value: } + * @return {string} A human readable string + */ + getFilterText(filtersWithLabel) { + let filterTextList = filtersWithLabel + // Iterate over all provided filterLabels and generate a list + // of human readable strings + .map((filterWithLabel) => { + let activeFilterWithLabel = filterWithLabel + .items + // If the filter is active (which it is when its value is true), + // we're going to include it's label into a list, + // otherwise we'll just return nothing + .map((filter) => { + if(filter.value) { + return filter.label; + } + }) + // if nothing is returned, that index is 'undefined'. + // As we only want active filter, we filter out all falsy values e.g. undefined + .filter((filterName) => !!filterName) + // and join the result to a string + .join(', '); + + // If this actually didn't generate an empty string, + // we take the label and concat it to the result. + if(activeFilterWithLabel) { + return filterWithLabel.label + ': ' + activeFilterWithLabel; + } + }) + // filter out strings that are undefined, as their filter's were not activated + .filter((filterText) => !!filterText) + // if there are multiple sentences, capitalize the first one and lowercase the others + .map((filterText, i) => i === 0 ? filterText.charAt(0).toUpperCase() + filterText.substr(1) : filterText.charAt(0).toLowerCase() + filterText.substr(1)) + .join(' and '); + + return filterTextList; + }, + + render() { + let { filterBy } = this.props; + let filtersWithLabel = this.transformFilterParamsItemsToBools(); + + // do not show the FilterDisplay if there are no filters applied + if(filterBy && Object.keys(filterBy).length === 0) { + return null; + } else { + return ( +
    +
    + {this.getFilterText(filtersWithLabel)} +
    +
    +
    + ); + } + } +}); + +export default PieceListFilterDisplay; \ No newline at end of file diff --git a/js/components/settings_container.js b/js/components/settings_container.js deleted file mode 100644 index 96ca3b3b..00000000 --- a/js/components/settings_container.js +++ /dev/null @@ -1,408 +0,0 @@ -'use strict'; - -import React from 'react'; -import Router from 'react-router'; - -import UserActions from '../actions/user_actions'; -import UserStore from '../stores/user_store'; - -import WalletSettingsActions from '../actions/wallet_settings_actions'; -import WalletSettingsStore from '../stores/wallet_settings_store'; - -import ApplicationActions from '../actions/application_actions'; -import ApplicationStore from '../stores/application_store'; - -import GlobalNotificationModel from '../models/global_notification_model'; -import GlobalNotificationActions from '../actions/global_notification_actions'; - -import ReactS3FineUploader from './ascribe_uploader/react_s3_fine_uploader'; - -import CollapsibleParagraph from './ascribe_collapsible/collapsible_paragraph'; -import Form from './ascribe_forms/form'; -import Property from './ascribe_forms/property'; -import InputCheckbox from './ascribe_forms/input_checkbox'; - -import ActionPanel from './ascribe_panel/action_panel'; - -import ApiUrls from '../constants/api_urls'; -import AppConstants from '../constants/application_constants'; - -import { getLangText } from '../utils/lang_utils'; -import { getCookie } from '../utils/fetch_api_utils'; - -let SettingsContainer = React.createClass({ - propTypes: { - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element]) - }, - - mixins: [Router.Navigation], - - render() { - return ( -
    - - {this.props.children} - - - -
    -
    -
    - ); - } -}); - - -let AccountSettings = React.createClass({ - getInitialState() { - return UserStore.getState(); - }, - - componentDidMount() { - UserStore.listen(this.onChange); - UserActions.fetchCurrentUser(); - }, - - componentWillUnmount() { - UserStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - - handleSuccess(){ - UserActions.fetchCurrentUser(); - let notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000); - GlobalNotificationActions.appendGlobalNotification(notification); - }, - - getFormDataProfile(){ - return {'email': this.state.currentUser.email}; - }, - - render() { - let content = ; - let profile = null; - - if (this.state.currentUser.username) { - content = ( -
    - - - - - - -
    -
    - ); - profile = ( -
    - - - - {' ' + getLangText('Enable hash option, e.g. slow connections or to keep piece private')} - - - -
    - {/* - - */} -
    - ); - } - return ( - - {content} - {profile} - {/*
    - - - -
    -
    */} -
    - ); - } -}); - - - -let BitcoinWalletSettings = React.createClass({ - - propTypes: { - defaultExpanded: React.PropTypes.bool - }, - - getInitialState() { - return WalletSettingsStore.getState(); - }, - - componentDidMount() { - WalletSettingsStore.listen(this.onChange); - WalletSettingsActions.fetchWalletSettings(); - }, - - componentWillUnmount() { - WalletSettingsStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - - render() { - let content = ; - if (this.state.walletSettings.btc_public_key) { - content = ( -
    - -
    {this.state.walletSettings.btc_public_key}
    -
    - -
    {this.state.walletSettings.btc_root_address}
    -
    -
    -
    ); - } - return ( - - {content} - - ); - } -}); - -let LoanContractSettings = React.createClass({ - propTypes: { - defaultExpanded: React.PropTypes.bool - }, - - render() { - return ( - - - - ); - } -}); - -let FileUploader = React.createClass({ - propTypes: { - }, - - render() { - return ( -
    - - - -
    -
    - ); - } -}); - -let APISettings = React.createClass({ - propTypes: { - defaultExpanded: React.PropTypes.bool - }, - - getInitialState() { - return ApplicationStore.getState(); - }, - - componentDidMount() { - ApplicationStore.listen(this.onChange); - ApplicationActions.fetchApplication(); - }, - - componentWillUnmount() { - ApplicationStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - - handleCreateSuccess() { - ApplicationActions.fetchApplication(); - let notification = new GlobalNotificationModel(getLangText('Application successfully created'), 'success', 5000); - GlobalNotificationActions.appendGlobalNotification(notification); - }, - - handleTokenRefresh(event) { - let applicationName = event.target.getAttribute('data-id'); - ApplicationActions.refreshApplicationToken(applicationName); - - let notification = new GlobalNotificationModel(getLangText('Token refreshed'), 'success', 2000); - GlobalNotificationActions.appendGlobalNotification(notification); - }, - - getApplications(){ - let content = ; - if (this.state.applications.length > -1) { - content = this.state.applications.map(function(app, i) { - return ( - -
    - {app.name} -
    -
    - {'Bearer ' + app.bearer_token.token} -
    -
    - } - buttons={ -
    -
    - -
    -
    - }/> - ); - }, this); - } - return content; - }, - - render() { - return ( - -
    - - - -
    -
    -
    -                    Usage: curl <url> -H 'Authorization: Bearer <token>'
    -                
    - {this.getApplications()} -
    - ); - } -}); - -export default SettingsContainer; diff --git a/js/components/signup_container.js b/js/components/signup_container.js index 46813b59..69fab1e8 100644 --- a/js/components/signup_container.js +++ b/js/components/signup_container.js @@ -1,8 +1,13 @@ 'use strict'; import React from 'react'; +import Router from 'react-router'; + import SignupForm from './ascribe_forms/form_signup'; +import { getLangText } from '../utils/lang_utils'; + +let Link = Router.Link; let SignupContainer = React.createClass({ getInitialState() { @@ -33,7 +38,11 @@ let SignupContainer = React.createClass({ return (
    +
    + {getLangText('Already an ascribe user')}? {getLangText('Log in')}...
    +
    + ); } }); diff --git a/js/components/whitelabel/prize/components/ascribe_accordion_list/accordion_list_item_prize.js b/js/components/whitelabel/prize/components/ascribe_accordion_list/accordion_list_item_prize.js index a2f34bba..7b3d5de0 100644 --- a/js/components/whitelabel/prize/components/ascribe_accordion_list/accordion_list_item_prize.js +++ b/js/components/whitelabel/prize/components/ascribe_accordion_list/accordion_list_item_prize.js @@ -124,7 +124,7 @@ let AccordionListItemPrize = React.createClass({
    + aclName="acl_wallet_submit"> 0) { + return ( + ); + } }, render() { @@ -121,11 +133,7 @@ let PieceContainer = React.createClass({ {artistEmail} - + {this.getActions()}
    } @@ -301,7 +309,6 @@ let PrizePieceRatings = React.createClass({
    @@ -321,7 +328,6 @@ let PrizePieceRatings = React.createClass({
    {Object.keys(this.props.piece.extra_data).map((data) => { @@ -418,10 +422,10 @@ let PrizePieceDetails = React.createClass({ + editable={false} + overrideForm={true}> ); } diff --git a/js/components/whitelabel/prize/components/prize_piece_list.js b/js/components/whitelabel/prize/components/prize_piece_list.js index b10d8bb5..04f8eb42 100644 --- a/js/components/whitelabel/prize/components/prize_piece_list.js +++ b/js/components/whitelabel/prize/components/prize_piece_list.js @@ -69,7 +69,7 @@ let PrizePieceList = React.createClass({ accordionListItemType={AccordionListItemPrize} orderParams={orderParams} orderBy={this.state.currentUser.is_jury ? 'rating' : null} - filterParams={null} + filterParams={[]} customSubmitButton={this.getButtonSubmit()}/>
    ); diff --git a/js/components/whitelabel/prize/components/prize_register_piece.js b/js/components/whitelabel/prize/components/prize_register_piece.js index e7a97541..0bd3fe75 100644 --- a/js/components/whitelabel/prize/components/prize_register_piece.js +++ b/js/components/whitelabel/prize/components/prize_register_piece.js @@ -41,20 +41,20 @@ let PrizeRegisterPiece = React.createClass({ + editable={true} + overrideForm={true}> + editable={true} + overrideForm={true}> diff --git a/js/components/whitelabel/prize/components/prize_settings_container.js b/js/components/whitelabel/prize/components/prize_settings_container.js index f81e7078..3e274b57 100644 --- a/js/components/whitelabel/prize/components/prize_settings_container.js +++ b/js/components/whitelabel/prize/components/prize_settings_container.js @@ -9,7 +9,7 @@ import PrizeStore from '../stores/prize_store'; import PrizeJuryActions from '../actions/prize_jury_actions'; import PrizeJuryStore from '../stores/prize_jury_store'; -import SettingsContainer from '../../../settings_container'; +import SettingsContainer from '../../../ascribe_settings/settings_container'; import CollapsibleParagraph from '../../../ascribe_collapsible/collapsible_paragraph'; import Form from '../../../ascribe_forms/form'; @@ -79,7 +79,6 @@ let PrizeSettings = React.createClass({ return (