diff --git a/js/actions/contract_list_actions.js b/js/actions/contract_list_actions.js index 1c5c0913..d368ac73 100644 --- a/js/actions/contract_list_actions.js +++ b/js/actions/contract_list_actions.js @@ -28,12 +28,10 @@ class ContractListActions { } - changeContract(contract){ + changeContract(contract) { return Q.Promise((resolve, reject) => { OwnershipFetcher.changeContract(contract) - .then((res) => { - resolve(res); - }) + .then(resolve) .catch((err)=> { console.logGlobal(err); reject(err); @@ -41,13 +39,11 @@ class ContractListActions { }); } - removeContract(contractId){ - return Q.Promise( (resolve, reject) => { + removeContract(contractId) { + return Q.Promise((resolve, reject) => { OwnershipFetcher.deleteContract(contractId) - .then((res) => { - resolve(res); - }) - .catch( (err) => { + .then(resolve) + .catch((err) => { console.logGlobal(err); reject(err); }); diff --git a/js/actions/edition_actions.js b/js/actions/edition_actions.js index 1727deff..1feee0dd 100644 --- a/js/actions/edition_actions.js +++ b/js/actions/edition_actions.js @@ -7,11 +7,11 @@ class EditionActions { constructor() { this.generateActions( 'fetchEdition', - 'successFetchEdition', 'successFetchCoa', - 'flushEdition', + 'successFetchEdition', 'errorCoa', - 'errorEdition' + 'errorEdition', + 'flushEdition' ); } } diff --git a/js/actions/edition_list_actions.js b/js/actions/edition_list_actions.js index 6f9881ee..a52e32b9 100644 --- a/js/actions/edition_list_actions.js +++ b/js/actions/edition_list_actions.js @@ -17,23 +17,31 @@ class EditionListActions { ); } - fetchEditionList(pieceId, page, pageSize, orderBy, orderAsc, filterBy) { - if((!orderBy && typeof orderAsc === 'undefined') || !orderAsc) { + fetchEditionList({ pieceId, page, pageSize, orderBy, orderAsc, filterBy, maxEdition }) { + if ((!orderBy && typeof orderAsc === 'undefined') || !orderAsc) { orderBy = 'edition_number'; orderAsc = true; } // Taken from: http://stackoverflow.com/a/519157/1263876 - if((typeof page === 'undefined' || !page) && (typeof pageSize === 'undefined' || !pageSize)) { + if ((typeof page === 'undefined' || !page) && (typeof pageSize === 'undefined' || !pageSize)) { page = 1; pageSize = 10; } + let itemsToFetch = pageSize; + // If we only want to fetch up to a specified edition, fetch all pages up to it + // as one page and adjust afterwards + if (typeof maxEdition === 'number') { + itemsToFetch = Math.ceil(maxEdition / pageSize) * pageSize; + page = 1; + } + return Q.Promise((resolve, reject) => { EditionListFetcher - .fetch(pieceId, page, pageSize, orderBy, orderAsc, filterBy) + .fetch({ pieceId, page, itemsToFetch, orderBy, orderAsc, filterBy }) .then((res) => { - if(res && !res.editions) { + if (res && !res.editions) { throw new Error('Piece has no editions to fetch.'); } @@ -44,8 +52,9 @@ class EditionListActions { orderBy, orderAsc, filterBy, - 'editionListOfPiece': res.editions, - 'count': res.count + maxEdition, + count: res.count, + editionListOfPiece: res.editions }); resolve(res); }) @@ -54,7 +63,6 @@ class EditionListActions { reject(err); }); }); - } } diff --git a/js/actions/facebook_actions.js b/js/actions/facebook_actions.js new file mode 100644 index 00000000..2e784fba --- /dev/null +++ b/js/actions/facebook_actions.js @@ -0,0 +1,14 @@ +'use strict'; + +import { altThirdParty } from '../alt'; + + +class FacebookActions { + constructor() { + this.generateActions( + 'sdkReady' + ); + } +} + +export default altThirdParty.createActions(FacebookActions); diff --git a/js/actions/notification_actions.js b/js/actions/notification_actions.js index c3a6db93..5d80e1e7 100644 --- a/js/actions/notification_actions.js +++ b/js/actions/notification_actions.js @@ -9,10 +9,13 @@ class NotificationActions { constructor() { this.generateActions( 'updatePieceListNotifications', + 'flushPieceListNotifications', 'updateEditionListNotifications', + 'flushEditionListNotifications', 'updateEditionNotifications', 'updatePieceNotifications', - 'updateContractAgreementListNotifications' + 'updateContractAgreementListNotifications', + 'flushContractAgreementListNotifications' ); } diff --git a/js/actions/piece_actions.js b/js/actions/piece_actions.js index 9002e8c5..7ad3ae29 100644 --- a/js/actions/piece_actions.js +++ b/js/actions/piece_actions.js @@ -1,28 +1,19 @@ 'use strict'; import { alt } from '../alt'; -import PieceFetcher from '../fetchers/piece_fetcher'; class PieceActions { constructor() { this.generateActions( + 'fetchPiece', + 'successFetchPiece', + 'errorPiece', + 'flushPiece', 'updatePiece', - 'updateProperty', - 'pieceFailed' + 'updateProperty' ); } - - fetchOne(pieceId) { - PieceFetcher.fetchOne(pieceId) - .then((res) => { - this.actions.updatePiece(res.piece); - }) - .catch((err) => { - console.logGlobal(err); - this.actions.pieceFailed(err.json); - }); - } } export default alt.createActions(PieceActions); diff --git a/js/actions/piece_list_actions.js b/js/actions/piece_list_actions.js index 7ef9cb59..c52ef5e2 100644 --- a/js/actions/piece_list_actions.js +++ b/js/actions/piece_list_actions.js @@ -15,7 +15,7 @@ class PieceListActions { ); } - fetchPieceList(page, pageSize, search, orderBy, orderAsc, filterBy) { + fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }) { // To prevent flickering on a pagination request, // we overwrite the piecelist with an empty list before // pieceListCount === -1 defines the loading state @@ -34,7 +34,7 @@ class PieceListActions { // afterwards, we can load the list return Q.Promise((resolve, reject) => { PieceListFetcher - .fetch(page, pageSize, search, orderBy, orderAsc, filterBy) + .fetch({ page, pageSize, search, orderBy, orderAsc, filterBy }) .then((res) => { this.actions.updatePieceList({ page, diff --git a/js/actions/prize_list_actions.js b/js/actions/prize_list_actions.js deleted file mode 100644 index da2f97df..00000000 --- a/js/actions/prize_list_actions.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -import { alt } from '../alt'; -import Q from 'q'; - -import PrizeListFetcher from '../fetchers/prize_list_fetcher'; - -class PrizeListActions { - constructor() { - this.generateActions( - 'updatePrizeList' - ); - } - - fetchPrizeList() { - return Q.Promise((resolve, reject) => { - PrizeListFetcher - .fetch() - .then((res) => { - this.actions.updatePrizeList({ - prizeList: res.prizes, - prizeListCount: res.count - }); - resolve(res); - }) - .catch((err) => { - console.logGlobal(err); - reject(err); - }); - }); - } -} - -export default alt.createActions(PrizeListActions); \ No newline at end of file diff --git a/js/app.js b/js/app.js index dc8204cf..dc8e3d62 100644 --- a/js/app.js +++ b/js/app.js @@ -1,14 +1,13 @@ 'use strict'; import 'babel/polyfill'; +import 'classlist-polyfill'; import React from 'react'; import { Router, Redirect } from 'react-router'; import history from './history'; -/* eslint-disable */ import fetch from 'isomorphic-fetch'; -/* eslint-enable */ import ApiUrls from './constants/api_urls'; @@ -17,44 +16,27 @@ import getRoutes from './routes'; import requests from './utils/requests'; import { updateApiUrls } from './constants/api_urls'; -import { getSubdomainSettings } from './utils/constants_utils'; +import { getDefaultSubdomainSettings, getSubdomainSettings } from './utils/constants_utils'; import { initLogging } from './utils/error_utils'; import { getSubdomain } from './utils/general_utils'; import EventActions from './actions/event_actions'; -/* eslint-disable */ // You can comment out the modules you don't need -// import DebugHandler from './third_party/debug'; -import GoogleAnalyticsHandler from './third_party/ga'; -import RavenHandler from './third_party/raven'; -import IntercomHandler from './third_party/intercom'; -import NotificationsHandler from './third_party/notifications'; -import FacebookHandler from './third_party/facebook'; -/* eslint-enable */ +// import DebugHandler from './third_party/debug_handler'; +import FacebookHandler from './third_party/facebook_handler'; +import GoogleAnalyticsHandler from './third_party/ga_handler'; +import IntercomHandler from './third_party/intercom_handler'; +import NotificationsHandler from './third_party/notifications_handler'; +import RavenHandler from './third_party/raven_handler'; -initLogging(); -let headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json' -}; - -requests.defaults({ - urlMap: ApiUrls, - http: { - headers: headers, - credentials: 'include' - } -}); - -class AppGateway { +const AppGateway = { start() { - let settings; - let subdomain = getSubdomain(); - try { - settings = getSubdomainSettings(subdomain); + const subdomain = getSubdomain(); + const settings = getSubdomainSettings(subdomain); + AppConstants.whitelabel = settings; updateApiUrls(settings.type, subdomain); this.load(settings); @@ -62,28 +44,25 @@ class AppGateway { // if there are no matching subdomains, we're routing // to the default frontend console.logGlobal(err); - this.load(); + this.load(getDefaultSubdomainSettings()); } - } + }, load(settings) { - let type = 'default'; - let subdomain = 'www'; + const { subdomain, type } = settings; let redirectRoute = (); - if (settings) { - type = settings.type; - subdomain = settings.subdomain; - } + if (subdomain) { + // Some whitelabels have landing pages so we should not automatically redirect from / to /collection. + // Only www and cc do not have a landing page. + if (subdomain !== 'cc') { + redirectRoute = null; + } - // www and cc do not have a landing page - if(subdomain && subdomain !== 'cc') { - redirectRoute = null; + // Adds a client specific class to the body for whitelabel styling + window.document.body.classList.add('client--' + subdomain); } - // Adds a client specific class to the body for whitelabel styling - window.document.body.classList.add('client--' + subdomain); - // Send the applicationWillBoot event to the third-party stores EventActions.applicationWillBoot(settings); @@ -101,8 +80,21 @@ class AppGateway { // Send the applicationDidBoot event to the third-party stores EventActions.applicationDidBoot(settings); } -} +}; -let ag = new AppGateway(); -ag.start(); +// Initialize pre-start components +initLogging(); +requests.defaults({ + urlMap: ApiUrls, + http: { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + credentials: 'include' + } +}); + +// And bootstrap app +AppGateway.start(); 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 27657e04..2bb8b2d0 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 @@ -19,9 +19,10 @@ import { getLangText } from '../../utils/lang_utils'; let AccordionListItemEditionWidget = React.createClass({ propTypes: { - className: React.PropTypes.string, piece: React.PropTypes.object.isRequired, toggleCreateEditionsDialog: React.PropTypes.func.isRequired, + + className: React.PropTypes.string, onPollingSuccess: React.PropTypes.func }, @@ -50,14 +51,15 @@ let AccordionListItemEditionWidget = React.createClass({ * Calls the store to either show or hide the editionListTable */ toggleTable() { - let pieceId = this.props.piece.id; - let isEditionListOpen = this.state.isEditionListOpenForPieceId[pieceId] ? this.state.isEditionListOpenForPieceId[pieceId].show : false; + const { piece: { id: pieceId } } = this.props; + const { filterBy, isEditionListOpenForPieceId } = this.state; + const isEditionListOpen = isEditionListOpenForPieceId[pieceId] ? isEditionListOpenForPieceId[pieceId].show : false; - if(isEditionListOpen) { + if (isEditionListOpen) { EditionListActions.toggleEditionList(pieceId); } else { EditionListActions.toggleEditionList(pieceId); - EditionListActions.fetchEditionList(pieceId, null, null, null, null, this.state.filterBy); + EditionListActions.fetchEditionList({ pieceId, filterBy }); } }, diff --git a/js/components/ascribe_accordion_list/accordion_list_item_piece.js b/js/components/ascribe_accordion_list/accordion_list_item_piece.js index 006479c5..9f876388 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_piece.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_piece.js @@ -12,8 +12,11 @@ import { getLangText } from '../../utils/lang_utils'; let AccordionListItemPiece = React.createClass({ propTypes: { className: React.PropTypes.string, - artistName: React.PropTypes.string, - piece: React.PropTypes.object, + artistName: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.element + ]), + piece: React.PropTypes.object.isRequired, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -51,17 +54,21 @@ let AccordionListItemPiece = React.createClass({ piece, subsubheading, thumbnailPlaceholder: ThumbnailPlaceholder } = this.props; - const { url, url_safe } = piece.thumbnail; + const { url: thumbnailUrl, url_safe: thumbnailSafeUrl } = piece.thumbnail; + + // Display the 300x300 thumbnail if we have it, otherwise just use the safe url + const thumbnailDisplayUrl = (piece.thumbnail.thumbnail_sizes && piece.thumbnail.thumbnail_sizes['300x300']) || thumbnailSafeUrl; + let thumbnail; // Since we're going to refactor the thumbnail generation anyway at one point, // for not use the annoying ascribe_spiral.png, we're matching the url against // this name and replace it with a CSS version of the new logo. - if (url.match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/)) { + if (thumbnailUrl.match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/)) { thumbnail = (); } else { thumbnail = ( -
+
); } @@ -79,8 +86,7 @@ let AccordionListItemPiece = React.createClass({ subsubheading={subsubheading} buttons={buttons} badge={badge} - linkData={this.getLinkData()} - > + linkData={this.getLinkData()}> {children} ); 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 23cfb239..3b0bb02e 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 @@ -66,22 +66,34 @@ let AccordionListItemTableEditions = React.createClass({ }, filterSelectedEditions() { - let selectedEditions = this.state.editionList[this.props.parentId] - .filter((edition) => edition.selected); - return selectedEditions; + return this.state + .editionList[this.props.parentId] + .filter((edition) => edition.selected); }, loadFurtherEditions() { + const { parentId: pieceId } = this.props; + const { page, pageSize, orderBy, orderAsc, filterBy } = this.state.editionList[pieceId]; + // trigger loading animation this.setState({ showMoreLoading: true }); - let editionList = this.state.editionList[this.props.parentId]; - EditionListActions.fetchEditionList(this.props.parentId, editionList.page + 1, editionList.pageSize, - editionList.orderBy, editionList.orderAsc, editionList.filterBy); + EditionListActions.fetchEditionList({ + pieceId, + pageSize, + orderBy, + orderAsc, + filterBy, + page: page + 1 + }); }, render() { + const { className, parentId } = this.props; + const { editionList, isEditionListOpenForPieceId, showMoreLoading } = this.state; + const editionsForPiece = editionList[parentId]; + let selectedEditionsCount = 0; let allEditionsCount = 0; let orderBy; @@ -89,95 +101,97 @@ let AccordionListItemTableEditions = React.createClass({ let show = false; let showExpandOption = false; - let editionsForPiece = this.state.editionList[this.props.parentId]; - let loadingSpinner = ; - // here we need to check if all editions of a specific // piece are already defined. Otherwise .length will throw an error and we'll not // be notified about it. - if(editionsForPiece) { + if (editionsForPiece) { selectedEditionsCount = this.filterSelectedEditions().length; allEditionsCount = editionsForPiece.length; orderBy = editionsForPiece.orderBy; orderAsc = editionsForPiece.orderAsc; } - if(this.props.parentId in this.state.isEditionListOpenForPieceId) { - show = this.state.isEditionListOpenForPieceId[this.props.parentId].show; + if (parentId in isEditionListOpenForPieceId) { + show = isEditionListOpenForPieceId[parentId].show; } // if the number of editions in the array is equal to the maximum number of editions, // then the "Show me more" dialog should be hidden from the user's view - if(editionsForPiece && editionsForPiece.count > editionsForPiece.length) { + if (editionsForPiece && editionsForPiece.count > editionsForPiece.length) { showExpandOption = true; } - let transition = new TransitionModel('editions', 'editionId', 'bitcoin_id', (e) => e.stopPropagation() ); + const transition = new TransitionModel({ + to: 'editions', + queryKey: 'editionId', + valueKey: 'bitcoin_id', + callback: (e) => e.stopPropagation() + }); - let columnList = [ - new ColumnModel( - (item) => { + const columnList = [ + new ColumnModel({ + transformFn: (item) => { return { 'editionId': item.id, - 'pieceId': this.props.parentId, + 'pieceId': parentId, 'selectItem': this.selectItem, 'selected': item.selected - }; }, - '', + }; + }, + displayElement: ( , - TableItemCheckbox, - 1, - false - ), - new ColumnModel( - (item) => { + numOfAllEditions={allEditionsCount}/> + ), + displayType: TableItemCheckbox, + rowWidth: 1 + }), + new ColumnModel({ + transition, + transformFn: (item) => { return { 'content': item.edition_number + ' ' + getLangText('of') + ' ' + item.num_editions - }; }, - 'edition_number', - getLangText('Edition'), - TableItemText, - 1, - false, - transition - ), - new ColumnModel( - (item) => { + }; + }, + columnName: 'edition_number', + displayElement: getLangText('Edition'), + displayType: TableItemText, + rowWidth: 1 + }), + new ColumnModel({ + transition, + transformFn: (item) => { return { 'content': item.bitcoin_id - }; }, - 'bitcoin_id', - getLangText('ID'), - TableItemText, - 5, - false, - transition, - 'hidden-xs visible-sm visible-md visible-lg' - ), - new ColumnModel( - (item) => { - let content = item.acl; + }; + }, + columnName: 'bitcoin_id', + displayElement: getLangText('ID'), + displayType: TableItemText, + rowWidth: 5, + className: 'hidden-xs visible-sm visible-md visible-lg' + }), + new ColumnModel({ + transition, + transformFn: (item) => { return { - 'content': content, + 'content': item.acl, 'notifications': item.notifications - }; }, - 'acl', - getLangText('Actions'), - TableItemAclFiltered, - 4, - false, - transition - ) + }; + }, + columnName: 'acl', + displayElement: getLangText('Actions'), + displayType: TableItemAclFiltered, + rowWidth: 4 + }) ]; - if(show && editionsForPiece && editionsForPiece.length > 0) { + if (show && editionsForPiece && editionsForPiece.length) { return ( -
+
{this.state.showMoreLoading ? loadingSpinner : : null} /> + message={show && showExpandOption ? ( + + {showMoreLoading ? + : + ) : null + } />
); } else { 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 a8cab166..f6712d37 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -88,11 +88,12 @@ let AccordionListItemWallet = React.createClass({ }, onPollingSuccess(pieceId) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); EditionListActions.toggleEditionList(pieceId); - let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); + const notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, diff --git a/js/components/ascribe_buttons/create_editions_button.js b/js/components/ascribe_buttons/create_editions_button.js index 08fb76ce..c78b0d63 100644 --- a/js/components/ascribe_buttons/create_editions_button.js +++ b/js/components/ascribe_buttons/create_editions_button.js @@ -28,6 +28,12 @@ let CreateEditionsButton = React.createClass({ EditionListStore.listen(this.onChange); }, + componentDidUpdate() { + if(this.props.piece.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') { + this.startPolling(); + } + }, + componentWillUnmount() { EditionListStore.unlisten(this.onChange); clearInterval(this.state.pollingIntervalIndex); @@ -37,28 +43,24 @@ let CreateEditionsButton = React.createClass({ this.setState(state); }, - componentDidUpdate() { - if(this.props.piece.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') { - this.startPolling(); - } - }, - startPolling() { // start polling until editions are defined let pollingIntervalIndex = setInterval(() => { // requests, will try to merge the filterBy parameter with other parameters (mergeOptions). // Therefore it can't but null but instead has to be an empty object - EditionListActions.fetchEditionList(this.props.piece.id, null, null, null, null, {}) - .then((res) => { - - clearInterval(this.state.pollingIntervalIndex); - this.props.onPollingSuccess(this.props.piece.id, res.editions[0].num_editions); - - }) - .catch((err) => { - /* Ignore and keep going */ - }); + EditionListActions + .fetchEditionList({ + pieceId: this.props.piece.id, + filterBy: {} + }) + .then((res) => { + clearInterval(this.state.pollingIntervalIndex); + this.props.onPollingSuccess(this.props.piece.id, res.editions[0].num_editions); + }) + .catch((err) => { + /* Ignore and keep going */ + }); }, 5000); this.setState({ diff --git a/js/components/ascribe_detail/detail_property.js b/js/components/ascribe_detail/detail_property.js index 8b0f50b5..0191ffa9 100644 --- a/js/components/ascribe_detail/detail_property.js +++ b/js/components/ascribe_detail/detail_property.js @@ -1,9 +1,10 @@ 'use strict'; import React from 'react'; +import classNames from 'classnames'; -let DetailProperty = React.createClass({ +const DetailProperty = React.createClass({ propTypes: { label: React.PropTypes.string, value: React.PropTypes.oneOfType([ @@ -12,6 +13,7 @@ let DetailProperty = React.createClass({ React.PropTypes.element ]), separator: React.PropTypes.string, + className: React.PropTypes.string, labelClassName: React.PropTypes.string, valueClassName: React.PropTypes.string, ellipsis: React.PropTypes.bool, @@ -30,31 +32,23 @@ let DetailProperty = React.createClass({ }, render() { - let styles = {}; - const { labelClassName, - label, - separator, - valueClassName, - children, - value } = this.props; - - if(this.props.ellipsis) { - styles = { - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis' - }; - } + const { + children, + className, + ellipsis, + label, + labelClassName, + separator, + valueClassName, + value } = this.props; return ( -
+
{label} {separator}
-
+
{children || value}
diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 068b526c..803d73bb 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -1,7 +1,7 @@ 'use strict'; import React from 'react'; -import { Link, History } from 'react-router'; +import { Link } from 'react-router'; import Moment from 'moment'; import Row from 'react-bootstrap/lib/Row'; @@ -16,7 +16,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 DetailProperty from './detail_property'; import LicenseDetail from './license_detail'; import FurtherDetails from './further_details'; @@ -44,8 +44,6 @@ let Edition = React.createClass({ loadEdition: React.PropTypes.func }, - mixins: [History], - getDefaultProps() { return { furtherDetailsType: FurtherDetails @@ -53,98 +51,103 @@ let Edition = React.createClass({ }, render() { - let FurtherDetailsType = this.props.furtherDetailsType; + const { + actionPanelButtonListType, + coaError, + currentUser, + edition, + furtherDetailsType: FurtherDetailsType, + loadEdition } = this.props; return ( - + + content={edition} + currentUser={currentUser} /> - +
-
-

{this.props.edition.title}

- - +
+

{edition.title}

+ +
+ actionPanelButtonListType={actionPanelButtonListType} + edition={edition} + currentUser={currentUser} + handleSuccess={loadEdition}/> + show={edition.acl.acl_coa === true}> + coa={edition.coa} + coaError={coaError} + editionId={edition.bitcoin_id}/> 0}> + show={edition.ownership_history && edition.ownership_history.length > 0}> + history={edition.ownership_history} /> 0}> + show={edition.consign_history && edition.consign_history.length > 0}> + history={edition.consign_history} /> 0}> + show={edition.loan_history && edition.loan_history.length > 0}> + history={edition.loan_history} /> + show={!!(currentUser.username || edition.acl.acl_edit || edition.public_note)}> {return {'bitcoin_id': this.props.edition.bitcoin_id}; }} + id={() => {return {'bitcoin_id': edition.bitcoin_id}; }} label={getLangText('Personal note (private)')} - defaultValue={this.props.edition.private_note ? this.props.edition.private_note : null} + defaultValue={edition.private_note ? edition.private_note : null} placeholder={getLangText('Enter your comments ...')} editable={true} successMessage={getLangText('Private note saved')} url={ApiUrls.note_private_edition} - currentUser={this.props.currentUser}/> + currentUser={currentUser}/> {return {'bitcoin_id': this.props.edition.bitcoin_id}; }} + id={() => {return {'bitcoin_id': edition.bitcoin_id}; }} label={getLangText('Personal note (public)')} - defaultValue={this.props.edition.public_note ? this.props.edition.public_note : null} + defaultValue={edition.public_note ? edition.public_note : null} placeholder={getLangText('Enter your comments ...')} - editable={!!this.props.edition.acl.acl_edit} - show={!!this.props.edition.public_note || !!this.props.edition.acl.acl_edit} + editable={!!edition.acl.acl_edit} + show={!!edition.public_note || !!edition.acl.acl_edit} successMessage={getLangText('Public edition note saved')} url={ApiUrls.note_public_edition} - currentUser={this.props.currentUser}/> + currentUser={currentUser}/> 0 - || this.props.edition.other_data.length > 0}> + show={edition.acl.acl_edit || + Object.keys(edition.extra_data).length > 0 || + edition.other_data.length > 0}> + editable={edition.acl.acl_edit} + pieceId={edition.parent} + extraData={edition.extra_data} + otherData={edition.other_data} + handleSuccess={loadEdition} /> + edition={edition} />
@@ -169,10 +172,10 @@ let EditionSummary = React.createClass({ let status = null; if (this.props.edition.status.length > 0){ let statusStr = this.props.edition.status.join(', ').replace(/_/g, ' '); - status = ; + status = ; if (this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer){ status = ( - + ); } } @@ -183,14 +186,14 @@ let EditionSummary = React.createClass({ let { actionPanelButtonListType, edition, currentUser } = this.props; return (
- - - @@ -201,14 +204,15 @@ let EditionSummary = React.createClass({ `AclInformation` would show up */} 1}> - + - +
@@ -220,66 +224,76 @@ let EditionSummary = React.createClass({ let CoaDetails = React.createClass({ propTypes: { editionId: React.PropTypes.string, - coa: React.PropTypes.object, + coa: React.PropTypes.oneOfType([ + React.PropTypes.number, + React.PropTypes.string, + React.PropTypes.object + ]), coaError: React.PropTypes.object }, contactOnIntercom() { - window.Intercom('showNewMessage', `Hi, I'm having problems generating a Certificate of Authenticity for Edition: ${this.props.editionId}`); - console.logGlobal(new Error(`Coa couldn't be created for edition: ${this.props.editionId}`)); + const { coaError, editionId } = this.props; + + window.Intercom('showNewMessage', getLangText("Hi, I'm having problems generating a Certificate of Authenticity for Edition: %s", editionId)); + console.logGlobal(new Error(`Coa couldn't be created for edition: ${editionId}`), coaError); }, render() { - if(this.props.coaError) { - return ( -
-

{getLangText('There was an error generating your Certificate of Authenticity.')}

-

- {getLangText('Try to refresh the page. If this happens repeatedly, please ')} - {getLangText('contact us')}. -

-
- ); - } - if(this.props.coa && this.props.coa.url_safe) { - return ( -
-
- -
-
- - - - - - + const { coa, coaError } = this.props; + let coaDetailElement; -
+ if (coaError) { + coaDetailElement = [ +

{getLangText('There was an error generating your Certificate of Authenticity.')}

, +

+ {getLangText('Try to refresh the page. If this happens repeatedly, please ')} + {getLangText('contact us')}. +

+ ]; + } else if (coa && coa.url_safe) { + coaDetailElement = [ +
+ +
, +
+ + + + + +
- ); - } else if(typeof this.props.coa === 'string'){ - return ( -
- {this.props.coa} -
- ); - } - return ( -
- -

{getLangText("Just a sec, we\'re generating your COA")}

+ ]; + } else if (typeof coa === 'string') { + coaDetailElement = coa; + } else { + coaDetailElement = [ + , +

{getLangText("Just a sec, we're generating your COA")}

,

{getLangText('(you may leave the page)')}

+ ]; + } + + return ( +
+
+ {coaDetailElement} +
+ {/* Hide the COA and just show that it's a seperate document when printing */} +
+ {getLangText('The COA is available as a seperate document')} +
); } @@ -291,16 +305,34 @@ let SpoolDetails = React.createClass({ }, render() { - let bitcoinIdValue = ( - {this.props.edition.bitcoin_id} + const { edition: { + bitcoin_id: bitcoinId, + hash_as_address: hashAsAddress, + btc_owner_address_noprefix: bitcoinOwnerAddress + } } = this.props; + + const bitcoinIdValue = ( + + {bitcoinId} + ); - let hashOfArtwork = ( - {this.props.edition.hash_as_address} + const hashOfArtwork = ( + + {hashAsAddress} + ); - let ownerAddress = ( - {this.props.edition.btc_owner_address_noprefix} + const ownerAddress = ( + + {bitcoinOwnerAddress} + ); return ( diff --git a/js/components/ascribe_detail/edition_action_panel.js b/js/components/ascribe_detail/edition_action_panel.js index 36a79e7c..71bf38fe 100644 --- a/js/components/ascribe_detail/edition_action_panel.js +++ b/js/components/ascribe_detail/edition_action_panel.js @@ -72,19 +72,20 @@ let EditionActionPanel = React.createClass({ EditionListActions.closeAllEditionLists(); EditionListActions.clearAllEditionSelections(); - let notification = new GlobalNotificationModel(response.notification, 'success'); + const notification = new GlobalNotificationModel(response.notification, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); - this.history.pushState(null, '/collection'); + this.history.push('/collection'); }, 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}); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); + EditionListActions.refreshEditionList({ pieceId: this.props.edition.parent }); }, - handleSuccess(response){ + handleSuccess(response) { this.refreshCollection(); this.props.handleSuccess(); if (response){ @@ -93,7 +94,7 @@ let EditionActionPanel = React.createClass({ } }, - render(){ + render() { const { actionPanelButtonListType: ActionPanelButtonListType, edition, diff --git a/js/components/ascribe_detail/edition_container.js b/js/components/ascribe_detail/edition_container.js index ee53f0e1..d0adadf0 100644 --- a/js/components/ascribe_detail/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -35,7 +35,7 @@ let EditionContainer = React.createClass({ getInitialState() { return mergeOptions( - EditionStore.getState(), + EditionStore.getInitialState(), UserStore.getState() ); }, @@ -44,27 +44,23 @@ let EditionContainer = React.createClass({ EditionStore.listen(this.onChange); UserStore.listen(this.onChange); - // Every time we're entering the edition detail page, - // just reset the edition that is saved in the edition store - // as it will otherwise display wrong/old data once the user loads - // the edition detail a second time - EditionActions.flushEdition(); - EditionActions.fetchEdition(this.props.params.editionId); - + this.loadEdition(); UserActions.fetchCurrentUser(); }, // 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.fetchEdition(this.props.params.editionId); + if (this.props.params.editionId !== nextProps.params.editionId) { + EditionActions.flushEdition(); + this.loadEdition(nextProps.params.editionId); } }, componentDidUpdate() { - const { editionMeta } = this.state; - if(editionMeta.err && editionMeta.err.json && editionMeta.err.json.status === 404) { + const { err: editionErr } = this.state.editionMeta; + + if (editionErr && editionErr.json && editionErr.json.status === 404) { this.throws(new ResourceNotFoundError(getLangText("Oops, the edition you're looking for doesn't exist."))); } }, @@ -81,18 +77,22 @@ let EditionContainer = React.createClass({ if(state && state.edition && state.edition.digital_work) { let isEncoding = state.edition.digital_work.isEncoding; if (state.edition.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { - let timerId = window.setInterval(() => EditionActions.fetchOne(this.props.params.editionId), 10000); + let timerId = window.setInterval(() => EditionActions.fetchEdition(this.props.params.editionId), 10000); this.setState({timerId: timerId}); } } }, + loadEdition(editionId = this.props.params.editionId) { + EditionActions.fetchEdition(editionId); + }, + render() { const { edition, currentUser, coaMeta } = this.state; const { actionPanelButtonListType, furtherDetailsType } = this.props; - if (Object.keys(edition).length && edition.id) { - setDocumentTitle([edition.artist_name, edition.title].join(', ')); + if (edition.id) { + setDocumentTitle(`${edition.artist_name}, ${edition.title}`); return ( EditionActions.fetchEdition(this.props.params.editionId)} /> + loadEdition={this.loadEdition} /> ); } else { return ( diff --git a/js/components/ascribe_detail/further_details.js b/js/components/ascribe_detail/further_details.js index c178fb93..b1d9c637 100644 --- a/js/components/ascribe_detail/further_details.js +++ b/js/components/ascribe_detail/further_details.js @@ -5,25 +5,27 @@ import React from 'react'; import Row from 'react-bootstrap/lib/Row'; import Col from 'react-bootstrap/lib/Col'; -import Form from './../ascribe_forms/form'; - -import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata'; - import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; import FurtherDetailsFileuploader from './further_details_fileuploader'; +import Form from './../ascribe_forms/form'; +import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata'; + import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; +import { getLangText } from '../../utils/lang_utils'; + let FurtherDetails = React.createClass({ propTypes: { + pieceId: React.PropTypes.number.isRequired, + editable: React.PropTypes.bool, - pieceId: React.PropTypes.number, extraData: React.PropTypes.object, + handleSuccess: React.PropTypes.func, otherData: React.PropTypes.arrayOf(React.PropTypes.object), - handleSuccess: React.PropTypes.func }, getInitialState() { @@ -32,13 +34,18 @@ let FurtherDetails = React.createClass({ }; }, - showNotification(){ - this.props.handleSuccess(); - let notification = new GlobalNotificationModel('Details updated', 'success'); + showNotification() { + const { handleSuccess } = this.props; + + if (typeof handleSucess === 'function') { + handleSuccess(); + } + + const notification = new GlobalNotificationModel(getLangText('Details updated'), 'success'); GlobalNotificationActions.appendGlobalNotification(notification); }, - submitFile(file){ + submitFile(file) { this.setState({ otherDataKey: file.key }); @@ -51,40 +58,42 @@ let FurtherDetails = React.createClass({ }, render() { + const { editable, extraData, otherData, pieceId } = this.props; + return ( + pieceId={pieceId} /> + pieceId={pieceId} /> + pieceId={pieceId} />
diff --git a/js/components/ascribe_detail/further_details_fileuploader.js b/js/components/ascribe_detail/further_details_fileuploader.js index b044bbbc..8546e43f 100644 --- a/js/components/ascribe_detail/further_details_fileuploader.js +++ b/js/components/ascribe_detail/further_details_fileuploader.js @@ -20,13 +20,16 @@ let FurtherDetailsFileuploader = React.createClass({ otherData: React.PropTypes.arrayOf(React.PropTypes.object), setIsUploadReady: React.PropTypes.func, submitFile: React.PropTypes.func, + onValidationFailed: React.PropTypes.func, isReadyForFormSubmission: React.PropTypes.func, editable: React.PropTypes.bool, - multiple: React.PropTypes.bool + multiple: React.PropTypes.bool, + areAssetsDownloadable: React.PropTypes.bool }, getDefaultProps() { return { + areAssetsDownloadable: true, label: getLangText('Additional files'), multiple: false }; @@ -60,6 +63,7 @@ let FurtherDetailsFileuploader = React.createClass({ }} validation={AppConstants.fineUploader.validation.additionalData} submitFile={this.props.submitFile} + onValidationFailed={this.props.onValidationFailed} setIsUploadReady={this.props.setIsUploadReady} isReadyForFormSubmission={this.props.isReadyForFormSubmission} session={{ @@ -89,7 +93,7 @@ let FurtherDetailsFileuploader = React.createClass({ 'X-CSRFToken': getCookie(AppConstants.csrftoken) } }} - areAssetsDownloadable={true} + areAssetsDownloadable={this.props.areAssetsDownloadable} areAssetsEditable={this.props.editable} multiple={this.props.multiple} /> diff --git a/js/components/ascribe_detail/history_iterator.js b/js/components/ascribe_detail/history_iterator.js index 413aeb21..03904863 100644 --- a/js/components/ascribe_detail/history_iterator.js +++ b/js/components/ascribe_detail/history_iterator.js @@ -22,7 +22,11 @@ let HistoryIterator = React.createClass({ return ( {historicalEventDescription} - {contractName} + + {contractName} + ); } else if(historicalEvent.length === 2) { diff --git a/js/components/ascribe_detail/media_container.js b/js/components/ascribe_detail/media_container.js index c6845a44..e4270132 100644 --- a/js/components/ascribe_detail/media_container.js +++ b/js/components/ascribe_detail/media_container.js @@ -14,10 +14,6 @@ import CollapsibleButton from './../ascribe_collapsible/collapsible_button'; import AclProxy from '../acl_proxy'; -import UserActions from '../../actions/user_actions'; -import UserStore from '../../stores/user_store'; - -import { mergeOptions } from '../../utils/general_utils.js'; import { getLangText } from '../../utils/lang_utils.js'; const EMBED_IFRAME_HEIGHT = { @@ -28,25 +24,22 @@ const EMBED_IFRAME_HEIGHT = { let MediaContainer = React.createClass({ propTypes: { content: React.PropTypes.object, + currentUser: React.PropTypes.object, refreshObject: React.PropTypes.func }, getInitialState() { - return mergeOptions( - UserStore.getState(), - { - timerId: null - }); + return { + timerId: null + }; }, componentDidMount() { - UserStore.listen(this.onChange); - UserActions.fetchCurrentUser(); - if (!this.props.content.digital_work) { return; } - let isEncoding = this.props.content.digital_work.isEncoding; + + const isEncoding = this.props.content.digital_work.isEncoding; if (this.props.content.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { let timerId = window.setInterval(this.props.refreshObject, 10000); this.setState({timerId: timerId}); @@ -60,22 +53,16 @@ let MediaContainer = React.createClass({ }, componentWillUnmount() { - UserStore.unlisten(this.onChange); - window.clearInterval(this.state.timerId); }, - onChange(state) { - this.setState(state); - }, - render() { - const { content } = this.props; + const { content, currentUser } = this.props; // Pieces and editions are joined to the user by a foreign key in the database, so // the information in content will be updated if a user updates their username. // We also force uniqueness of usernames, so this check is safe to dtermine if the // content was registered by the current user. - const didUserRegisterContent = this.state.currentUser && (this.state.currentUser.username === content.user_registered); + const didUserRegisterContent = currentUser && (currentUser.username === content.user_registered); let thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ? content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe; @@ -94,8 +81,11 @@ let MediaContainer = React.createClass({ embed = ( - Embed + } panel={ @@ -114,7 +104,7 @@ let MediaContainer = React.createClass({ url={content.digital_work.url} extraData={extraData} encodingStatus={content.digital_work.isEncoding} /> -

+

- {embed} diff --git a/js/components/ascribe_detail/piece.js b/js/components/ascribe_detail/piece.js index 7481d1fb..e4ff4ea7 100644 --- a/js/components/ascribe_detail/piece.js +++ b/js/components/ascribe_detail/piece.js @@ -16,10 +16,10 @@ import MediaContainer from './media_container'; let Piece = React.createClass({ propTypes: { piece: React.PropTypes.object, + currentUser: React.PropTypes.object, header: React.PropTypes.object, subheader: React.PropTypes.object, buttons: React.PropTypes.object, - loadPiece: React.PropTypes.func, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -28,24 +28,26 @@ let Piece = React.createClass({ updateObject() { - return PieceActions.fetchOne(this.props.piece.id); + return PieceActions.fetchPiece(this.props.piece.id); }, render() { + const { buttons, children, currentUser, header, piece, subheader } = this.props; + return ( - + + content={piece} + currentUser={currentUser} + refreshObject={this.updateObject} /> - - {this.props.header} - {this.props.subheader} - {this.props.buttons} - - {this.props.children} + + {header} + {subheader} + {buttons} + {children} ); diff --git a/js/components/ascribe_detail/piece_container.js b/js/components/ascribe_detail/piece_container.js index 7dfc9570..f7ef024f 100644 --- a/js/components/ascribe_detail/piece_container.js +++ b/js/components/ascribe_detail/piece_container.js @@ -69,7 +69,7 @@ let PieceContainer = React.createClass({ return mergeOptions( UserStore.getState(), PieceListStore.getState(), - PieceStore.getState(), + PieceStore.getInitialState(), { showCreateEditionsDialog: false } @@ -81,19 +81,24 @@ let PieceContainer = React.createClass({ PieceListStore.listen(this.onChange); PieceStore.listen(this.onChange); - // Every time we enter the piece detail page, just reset the piece - // store as it will otherwise display wrong/old data once the user loads - // the piece detail a second time - PieceActions.updatePiece({}); - this.loadPiece(); UserActions.fetchCurrentUser(); }, - componentDidUpdate() { - const { pieceError } = this.state; + // 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) or + // when the user clicks on a notification while being in another piece view + componentWillReceiveProps(nextProps) { + if (this.props.params.pieceId !== nextProps.params.pieceId) { + PieceActions.flushPiece(); + this.loadPiece(nextProps.params.pieceId); + } + }, - if (pieceError && pieceError.status === 404) { + componentDidUpdate() { + const { err: pieceErr } = this.state.pieceMeta; + + if (pieceErr && pieceErr.json && pieceErr.json.status === 404) { this.throws(new ResourceNotFoundError(getLangText("Oops, the piece you're looking for doesn't exist."))); } }, @@ -116,8 +121,7 @@ let PieceContainer = React.createClass({ ALSO, WE ENABLED THE LOAN BUTTON FOR IKONOTV TO LET THEM LOAN ON A PIECE LEVEL */ - if(state && state.piece && state.piece.acl && typeof state.piece.acl.acl_loan !== 'undefined') { - + if (state && state.piece && state.piece.acl && typeof state.piece.acl.acl_loan !== 'undefined') { let pieceState = mergeOptions({}, state.piece); pieceState.acl.acl_loan = false; this.setState({ @@ -129,11 +133,10 @@ let PieceContainer = React.createClass({ } }, - loadPiece() { - PieceActions.fetchOne(this.props.params.pieceId); + loadPiece(pieceId = this.props.params.pieceId) { + PieceActions.fetchPiece(pieceId); }, - toggleCreateEditionsDialog() { this.setState({ showCreateEditionsDialog: !this.state.showCreateEditionsDialog @@ -141,43 +144,47 @@ let PieceContainer = React.createClass({ }, handleEditionCreationSuccess() { - PieceActions.updateProperty({key: 'num_editions', value: 0}); - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceActions.updateProperty({ key: 'num_editions', value: 0 }); + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); + this.toggleCreateEditionsDialog(); }, handleDeleteSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); // since we're deleting a piece, we just need to close // all editions dialogs and not reload them EditionListActions.closeAllEditionLists(); EditionListActions.clearAllEditionSelections(); - let notification = new GlobalNotificationModel(response.notification, 'success'); + const notification = new GlobalNotificationModel(response.notification, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); - this.history.pushState(null, '/collection'); + this.history.push('/collection'); }, getCreateEditionsDialog() { - if(this.state.piece.num_editions < 1 && this.state.showCreateEditionsDialog) { + if (this.state.piece.num_editions < 1 && this.state.showCreateEditionsDialog) { return (

-
+
); } else { - return (
); + return (
); } }, handlePollingSuccess(pieceId, numEditions) { + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; // we need to refresh the num_editions property of the actual piece we're looking at PieceActions.updateProperty({ @@ -189,27 +196,26 @@ let PieceContainer = React.createClass({ // btw.: It's not sufficient to just set num_editions to numEditions, since a single accordion // list item also uses the firstEdition property which we can only get from the server in that case. // Therefore we need to at least refetch the changed piece from the server or on our case simply all - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); - let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); + const notification = new GlobalNotificationModel(getLangText('Editions successfully created'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, getId() { - return {'id': this.state.piece.id}; + return { 'id': this.state.piece.id }; }, getActions() { const { piece, currentUser } = this.state; - if (piece && piece.notifications && piece.notifications.length > 0) { + if (piece.notifications && piece.notifications.length > 0) { return ( ); + notifications={piece.notifications} />); } else { return ( + + piece={piece} /> + aclObject={piece.acl} /> @@ -247,76 +255,76 @@ let PieceContainer = React.createClass({ }, render() { - if (this.state.piece && this.state.piece.id) { - let FurtherDetailsType = this.props.furtherDetailsType; - setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', ')); + const { furtherDetailsType: FurtherDetailsType } = this.props; + const { currentUser, piece } = this.state; + + if (piece.id) { + setDocumentTitle(`${piece.artist_name}, ${piece.title}`); return ( -
-

{this.state.piece.title}

- - - {this.state.piece.num_editions > 0 ? : null} +
+

{piece.title}

+ + + {piece.num_editions > 0 ? : null}
} subheader={
- - - + + +
} buttons={this.getActions()}> {this.getCreateEditionsDialog()} 0}> + show={piece.loan_history && piece.loan_history.length > 0}> + history={piece.loan_history} /> + show={!!(currentUser.username || piece.acl.acl_edit || piece.public_note)}> + currentUser={currentUser} /> + currentUser={currentUser} /> 0 - || this.state.piece.other_data.length > 0} + show={piece.acl.acl_edit + || Object.keys(piece.extra_data).length > 0 + || piece.other_data.length > 0} defaultExpanded={true}> diff --git a/js/components/ascribe_forms/create_editions_form.js b/js/components/ascribe_forms/create_editions_form.js index 1e8ac23e..b2bf3c1c 100644 --- a/js/components/ascribe_forms/create_editions_form.js +++ b/js/components/ascribe_forms/create_editions_form.js @@ -19,7 +19,7 @@ let CreateEditionsForm = React.createClass({ pieceId: React.PropTypes.number }, - getFormData(){ + getFormData() { return { piece_id: parseInt(this.props.pieceId, 10) }; @@ -58,11 +58,12 @@ let CreateEditionsForm = React.createClass({ + min={1} + max={100} /> ); } }); -export default CreateEditionsForm; \ No newline at end of file +export default CreateEditionsForm; diff --git a/js/components/ascribe_forms/form.js b/js/components/ascribe_forms/form.js index d4002e85..dc870d5a 100644 --- a/js/components/ascribe_forms/form.js +++ b/js/components/ascribe_forms/form.js @@ -178,20 +178,20 @@ let Form = React.createClass({ let formData = this.getFormData(); // sentry shouldn't post the user's password - if(formData.password) { + if (formData.password) { delete formData.password; } console.logGlobal(err, formData); - if(this.props.isInline) { + if (this.props.isInline) { let notification = new GlobalNotificationModel(getLangText('Something went wrong, please try again later'), 'danger'); GlobalNotificationActions.appendGlobalNotification(notification); } else { this.setState({errors: [getLangText('Something went wrong, please try again later')]}); } - } + this.setState({submitted: false}); }, @@ -205,16 +205,15 @@ let Form = React.createClass({ }, getButtons() { - if (this.state.submitted){ + if (this.state.submitted) { return this.props.spinner; } - if (this.props.buttons){ + if (this.props.buttons !== undefined) { return this.props.buttons; } - let buttons = null; - if (this.state.edited && !this.props.disabled){ - buttons = ( + if (this.state.edited && !this.props.disabled) { + return (

); - + } else { + return null; } - return buttons; }, getErrors() { diff --git a/js/components/ascribe_forms/form_piece_extradata.js b/js/components/ascribe_forms/form_piece_extradata.js index f6ee4177..9ba53f3b 100644 --- a/js/components/ascribe_forms/form_piece_extradata.js +++ b/js/components/ascribe_forms/form_piece_extradata.js @@ -13,43 +13,49 @@ import InputTextAreaToggable from './input_textarea_toggable'; let PieceExtraDataForm = React.createClass({ propTypes: { - pieceId: React.PropTypes.number, + name: React.PropTypes.string.isRequired, + pieceId: React.PropTypes.number.isRequired, + + convertLinks: React.PropTypes.bool, + editable: React.PropTypes.bool, extraData: React.PropTypes.object, handleSuccess: React.PropTypes.func, name: React.PropTypes.string, - title: React.PropTypes.string, - editable: React.PropTypes.bool + title: React.PropTypes.string }, getFormData() { - let extradata = {}; - extradata[this.props.name] = this.refs.form.refs[this.props.name].state.value; return { - extradata: extradata, + extradata: { + [this.props.name]: this.refs.form.refs[this.props.name].state.value + }, piece_id: this.props.pieceId }; }, - + render() { - let defaultValue = this.props.extraData[this.props.name] || ''; - if (defaultValue.length === 0 && !this.props.editable){ + const { convertLinks, editable, extraData, handleSuccess, name, pieceId, title } = this.props; + const defaultValue = (extraData && extraData[name]) || null; + + if (!defaultValue && !editable) { return null; } - let url = requests.prepareUrl(ApiUrls.piece_extradata, {piece_id: this.props.pieceId}); + return (
+ handleSuccess={handleSuccess} + url={requests.prepareUrl(ApiUrls.piece_extradata, { piece_id: pieceId })}> + name={name} + label={title}>
diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index 83d38b50..596f8a56 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -109,6 +109,11 @@ let RegisterPieceForm = React.createClass({ ); }, + handleThumbnailValidationFailed(thumbnailFile) { + // If the validation fails, set the thumbnail as submittable since its optional + this.refs.submitButton.setReadyStateForKey('thumbnailKeyReady', true); + }, + isThumbnailDialogExpanded() { const { enableSeparateThumbnail } = this.props; const { digitalWorkFile } = this.state; @@ -194,14 +199,15 @@ let RegisterPieceForm = React.createClass({ url: ApiUrls.blob_thumbnails }} handleChangedFile={this.handleChangedThumbnail} + onValidationFailed={this.handleThumbnailValidationFailed} isReadyForFormSubmission={formSubmissionValidation.fileOptional} keyRoutine={{ url: AppConstants.serverUrl + 's3/key/', fileClass: 'thumbnail' }} validation={{ - itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit, - sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit, + itemLimit: AppConstants.fineUploader.validation.workThumbnail.itemLimit, + sizeLimit: AppConstants.fineUploader.validation.workThumbnail.sizeLimit, allowedExtensions: ['png', 'jpg', 'jpeg', 'gif'] }} setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')} diff --git a/js/components/ascribe_forms/form_send_contract_agreement.js b/js/components/ascribe_forms/form_send_contract_agreement.js index 6f5f74d7..043c0361 100644 --- a/js/components/ascribe_forms/form_send_contract_agreement.js +++ b/js/components/ascribe_forms/form_send_contract_agreement.js @@ -58,7 +58,7 @@ let SendContractAgreementForm = React.createClass({ notification = new GlobalNotificationModel(notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); - this.history.pushState(null, '/collection'); + this.history.push('/collection'); }, getFormData() { diff --git a/js/components/ascribe_forms/input_fineuploader.js b/js/components/ascribe_forms/input_fineuploader.js index db5bae05..fa9c72b6 100644 --- a/js/components/ascribe_forms/input_fineuploader.js +++ b/js/components/ascribe_forms/input_fineuploader.js @@ -52,6 +52,7 @@ const InputFineUploader = React.createClass({ plural: string }), handleChangedFile: func, + onValidationFailed: func, // Provided by `Property` onChange: React.PropTypes.func @@ -107,6 +108,7 @@ const InputFineUploader = React.createClass({ isFineUploaderActive, isReadyForFormSubmission, keyRoutine, + onValidationFailed, setIsUploadReady, uploadMethod, validation, @@ -127,6 +129,7 @@ const InputFineUploader = React.createClass({ createBlobRoutine={createBlobRoutine} validation={validation} submitFile={this.submitFile} + onValidationFailed={onValidationFailed} setIsUploadReady={setIsUploadReady} isReadyForFormSubmission={isReadyForFormSubmission} areAssetsDownloadable={areAssetsDownloadable} diff --git a/js/components/ascribe_forms/input_textarea_toggable.js b/js/components/ascribe_forms/input_textarea_toggable.js index 0be8b87a..05a1f011 100644 --- a/js/components/ascribe_forms/input_textarea_toggable.js +++ b/js/components/ascribe_forms/input_textarea_toggable.js @@ -4,17 +4,20 @@ import React from 'react'; import TextareaAutosize from 'react-textarea-autosize'; +import { anchorize } from '../../utils/dom_utils'; + let InputTextAreaToggable = React.createClass({ propTypes: { autoFocus: React.PropTypes.bool, - disabled: React.PropTypes.bool, - rows: React.PropTypes.number.isRequired, - required: React.PropTypes.bool, + convertLinks: React.PropTypes.bool, defaultValue: React.PropTypes.string, - placeholder: React.PropTypes.string, + disabled: React.PropTypes.bool, onBlur: React.PropTypes.func, - onChange: React.PropTypes.func + onChange: React.PropTypes.func, + placeholder: React.PropTypes.string, + required: React.PropTypes.bool, + rows: React.PropTypes.number.isRequired }, getInitialState() { @@ -36,7 +39,7 @@ let InputTextAreaToggable = React.createClass({ componentDidUpdate() { // If the initial value of state.value is null, we want to set props.defaultValue // as a value. In all other cases TextareaAutosize.onChange is updating.handleChange already - if(this.state.value === null && this.props.defaultValue) { + if (this.state.value === null && this.props.defaultValue) { this.setState({ value: this.props.defaultValue }); @@ -49,28 +52,26 @@ let InputTextAreaToggable = React.createClass({ }, render() { - let className = 'form-control ascribe-textarea'; - let textarea = null; + const { convertLinks, disabled, onBlur, placeholder, required, rows } = this.props; + const { value } = this.state; - if(!this.props.disabled) { - className = className + ' ascribe-textarea-editable'; - textarea = ( + if (!disabled) { + return ( + onBlur={onBlur} + placeholder={placeholder} /> ); } else { - textarea =
{this.state.value}
; + // Can only convert links when not editable, as textarea does not support anchors + return
{convertLinks ? anchorize(value) : value}
; } - - return textarea; } }); diff --git a/js/components/ascribe_forms/property.js b/js/components/ascribe_forms/property.js index 432591bf..6d46a9d3 100644 --- a/js/components/ascribe_forms/property.js +++ b/js/components/ascribe_forms/property.js @@ -240,7 +240,17 @@ const Property = React.createClass({ }, handleCheckboxToggle() { - this.setExpanded(!this.state.expanded); + const expanded = !this.state.expanded; + + this.setExpanded(expanded); + + // Reset the value to be the initial value when the checkbox is unticked since the + // user doesn't want to specify their own value. + if (!expanded) { + this.setState({ + value: this.state.initialValue + }); + } }, renderChildren(style) { diff --git a/js/components/ascribe_media/media_player.js b/js/components/ascribe_media/media_player.js index 2a23f2ed..1552b44c 100644 --- a/js/components/ascribe_media/media_player.js +++ b/js/components/ascribe_media/media_player.js @@ -184,7 +184,7 @@ let Video = React.createClass({ ); } else { return ( - + ); } } diff --git a/js/components/ascribe_modal/modal_wrapper.js b/js/components/ascribe_modal/modal_wrapper.js index 3f3b4af4..511e7f8c 100644 --- a/js/components/ascribe_modal/modal_wrapper.js +++ b/js/components/ascribe_modal/modal_wrapper.js @@ -1,19 +1,20 @@ 'use strict'; import React from 'react'; -import ReactAddons from 'react/addons'; import Modal from 'react-bootstrap/lib/Modal'; let ModalWrapper = React.createClass({ propTypes: { - trigger: React.PropTypes.element, title: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element, React.PropTypes.string ]).isRequired, - handleSuccess: React.PropTypes.func.isRequired, + + handleCancel: React.PropTypes.func, + handleSuccess: React.PropTypes.func, + trigger: React.PropTypes.element, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -38,15 +39,32 @@ let ModalWrapper = React.createClass({ }); }, + handleCancel() { + if (typeof this.props.handleCancel === 'function') { + this.props.handleCancel(); + } + + this.hide(); + }, + handleSuccess(response) { - this.props.handleSuccess(response); + if (typeof this.props.handleSuccess === 'function') { + this.props.handleSuccess(response); + } + this.hide(); }, renderChildren() { - return ReactAddons.Children.map(this.props.children, (child) => { - return ReactAddons.addons.cloneWithProps(child, { - handleSuccess: this.handleSuccess + return React.Children.map(this.props.children, (child) => { + return React.cloneElement(child, { + handleSuccess: (response) => { + if (typeof child.props.handleSuccess === 'function') { + child.props.handleSuccess(response); + } + + this.handleSuccess(response); + } }); }); }, @@ -54,14 +72,23 @@ let ModalWrapper = React.createClass({ render() { const { trigger, title } = this.props; - // If the trigger component exists, we add the ModalWrapper's show() as its onClick method. + // If the trigger component exists, we add the ModalWrapper's show() to its onClick method. // The trigger component should, in most cases, be a button. - const clonedTrigger = React.isValidElement(trigger) ? React.cloneElement(trigger, {onClick: this.show}) - : null; + const clonedTrigger = React.isValidElement(trigger) ? + React.cloneElement(trigger, { + onClick: (...params) => { + if (typeof trigger.props.onClick === 'function') { + trigger.props.onClick(...params); + } + + this.show(); + } + }) : null; + return ( {clonedTrigger} - + {title} diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js index 60370431..bcc15603 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js @@ -39,30 +39,6 @@ let PieceListToolbar = React.createClass({ ]) }, - getFilterWidget(){ - if (this.props.filterParams){ - return ( - - ); - } - return null; - }, - - getOrderWidget(){ - if (this.props.orderParams){ - return ( - - ); - } - return null; - }, - render() { const { className, children, searchFor, searchQuery } = this.props; @@ -75,8 +51,14 @@ let PieceListToolbar = React.createClass({ {children} - {this.getOrderWidget()} - {this.getFilterWidget()} + + 0) { + if (trueValuesOnly.length) { return { visibility: 'visible'}; } else { return { visibility: 'hidden' }; @@ -81,62 +81,66 @@ let PieceListToolbarFilterWidget = React.createClass({ ); - return ( - - {/* We iterate over filterParams, to receive the label and then for each - label also iterate over its items, to get all filterable options */} - {this.props.filterParams.map(({ label, items }, i) => { - return ( -
-
  • - {label}: -
  • - {items.map((param, j) => { + if (this.props.filterParams && this.props.filterParams.length) { + return ( + + {/* We iterate over filterParams, to receive the label and then for each + label also iterate over its items, to get all filterable options */} + {this.props.filterParams.map(({ label, items }, i) => { + return ( +
    +
  • + {label}: +
  • + {items.map((paramItem) => { + let itemLabel; + let param; - // As can be seen in the PropTypes, a param can either - // be a string or an object of the shape: - // - // { - // key: , - // label: - // } - // - // This is why we need to distinguish between both here. - if(typeof param !== 'string') { - label = param.label; - param = param.key; - } else { - param = param; - label = param.split('acl_')[1].replace(/_/g, ' '); - } + // As can be seen in the PropTypes, a param can either + // be a string or an object of the shape: + // + // { + // key: , + // label: + // } + // + // This is why we need to distinguish between both here. + if (typeof paramItem !== 'string') { + param = paramItem.key; + itemLabel = paramItem.label; + } else { + param = paramItem; + itemLabel = paramItem.split('acl_')[1].replace(/_/g, ' '); + } - return ( -
  • -
    - - {getLangText(label)} - - -
    -
  • - ); - })} -
    - ); - })} -
    - ); + return ( +
  • +
    + + {getLangText(itemLabel)} + + +
    +
  • + ); + })} +
    + ); + })} +
    + ); + } else { + return null; + } } }); diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js index c38144b0..5257cc07 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js @@ -37,7 +37,7 @@ let PieceListToolbarOrderWidget = React.createClass({ isOrderActive() { // We're hiding the star in that complicated matter so that, // the surrounding button is not resized up on appearance - if(this.props.orderBy.length > 0) { + if (this.props.orderBy && this.props.orderBy.length) { return { visibility: 'visible'}; } else { return { visibility: 'hidden' }; @@ -51,18 +51,18 @@ let PieceListToolbarOrderWidget = React.createClass({ · ); - return ( - -
  • - {getLangText('Sort by')}: -
  • - {this.props.orderParams.map((param) => { - return ( -
    + if (this.props.orderParams && this.props.orderParams.length) { + return ( + +
  • + {getLangText('Sort by')}: +
  • + {this.props.orderParams.map((param) => { + return (
  • -1} />
  • -
    - ); - })} - - ); + ); + })} + + ); + } else { + return null; + } } }); -export default PieceListToolbarOrderWidget; \ No newline at end of file +export default PieceListToolbarOrderWidget; diff --git a/js/components/ascribe_routes/proxy_handler.js b/js/components/ascribe_routes/proxy_handler.js index 228f0f62..7752912a 100644 --- a/js/components/ascribe_routes/proxy_handler.js +++ b/js/components/ascribe_routes/proxy_handler.js @@ -40,7 +40,7 @@ export function AuthRedirect({to, when}) { // and redirect if `true`. if(exprToValidate) { - window.setTimeout(() => history.replaceState(null, to, query)); + window.setTimeout(() => history.replace({ query, pathname: to })); return true; // Otherwise there can also be the case that the backend @@ -48,7 +48,7 @@ export function AuthRedirect({to, when}) { } else if(!exprToValidate && when === 'loggedIn' && redirect) { delete query.redirect; - window.setTimeout(() => history.replaceState(null, '/' + redirect, query)); + window.setTimeout(() => history.replace({ query, pathname: '/' + redirect })); return true; } else if(!exprToValidate && when === 'loggedOut' && redirectAuthenticated) { diff --git a/js/components/ascribe_settings/contract_settings.js b/js/components/ascribe_settings/contract_settings.js index 71d97542..be723295 100644 --- a/js/components/ascribe_settings/contract_settings.js +++ b/js/components/ascribe_settings/contract_settings.js @@ -28,11 +28,7 @@ import { mergeOptions, truncateTextAtCharIndex } from '../../utils/general_utils let ContractSettings = React.createClass({ - propTypes: { - location: React.PropTypes.object - }, - - getInitialState(){ + getInitialState() { return mergeOptions( ContractListStore.getState(), UserStore.getState() @@ -64,40 +60,39 @@ let ContractSettings = React.createClass({ ContractListActions.removeContract(contract.id) .then((response) => { ContractListActions.fetchContractList(true); - let notification = new GlobalNotificationModel(response.notification, 'success', 4000); + const notification = new GlobalNotificationModel(response.notification, 'success', 4000); GlobalNotificationActions.appendGlobalNotification(notification); }) .catch((err) => { - let notification = new GlobalNotificationModel(err, 'danger', 10000); + const notification = new GlobalNotificationModel(err, 'danger', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }); }; }, - getPublicContracts(){ + getPublicContracts() { return this.state.contractList.filter((contract) => contract.is_public); }, - getPrivateContracts(){ + getPrivateContracts() { return this.state.contractList.filter((contract) => !contract.is_public); }, render() { - let publicContracts = this.getPublicContracts(); - let privateContracts = this.getPrivateContracts(); + const publicContracts = this.getPublicContracts(); + const privateContracts = this.getPrivateContracts(); let createPublicContractForm = null; setDocumentTitle(getLangText('Contracts settings')); - if(publicContracts.length === 0) { + if (publicContracts.length === 0) { createPublicContractForm = ( + }} /> ); } @@ -114,7 +109,7 @@ let ContractSettings = React.createClass({ {publicContracts.map((contract, i) => { return ( + contract={contract} /> + }} /> {privateContracts.map((contract, i) => { return ( + contract={contract} /> { - // Display feedback to the user - let notification = new GlobalNotificationModel(getLangText('Contract %s successfully updated', res.name), 'success', 5000); + const notification = new GlobalNotificationModel(getLangText('Contract %s successfully updated', contract.name), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); // and refresh the contract list to get the updated contracs - return ContractListActions.fetchContractList(true); - }) - .then(() => { - // Also, reset the fineuploader component so that the user can again 'update' his contract - this.refs.fineuploader.reset(); - }) - .catch((err) => { - console.logGlobal(err); - let notification = new GlobalNotificationModel(getLangText('Contract could not be updated'), 'success', 5000); + return ContractListActions + .fetchContractList(true) + // Also, reset the fineuploader component if fetch is successful so that the user can again 'update' his contract + .then(this.refs.fineuploader.reset) + .catch((err) => { + const notification = new GlobalNotificationModel(getLangText('Latest contract failed to load'), 'danger', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + + return Promise.reject(err); + }); + }, (err) => { + const notification = new GlobalNotificationModel(getLangText('Contract could not be updated'), 'danger', 5000); GlobalNotificationActions.appendGlobalNotification(notification); - }); + + return Promise.reject(err); + }) + .catch(console.logGlobal); }, render() { return ( {/* 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} /> + ref='fineuploader' + fileInputElement={UploadButton({ showLabel: false })} + keyRoutine={{ + url: AppConstants.serverUrl + 's3/key/', + fileClass: 'contract' + }} + createBlobRoutine={{ + url: ApiUrls.blob_contracts + }} + validation={{ + itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit, + sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit, + allowedExtensions: ['pdf'] + }} + setIsUploadReady={() =>{/* So that ReactS3FineUploader is not complaining */}} + signature={{ + endpoint: AppConstants.serverUrl + 's3/signature/', + customHeaders: { + 'X-CSRFToken': getCookie(AppConstants.csrftoken) + } + }} + deleteFile={{ + enabled: true, + method: 'DELETE', + endpoint: AppConstants.serverUrl + 's3/delete', + customHeaders: { + 'X-CSRFToken': getCookie(AppConstants.csrftoken) + } + }} + fileClassToUpload={{ + singular: getLangText('UPDATE'), + plural: getLangText('UPDATE') + }} + isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} + submitFile={this.submitFile} /> ); } }); diff --git a/js/components/ascribe_settings/webhook_settings.js b/js/components/ascribe_settings/webhook_settings.js index 9deecbcd..4928c408 100644 --- a/js/components/ascribe_settings/webhook_settings.js +++ b/js/components/ascribe_settings/webhook_settings.js @@ -34,7 +34,6 @@ let WebhookSettings = React.createClass({ componentDidMount() { WebhookStore.listen(this.onChange); WebhookActions.fetchWebhooks(); - WebhookActions.fetchWebhookEvents(); }, componentWillUnmount() { @@ -49,7 +48,7 @@ let WebhookSettings = React.createClass({ return (event) => { WebhookActions.removeWebhook(webhookId); - let notification = new GlobalNotificationModel(getLangText('Webhook deleted'), 'success', 2000); + const notification = new GlobalNotificationModel(getLangText('Webhook deleted'), 'success', 2000); GlobalNotificationActions.appendGlobalNotification(notification); }; }, @@ -57,16 +56,16 @@ let WebhookSettings = React.createClass({ handleCreateSuccess() { this.refs.webhookCreateForm.reset(); WebhookActions.fetchWebhooks(true); - let notification = new GlobalNotificationModel(getLangText('Webhook successfully created'), 'success', 5000); + + const notification = new GlobalNotificationModel(getLangText('Webhook successfully created'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); }, - getWebhooks(){ - let content = ; - + getWebhooks() { if (this.state.webhooks) { - content = this.state.webhooks.map(function(webhook, i) { + return this.state.webhooks.map(function(webhook, i) { const event = webhook.event.split('.')[0]; + return (
    - }/> + } /> ); }, this); + } else { + return ( + + ); } - return content; }, getEvents() { @@ -110,18 +112,18 @@ let WebhookSettings = React.createClass({ ); })} ); + } else { + return null; } - return null; }, - render() { return (
    - + - { this.getEvents() } + {this.getEvents()} + required />
    @@ -162,4 +163,4 @@ let WebhookSettings = React.createClass({ } }); -export default WebhookSettings; \ No newline at end of file +export default WebhookSettings; diff --git a/js/components/ascribe_slides_container/slides_container.js b/js/components/ascribe_slides_container/slides_container.js index 39d515a3..109bbae7 100644 --- a/js/components/ascribe_slides_container/slides_container.js +++ b/js/components/ascribe_slides_container/slides_container.js @@ -57,21 +57,21 @@ const SlidesContainer = React.createClass({ // When the start_from parameter is used, this.setSlideNum can not simply be used anymore. nextSlide(additionalQueryParams) { const slideNum = parseInt(this.props.location.query.slide_num, 10) || 0; - let nextSlide = slideNum + 1; - this.setSlideNum(nextSlide, additionalQueryParams); + this.setSlideNum(slideNum + 1, additionalQueryParams); }, setSlideNum(nextSlideNum, additionalQueryParams = {}) { - let queryParams = Object.assign(this.props.location.query, additionalQueryParams); - queryParams.slide_num = nextSlideNum; - this.history.pushState(null, this.props.location.pathname, queryParams); + const { location: { pathname } } = this.props; + const query = Object.assign({}, this.props.location.query, additionalQueryParams, { slide_num: nextSlideNum }); + + this.history.push({ pathname, query }); }, // breadcrumbs are defined as attributes of the slides. // To extract them we have to read the DOM element's attributes extractBreadcrumbs() { const startFrom = parseInt(this.props.location.query.start_from, 10) || -1; - let breadcrumbs = []; + const breadcrumbs = []; React.Children.map(this.props.children, (child, i) => { if(child && i >= startFrom && child.props['data-slide-title']) { @@ -179,4 +179,4 @@ const SlidesContainer = React.createClass({ } }); -export default SlidesContainer; \ No newline at end of file +export default SlidesContainer; diff --git a/js/components/ascribe_social_share/facebook_share_button.js b/js/components/ascribe_social_share/facebook_share_button.js index aa0b6691..d5fbf699 100644 --- a/js/components/ascribe_social_share/facebook_share_button.js +++ b/js/components/ascribe_social_share/facebook_share_button.js @@ -2,6 +2,8 @@ import React from 'react'; +import FacebookHandler from '../../third_party/facebook_handler'; + import AppConstants from '../../constants/application_constants'; import { InjectInHeadUtils } from '../../utils/inject_utils'; @@ -17,24 +19,40 @@ let FacebookShareButton = React.createClass({ }; }, - componentDidMount() { - /** - * Ideally we would only use FB.XFBML.parse() on the component that we're - * mounting, but doing this when we first load the FB sdk causes unpredictable behaviour. - * The button sometimes doesn't get initialized, likely because FB hasn't properly - * been initialized yet. - * - * To circumvent this, we always have the sdk parse the entire DOM on the initial load - * (see FacebookHandler) and then use FB.XFBML.parse() on the mounting component later. - */ - - InjectInHeadUtils - .inject(AppConstants.facebook.sdkUrl) - .then(() => { FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement) }); + getInitialState() { + return FacebookHandler.getState(); }, - shouldComponentUpdate(nextProps) { - return this.props.type !== nextProps.type; + componentDidMount() { + FacebookHandler.listen(this.onChange); + + this.loadFacebook(); + }, + + shouldComponentUpdate(nextProps, nextState) { + // Don't update if the props haven't changed or the FB SDK loading status is still the same + return this.props.type !== nextProps.type || nextState.loaded !== this.state.loaded; + }, + + componentDidUpdate() { + // If the component changes, we need to reparse the share button's XFBML. + // To prevent cases where the Facebook SDK hasn't been loaded yet at this stage, + // let's make sure that it's injected before trying to reparse. + this.loadFacebook(); + }, + + onChange(state) { + this.setState(state); + }, + + loadFacebook() { + InjectInHeadUtils + .inject(AppConstants.facebook.sdkUrl) + .then(() => { + if (this.state.loaded) { + FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parent) + } + }); }, render() { diff --git a/js/components/ascribe_table/models/table_models.js b/js/components/ascribe_table/models/table_models.js index b675d14e..99cf7e64 100644 --- a/js/components/ascribe_table/models/table_models.js +++ b/js/components/ascribe_table/models/table_models.js @@ -2,15 +2,15 @@ export class ColumnModel { // ToDo: Add validation for all passed-in parameters - constructor(transformFn, columnName, displayName, displayType, rowWidth, canBeOrdered, transition, className) { + constructor({ transformFn, columnName = '', displayElement, displayType, rowWidth, canBeOrdered, transition, className = '' }) { this.transformFn = transformFn; this.columnName = columnName; - this.displayName = displayName; + this.displayElement = displayElement; this.displayType = displayType; this.rowWidth = rowWidth; this.canBeOrdered = canBeOrdered; this.transition = transition; - this.className = className ? className : ''; + this.className = className; } } @@ -28,7 +28,7 @@ export class ColumnModel { * our selfes, using this TransitionModel. */ export class TransitionModel { - constructor(to, queryKey, valueKey, callback) { + constructor({ to, queryKey, valueKey, callback }) { this.to = to; this.queryKey = queryKey; this.valueKey = valueKey; @@ -38,4 +38,4 @@ export class TransitionModel { toReactRouterLink(queryValue) { return '/' + this.to + '/' + queryValue; } -} \ No newline at end of file +} diff --git a/js/components/ascribe_table/table_header.js b/js/components/ascribe_table/table_header.js index f807627b..78a31681 100644 --- a/js/components/ascribe_table/table_header.js +++ b/js/components/ascribe_table/table_header.js @@ -1,5 +1,5 @@ -'use strict'; +'use strict'; import React from 'react'; import TableHeaderItem from './table_header_item'; @@ -29,7 +29,7 @@ let TableHeader = React.createClass({ - {this.props.displayName} + {displayElement} ); } else { return ( - {this.props.displayName} + {displayElement} ); } } else { return ( - + - {this.props.displayName} + {displayElement} ); diff --git a/js/components/ascribe_table/table_item_acl_filtered.js b/js/components/ascribe_table/table_item_acl_filtered.js index 22a28130..9ca91bcf 100644 --- a/js/components/ascribe_table/table_item_acl_filtered.js +++ b/js/components/ascribe_table/table_item_acl_filtered.js @@ -3,15 +3,15 @@ import React from 'react'; -let TableItemAclFiltered = React.createClass({ +const TableItemAclFiltered = React.createClass({ propTypes: { content: React.PropTypes.object, - notifications: React.PropTypes.string + notifications: React.PropTypes.array }, render() { - var availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete']; - if (this.props.notifications && this.props.notifications.length > 0){ + const availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete']; + if (this.props.notifications && this.props.notifications.length) { return ( {this.props.notifications[0].action_str} @@ -19,15 +19,14 @@ let TableItemAclFiltered = React.createClass({ ); } - let filteredAcls = Object.keys(this.props.content).filter((key) => { - return availableAcls.indexOf(key) > -1 && this.props.content[key]; - }); - - filteredAcls = filteredAcls.map((acl) => acl.split('acl_')[1]); + const filteredAcls = Object.keys(this.props.content) + .filter((key) => availableAcls.indexOf(key) > -1 && this.props.content[key]) + .map((acl) => acl.split('acl_')[1]) + .join('/'); return ( - {filteredAcls.join('/')} + {filteredAcls} ); } diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js index 25552819..db28846b 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js @@ -26,7 +26,7 @@ let FileDragAndDropDialog = React.createClass({ getDragDialog(fileClass) { if (dragAndDropAvailable) { return [ -

    {getLangText('Drag %s here', fileClass)}

    , +

    {getLangText('Drag %s here', fileClass)}

    ,

    {getLangText('or')}

    ]; } else { @@ -46,6 +46,8 @@ let FileDragAndDropDialog = React.createClass({ if (hasFiles) { return null; } else { + let dialogElement; + if (enableLocalHashing && !uploadMethod) { const currentQueryParams = getCurrentQueryParams(); @@ -55,9 +57,9 @@ let FileDragAndDropDialog = React.createClass({ const queryParamsUpload = Object.assign({}, currentQueryParams); queryParamsUpload.method = 'upload'; - return ( -
    -

    {getLangText('Would you rather')}

    + dialogElement = ( +
    +

    {getLangText('Would you rather')}

    {/* The frontend in live is hosted under /app, Since `Link` is appending that base url, if its defined @@ -85,32 +87,40 @@ let FileDragAndDropDialog = React.createClass({ ); } else { if (multipleFiles) { - return ( - - {this.getDragDialog(fileClassToUpload.plural)} - - {getLangText('choose %s to upload', fileClassToUpload.plural)} - + dialogElement = [ + this.getDragDialog(fileClassToUpload.plural), + + {getLangText('choose %s to upload', fileClassToUpload.plural)} - ); + ]; } else { const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular) : getLangText('choose a %s to upload', fileClassToUpload.singular); - return ( - - {this.getDragDialog(fileClassToUpload.singular)} - - {dialog} - + dialogElement = [ + this.getDragDialog(fileClassToUpload.singular), + + {dialog} - ); + ]; } } + + return ( +
    +
    + {dialogElement} +
    + {/* Hide the uploader and just show that there's been on files uploaded yet when printing */} +

    + {getLangText('No files uploaded')} +

    +
    + ); } } }); diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_image.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_image.js index 927a5b22..5c757121 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_image.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview_image.js @@ -49,7 +49,7 @@ const FileDragAndDropPreviewImage = React.createClass({ }; let actionSymbol; - + // only if assets are actually downloadable, there should be a download icon if the process is already at // 100%. If not, no actionSymbol should be displayed if(progress === 100 && areAssetsDownloadable) { @@ -68,7 +68,7 @@ const FileDragAndDropPreviewImage = 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 index 6612f968..ffd26a13 100644 --- a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js +++ b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js @@ -1,6 +1,7 @@ 'use strict'; import React from 'react'; +import classNames from 'classnames'; import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils'; import { getLangText } from '../../../utils/lang_utils'; @@ -9,7 +10,7 @@ import { truncateTextAtCharIndex } from '../../../utils/general_utils'; const { func, array, bool, shape, string } = React.PropTypes; -export default function UploadButton({ className = 'btn btn-default btn-sm' } = {}) { +export default function UploadButton({ className = 'btn btn-default btn-sm', showLabel = true } = {}) { return React.createClass({ displayName: 'UploadButton', @@ -119,28 +120,28 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = }, getUploadedFileLabel() { - const uploadedFile = this.getUploadedFile(); - const uploadingFiles = this.getUploadingFiles(); + if (showLabel) { + const uploadedFile = this.getUploadedFile(); + const uploadingFiles = this.getUploadingFiles(); - if(uploadingFiles.length) { - return ( - - {' ' + truncateTextAtCharIndex(uploadingFiles[0].name, 40) + ' '} - [{getLangText('cancel upload')}] - - ); - } else if(uploadedFile) { - return ( - - - {' ' + truncateTextAtCharIndex(uploadedFile.name, 40) + ' '} - [{getLangText('remove')}] - - ); - } else { - return ( - {getLangText('No file chosen')} - ); + if (uploadingFiles.length) { + return ( + + {' ' + truncateTextAtCharIndex(uploadingFiles[0].name, 40) + ' '} + [{getLangText('cancel upload')}] + + ); + } else if (uploadedFile) { + return ( + + + {' ' + truncateTextAtCharIndex(uploadedFile.name, 40) + ' '} + [{getLangText('remove')}] + + ); + } else { + return {getLangText('No file chosen')}; + } } }, @@ -158,7 +159,7 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = * Therefore the wrapping component needs to be an `anchor` tag instead of a `button` */ return ( -
    +
    {/* The button needs to be of `type="button"` as it would otherwise submit the form its in. diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index eb211504..feb0479d 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -36,20 +36,15 @@ const ReactS3FineUploader = React.createClass({ keyRoutine: shape({ url: string, fileClass: string, - pieceId: oneOfType([ - string, - number - ]) + pieceId: number }), createBlobRoutine: shape({ url: string, - pieceId: oneOfType([ - string, - number - ]) + pieceId: number }), handleChangedFile: func, // is for when a file is dropped or selected submitFile: func, // is for when a file has been successfully uploaded, TODO: rename to handleSubmitFile + onValidationFailed: func, autoUpload: bool, debug: bool, objectProperties: shape({ @@ -523,13 +518,16 @@ const ReactS3FineUploader = React.createClass({ }, isFileValid(file) { - if(file.size > this.props.validation.sizeLimit) { + if (file.size > this.props.validation.sizeLimit) { + const fileSizeInMegaBytes = this.props.validation.sizeLimit / 1000000; - let fileSizeInMegaBytes = this.props.validation.sizeLimit / 1000000; - - let notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000); + const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000); GlobalNotificationActions.appendGlobalNotification(notification); + if (typeof this.props.onValidationFailed === 'function') { + this.props.onValidationFailed(file); + } + return false; } else { return true; diff --git a/js/components/footer.js b/js/components/footer.js index 31145d4b..f2e35dfc 100644 --- a/js/components/footer.js +++ b/js/components/footer.js @@ -7,7 +7,7 @@ import { getLangText } from '../utils/lang_utils'; let Footer = React.createClass({ render() { return ( -
    +


    api | diff --git a/js/components/global_action.js b/js/components/global_action.js deleted file mode 100644 index 80df0c75..00000000 --- a/js/components/global_action.js +++ /dev/null @@ -1,43 +0,0 @@ -'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 797684ec..c16cba86 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -219,10 +219,11 @@ let Header = React.createClass({ return (
    + className="hidden-print">
    ); } diff --git a/js/components/password_reset_container.js b/js/components/password_reset_container.js index 31275a08..6d2d089e 100644 --- a/js/components/password_reset_container.js +++ b/js/components/password_reset_container.js @@ -130,8 +130,9 @@ let PasswordResetForm = React.createClass({ }, handleSuccess() { - this.history.pushState(null, '/collection'); - let notification = new GlobalNotificationModel(getLangText('password successfully updated'), 'success', 10000); + this.history.push('/collection'); + + const notification = new GlobalNotificationModel(getLangText('password successfully updated'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, diff --git a/js/components/piece_list.js b/js/components/piece_list.js index 9424117c..666d1b54 100644 --- a/js/components/piece_list.js +++ b/js/components/piece_list.js @@ -37,6 +37,7 @@ let PieceList = React.createClass({ bulkModalButtonListType: React.PropTypes.func, canLoadPieceList: React.PropTypes.bool, redirectTo: React.PropTypes.string, + shouldRedirect: React.PropTypes.func, customSubmitButton: React.PropTypes.element, customThumbnailPlaceholder: React.PropTypes.func, filterParams: React.PropTypes.array, @@ -114,9 +115,13 @@ let PieceList = React.createClass({ }, componentDidUpdate() { - if (this.props.redirectTo && this.state.unfilteredPieceListCount === 0) { + const { location: { query }, redirectTo, shouldRedirect } = this.props; + const { unfilteredPieceListCount } = this.state; + + if (redirectTo && unfilteredPieceListCount === 0 && + (typeof shouldRedirect === 'function' && shouldRedirect(unfilteredPieceListCount))) { // FIXME: hack to redirect out of the dispatch cycle - window.setTimeout(() => this.history.pushState(null, this.props.redirectTo, this.props.location.query), 0); + window.setTimeout(() => this.history.push({ query, pathname: redirectTo }), 0); } }, @@ -169,15 +174,16 @@ let PieceList = React.createClass({ } }, - searchFor(searchTerm) { - this.loadPieceList({ - page: 1, - search: searchTerm - }); - this.history.pushState(null, this.props.location.pathname, {page: 1}); + searchFor(search) { + const { location: { pathname } } = this.props; + + this.loadPieceList({ search, page: 1 }); + this.history.push({ pathname, query: { page: 1 } }); }, - applyFilterBy(filterBy){ + applyFilterBy(filterBy) { + const { location: { pathname } } = this.props; + this.setState({ isFilterDirty: true }); @@ -190,31 +196,32 @@ let PieceList = React.createClass({ this.state.pieceList .forEach((piece) => { // but only if they're actually open - if(this.state.isEditionListOpenForPieceId[piece.id].show) { + const isEditionListOpenForPiece = this.state.isEditionListOpenForPieceId[piece.id]; + + if (isEditionListOpenForPiece && isEditionListOpenForPiece.show) { EditionListActions.refreshEditionList({ pieceId: piece.id, filterBy }); } - }); }); // we have to redirect the user always to page one as it could be that there is no page two // for filtered pieces - this.history.pushState(null, this.props.location.pathname, {page: 1}); + this.history.push({ pathname, query: { page: 1 } }); }, applyOrderBy(orderBy) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, page, pageSize, search } = this.state; + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); }, loadPieceList({ page, filterBy = this.state.filterBy, search = this.state.search }) { + const { orderAsc, pageSize } = this.state; const orderBy = this.state.orderBy || this.props.orderBy; - return PieceListActions.fetchPieceList(page, this.state.pageSize, search, - orderBy, this.state.orderAsc, filterBy); + return PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); }, fetchSelectedPieceEditionList() { @@ -240,8 +247,9 @@ let PieceList = React.createClass({ }, handleAclSuccess() { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderBy, orderAsc, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); this.fetchSelectedPieceEditionList() .forEach((pieceId) => { diff --git a/js/components/register_piece.js b/js/components/register_piece.js index 8211e91e..3f268760 100644 --- a/js/components/register_piece.js +++ b/js/components/register_piece.js @@ -65,26 +65,21 @@ let RegisterPiece = React.createClass( { this.setState(state); }, - handleSuccess(response){ - let notification = new GlobalNotificationModel(response.notification, 'success', 10000); + handleSuccess(response) { + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + const notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); // once the user was able to register a piece successfully, we need to make sure to keep // the piece list up to date - PieceListActions.fetchPieceList( - this.state.page, - this.state.pageSize, - this.state.searchTerm, - this.state.orderBy, - this.state.orderAsc, - this.state.filterBy - ); + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); - this.history.pushState(null, `/pieces/${response.piece.id}`); + this.history.push(`/pieces/${response.piece.id}`); }, getSpecifyEditions() { - if(this.state.whitelabel && this.state.whitelabel.acl_create_editions || Object.keys(this.state.whitelabel).length === 0) { + if (this.state.whitelabel && this.state.whitelabel.acl_create_editions || Object.keys(this.state.whitelabel).length === 0) { return ( + min={1} + max={100} /> ); } diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js index a8d946b5..2ef96661 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js @@ -19,8 +19,10 @@ import ApiUrls from '../../../../../../constants/api_urls'; import requests from '../../../../../../utils/requests'; -import { getLangText } from '../../../../../../utils/lang_utils'; +import { getErrorNotificationMessage } from '../../../../../../utils/error_utils'; import { setCookie } from '../../../../../../utils/fetch_api_utils'; +import { validateForms } from '../../../../../../utils/form_utils'; +import { getLangText } from '../../../../../../utils/lang_utils'; import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_s3_fine_uploader_utils'; @@ -35,7 +37,7 @@ const PRRegisterPieceForm = React.createClass({ mixins: [History], - getInitialState(){ + getInitialState() { return { digitalWorkKeyReady: true, thumbnailKeyReady: true, @@ -54,16 +56,16 @@ const PRRegisterPieceForm = React.createClass({ * second adding all the additional details */ submit() { - if(!this.validateForms()) { + if (!this.validateForms()) { return; - } else { - // disable the submission button right after the user - // clicks on it to avoid double submission - this.setState({ - submitted: true - }); } + // disable the submission button right after the user + // clicks on it to avoid double submission + this.setState({ + submitted: true + }); + const { currentUser } = this.props; const { registerPieceForm, additionalDataForm, @@ -104,12 +106,20 @@ const PRRegisterPieceForm = React.createClass({ GlobalNotificationActions.appendGlobalNotification(notificationMessage); }); }) - .then(() => this.history.pushState(null, `/pieces/${this.state.piece.id}`)) + .then(() => this.history.push(`/pieces/${this.state.piece.id}`)) .catch((err) => { - const notificationMessage = new GlobalNotificationModel(getLangText("Oops! We weren't able to send your submission. Contact: support@ascribe.io"), 'danger', 5000); + const errMessage = (getErrorNotificationMessage(err) || getLangText("Oops! We weren't able to send your submission.")) + + getLangText(' Please contact support@ascribe.io'); + + const notificationMessage = new GlobalNotificationModel(errMessage, 'danger', 10000); GlobalNotificationActions.appendGlobalNotification(notificationMessage); console.logGlobal(new Error('Portfolio Review piece registration failed'), err); + + // Reset the submit button + this.setState({ + submitted: false + }); }); }, @@ -118,11 +128,7 @@ const PRRegisterPieceForm = React.createClass({ additionalDataForm, uploadersForm } = this.refs; - const registerPieceFormValidation = registerPieceForm.validate(); - const additionalDataFormValidation = additionalDataForm.validate(); - const uploaderFormValidation = uploadersForm.validate(); - - return registerPieceFormValidation && additionalDataFormValidation && uploaderFormValidation; + return validateForms([registerPieceForm, additionalDataForm, uploadersForm], true); }, getCreateBlobRoutine() { @@ -139,7 +145,7 @@ const PRRegisterPieceForm = React.createClass({ }, /** - * This method is overloaded so that we can track the ready-state + * These two methods are overloaded so that we can track the ready-state * of each uploader in the component * @param {string} uploaderKey Name of the uploader's key to track */ @@ -151,6 +157,14 @@ const PRRegisterPieceForm = React.createClass({ }; }, + handleOptionalFileValidationFailed(uploaderKey) { + return () => { + this.setState({ + [uploaderKey]: true + }); + }; + }, + getSubmitButton() { const { digitalWorkKeyReady, thumbnailKeyReady, @@ -179,11 +193,12 @@ const PRRegisterPieceForm = React.createClass({ render() { const { location } = this.props; + const maxThumbnailSize = AppConstants.fineUploader.validation.workThumbnail.sizeLimit / 1000000; return (
    + label={`${getLangText('Featured Cover photo')} max ${maxThumbnailSize}MB`}>
    this.history.replaceState(null, `/${location.query.redirect}`, queryCopy)); + if (location && location.query && location.query.redirect) { + window.setTimeout(() => this.history.replace({ + pathname: `/${location.query.redirect}`, + query: omitFromObject(location.query, ['redirect']) + })); } }, @@ -125,4 +128,4 @@ const PRLanding = React.createClass({ } }); -export default PRLanding; \ No newline at end of file +export default PRLanding; diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js b/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js index 0fbca419..99c9a401 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js @@ -39,7 +39,7 @@ const PRRegisterPiece = React.createClass({ if(currentUser && currentUser.email) { const submittedPieceId = getCookie(currentUser.email); if(submittedPieceId) { - this.history.pushState(null, `/pieces/${submittedPieceId}`); + this.history.push(`/pieces/${submittedPieceId}`); } } }, @@ -62,7 +62,7 @@ const PRRegisterPiece = React.createClass({

    Portfolio Review

    -

    {getLangText('Submission closing on %s', ' 22 Dec 2015')}

    +

    {getLangText('Submission closing on %s', ' 27 Dec 2015')}

    For more information, visit:  portfolio-review.de @@ -84,4 +84,4 @@ const PRRegisterPiece = React.createClass({ } }); -export default PRRegisterPiece; \ No newline at end of file +export default PRRegisterPiece; diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_routes/pr_proxy_handler.js b/js/components/whitelabel/prize/portfolioreview/components/pr_routes/pr_proxy_handler.js index 04ff8ce6..f81c4539 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_routes/pr_proxy_handler.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_routes/pr_proxy_handler.js @@ -17,7 +17,7 @@ export function AuthPrizeRoleRedirect({ to, when }) { .reduce((a, b) => a || b); if (exprToValidate) { - window.setTimeout(() => history.replaceState(null, to, query)); + window.setTimeout(() => history.replace({ query, pathname: to })); return true; } else { return false; diff --git a/js/components/whitelabel/prize/prize_routes.js b/js/components/whitelabel/prize/prize_routes.js index 7a72e5d9..5f80b30c 100644 --- a/js/components/whitelabel/prize/prize_routes.js +++ b/js/components/whitelabel/prize/prize_routes.js @@ -12,6 +12,8 @@ import SPPieceContainer from './simple_prize/components/ascribe_detail/prize_pie import SPSettingsContainer from './simple_prize/components/prize_settings_container'; import SPApp from './simple_prize/prize_app'; +import SluicePieceContainer from './sluice/components/sluice_detail/sluice_piece_container'; + import PRApp from './portfolioreview/pr_app'; import PRLanding from './portfolioreview/components/pr_landing'; import PRRegisterPiece from './portfolioreview/components/pr_register_piece'; @@ -53,7 +55,7 @@ const ROUTES = { path='collection' component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPPieceList)} headerTitle='COLLECTION'/> - + diff --git a/js/components/whitelabel/prize/simple_prize/actions/prize_rating_actions.js b/js/components/whitelabel/prize/simple_prize/actions/prize_rating_actions.js index 68b5334b..91852704 100644 --- a/js/components/whitelabel/prize/simple_prize/actions/prize_rating_actions.js +++ b/js/components/whitelabel/prize/simple_prize/actions/prize_rating_actions.js @@ -10,14 +10,15 @@ class PrizeRatingActions { this.generateActions( 'updatePrizeRatings', 'updatePrizeRatingAverage', - 'updatePrizeRating' + 'updatePrizeRating', + 'resetPrizeRatings' ); } - fetchAverage(pieceId) { + fetchAverage(pieceId, round) { return Q.Promise((resolve, reject) => { PrizeRatingFetcher - .fetchAverage(pieceId) + .fetchAverage(pieceId, round) .then((res) => { this.actions.updatePrizeRatingAverage(res.data); resolve(res); @@ -29,10 +30,10 @@ class PrizeRatingActions { }); } - fetchOne(pieceId) { + fetchOne(pieceId, round) { return Q.Promise((resolve, reject) => { PrizeRatingFetcher - .fetchOne(pieceId) + .fetchOne(pieceId, round) .then((res) => { this.actions.updatePrizeRating(res.rating.rating); resolve(res); @@ -43,10 +44,10 @@ class PrizeRatingActions { }); } - createRating(pieceId, rating) { + createRating(pieceId, rating, round) { return Q.Promise((resolve, reject) => { PrizeRatingFetcher - .rate(pieceId, rating) + .rate(pieceId, rating, round) .then((res) => { this.actions.updatePrizeRating(res.rating.rating); resolve(res); @@ -70,10 +71,6 @@ class PrizeRatingActions { }); }); } - - updateRating(rating) { - this.actions.updatePrizeRating(rating); - } } -export default alt.createActions(PrizeRatingActions); \ No newline at end of file +export default alt.createActions(PrizeRatingActions); diff --git a/js/components/whitelabel/prize/simple_prize/components/ascribe_accordion_list/accordion_list_item_prize.js b/js/components/whitelabel/prize/simple_prize/components/ascribe_accordion_list/accordion_list_item_prize.js index 965b9012..e34f8347 100644 --- a/js/components/whitelabel/prize/simple_prize/components/ascribe_accordion_list/accordion_list_item_prize.js +++ b/js/components/whitelabel/prize/simple_prize/components/ascribe_accordion_list/accordion_list_item_prize.js @@ -58,8 +58,9 @@ let AccordionListItemPrize = React.createClass({ }, handleSubmitPrizeSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); let notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); @@ -138,8 +139,9 @@ let AccordionListItemPrize = React.createClass({ }, refreshPieceData() { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); }, onSelectChange(){ @@ -171,23 +173,25 @@ let AccordionListItemPrize = React.createClass({ }, render() { + const { children, className, content } = this.props; + const { currentUser } = this.state; + // Only show the artist name if you are the participant or if you are a judge and the piece is shortlisted - let artistName = ((this.state.currentUser.is_jury && !this.state.currentUser.is_judge) || - (this.state.currentUser.is_judge && !this.props.content.selected )) ? -

    } buttons={this.getPrizeButtons()} badge={this.getPrizeBadge()}> - {this.props.children} + {children}
    ); diff --git a/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js b/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js index 16849ed1..bdaf385d 100644 --- a/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js +++ b/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js @@ -9,15 +9,16 @@ import StarRating from 'react-star-rating'; import ReactError from '../../../../../../mixins/react_error'; import { ResourceNotFoundError } from '../../../../../../models/errors'; -import PieceActions from '../../../../../../actions/piece_actions'; -import PieceStore from '../../../../../../stores/piece_store'; - -import PieceListStore from '../../../../../../stores/piece_list_store'; -import PieceListActions from '../../../../../../actions/piece_list_actions'; - +import PrizeActions from '../../actions/prize_actions'; +import PrizeStore from '../../stores/prize_store'; import PrizeRatingActions from '../../actions/prize_rating_actions'; import PrizeRatingStore from '../../stores/prize_rating_store'; +import PieceActions from '../../../../../../actions/piece_actions'; +import PieceStore from '../../../../../../stores/piece_store'; +import PieceListStore from '../../../../../../stores/piece_list_store'; +import PieceListActions from '../../../../../../actions/piece_list_actions'; + import UserStore from '../../../../../../stores/user_store'; import UserActions from '../../../../../../actions/user_actions'; @@ -34,9 +35,7 @@ import CollapsibleParagraph from '../../../../../../components/ascribe_collapsib import FurtherDetailsFileuploader from '../../../../../ascribe_detail/further_details_fileuploader'; import InputCheckbox from '../../../../../ascribe_forms/input_checkbox'; -import LoanForm from '../../../../../ascribe_forms/form_loan'; import ListRequestActions from '../../../../../ascribe_forms/list_form_request_actions'; -import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper'; import GlobalNotificationModel from '../../../../../../models/global_notification_model'; import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; @@ -52,16 +51,17 @@ import { setDocumentTitle } from '../../../../../../utils/dom_utils'; /** * This is the component that implements resource/data specific functionality */ -let PieceContainer = React.createClass({ +let PrizePieceContainer = React.createClass({ propTypes: { - params: React.PropTypes.object + params: React.PropTypes.object, + selectedPrizeActionButton: React.PropTypes.func }, mixins: [ReactError], getInitialState() { return mergeOptions( - PieceStore.getState(), + PieceStore.getInitialState(), UserStore.getState() ); }, @@ -70,28 +70,24 @@ let PieceContainer = React.createClass({ PieceStore.listen(this.onChange); UserStore.listen(this.onChange); - // Every time we enter the piece detail page, just reset the piece - // store as it will otherwise display wrong/old data once the user loads - // the piece detail a second time - PieceActions.updatePiece({}); - - PieceActions.fetchOne(this.props.params.pieceId); UserActions.fetchCurrentUser(); + this.loadPiece(); }, // 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) + // button to update the URL parameter (and therefore to switch pieces) or + // when the user clicks on a notification while being in another piece view componentWillReceiveProps(nextProps) { - if(this.props.params.pieceId !== nextProps.params.pieceId) { - PieceActions.updatePiece({}); - PieceActions.fetchOne(nextProps.params.pieceId); + if (this.props.params.pieceId !== nextProps.params.pieceId) { + PieceActions.flushPiece(); + this.loadPiece(nextProps.params.pieceId); } }, componentDidUpdate() { - const { pieceError } = this.state; + const { pieceMeta: { err: pieceErr } } = this.state; - if (pieceError && pieceError.status === 404) { + if (pieceErr && pieceErr.status === 404) { this.throws(new ResourceNotFoundError(getLangText("Oops, the piece you're looking for doesn't exist."))); } }, @@ -101,26 +97,32 @@ let PieceContainer = React.createClass({ UserStore.unlisten(this.onChange); }, - onChange(state) { this.setState(state); }, getActions() { - if (this.state.piece && - this.state.piece.notifications && - this.state.piece.notifications.length > 0) { + const { currentUser, piece } = this.state; + + if (piece.notifications && piece.notifications.length > 0) { return ( ); + notifications={piece.notifications} />); } }, + loadPiece(pieceId = this.props.params.pieceId) { + PieceActions.fetchPiece(pieceId); + }, + render() { - if(this.state.piece && this.state.piece.id) { + const { selectedPrizeActionButton } = this.props; + const { currentUser, piece } = this.state; + + if (piece.id) { /* This really needs a refactor! @@ -129,49 +131,48 @@ let PieceContainer = React.createClass({ */ // Only show the artist name if you are the participant or if you are a judge and the piece is shortlisted - let artistName = ((this.state.currentUser.is_jury && !this.state.currentUser.is_judge) || - (this.state.currentUser.is_judge && !this.state.piece.selected )) ? - null : this.state.piece.artist_name; + let artistName; + if ((currentUser.is_jury && !currentUser.is_judge) || (currentUser.is_judge && !piece.selected )) { + artistName =
    - } + } subheader={ + piece={piece} + currentUser={currentUser} + selectedPrizeActionButton={selectedPrizeActionButton} /> }> - + ); } else { @@ -186,16 +187,15 @@ let PieceContainer = React.createClass({ let NavigationHeader = React.createClass({ propTypes: { - piece: React.PropTypes.object, - currentUser: React.PropTypes.object + currentUser: React.PropTypes.object.isRequired, + piece: React.PropTypes.object.isRequired }, render() { const { currentUser, piece } = this.props; - if (currentUser && currentUser.email && currentUser.is_judge && currentUser.is_jury && - !currentUser.is_admin && piece && piece.navigation) { - let nav = piece.navigation; + if (currentUser.email && currentUser.is_judge && currentUser.is_jury && !currentUser.is_admin && piece.navigation) { + const nav = piece.navigation; return (
    @@ -215,41 +215,49 @@ let NavigationHeader = React.createClass({
    ); + } else { + return null; } - return null; } }); let PrizePieceRatings = React.createClass({ propTypes: { - loadPiece: React.PropTypes.func, - piece: React.PropTypes.object, - currentUser: React.PropTypes.object + currentUser: React.PropTypes.object.isRequired, + loadPiece: React.PropTypes.func.isRequired, + piece: React.PropTypes.object.isRequired, + + selectedPrizeActionButton: React.PropTypes.func }, getInitialState() { return mergeOptions( PieceListStore.getState(), - PrizeRatingStore.getState() + PrizeStore.getState(), + PrizeRatingStore.getInitialState() ); }, componentDidMount() { PrizeRatingStore.listen(this.onChange); - PrizeRatingActions.fetchOne(this.props.piece.id); - PrizeRatingActions.fetchAverage(this.props.piece.id); + PrizeStore.listen(this.onChange); PieceListStore.listen(this.onChange); + + PrizeActions.fetchPrize(); + this.fetchRatingsIfAuthorized(); + }, + + componentWillReceiveProps(nextProps) { + if (nextProps.currentUser.email !== this.props.currentUser.email) { + this.fetchRatingsIfAuthorized(); + } }, componentWillUnmount() { - // Every time we're leaving the piece detail page, - // just reset the piece that is saved in the piece store - // as it will otherwise display wrong/old data once the user loads - // the piece detail a second time - PrizeRatingActions.updateRating({}); - PrizeRatingStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange); + PrizeStore.unlisten(this.onChange); + PrizeRatingStore.unlisten(this.onChange); }, // The StarRating component does not have a property that lets us set @@ -257,7 +265,12 @@ let PrizePieceRatings = React.createClass({ // every mouseover be overridden, we need to set it ourselves initially to deal // with the problem. onChange(state) { + if (state.prize && state.prize.active_round != this.state.prize.active_round) { + this.fetchRatingsIfAuthorized(state); + } + this.setState(state); + if (this.refs.rating) { this.refs.rating.state.ratingCache = { pos: this.refs.rating.state.pos, @@ -268,74 +281,64 @@ let PrizePieceRatings = React.createClass({ } }, + fetchRatingsIfAuthorized(state = this.state) { + const { + currentUser: { + is_admin: isAdmin, + is_judge: isJudge, + is_jury: isJury + }, + piece: { id: pieceId } } = this.props; + + if (state.prize && 'active_round' in state.prize && (isAdmin || isJudge || isJury)) { + PrizeRatingActions.fetchOne(pieceId, state.prize.active_round); + PrizeRatingActions.fetchAverage(pieceId, state.prize.active_round); + } + }, + onRatingClick(event, args) { event.preventDefault(); - PrizeRatingActions.createRating(this.props.piece.id, args.rating).then( - this.refreshPieceData() - ); + PrizeRatingActions + .createRating(this.props.piece.id, args.rating, this.state.prize.active_round) + .then(this.refreshPieceData); }, - handleLoanRequestSuccess(message){ - let notification = new GlobalNotificationModel(message, 'success', 4000); - GlobalNotificationActions.appendGlobalNotification(notification); - }, + getSelectedActionButton() { + const { currentUser, piece, selectedPrizeActionButton: SelectedPrizeActionButton } = this.props; - getLoanButton(){ - let today = new Moment(); - let endDate = new Moment(); - endDate.add(6, 'months'); - return ( - - {getLangText('SEND LOAN REQUEST')} - - } - handleSuccess={this.handleLoanRequestSuccess} - title='REQUEST LOAN'> - - ); - }, - - handleShortlistSuccess(message){ - let notification = new GlobalNotificationModel(message, 'success', 2000); - GlobalNotificationActions.appendGlobalNotification(notification); + if (piece.selected && SelectedPrizeActionButton) { + return ( + + + + ); + } }, refreshPieceData() { + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + this.props.loadPiece(); - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); }, onSelectChange() { - PrizeRatingActions.toggleShortlist(this.props.piece.id) - .then( - (res) => { + PrizeRatingActions + .toggleShortlist(this.props.piece.id) + .then((res) => { this.refreshPieceData(); - return res; - }) - .then( - (res) => { - this.handleShortlistSuccess(res.notification); - } - ); + + if (res && res.notification) { + const notification = new GlobalNotificationModel(res.notification, 'success', 2000); + GlobalNotificationActions.appendGlobalNotification(notification); + } + }); }, - render(){ - if (this.props.piece && this.props.currentUser && this.props.currentUser.is_judge && this.state.average) { + render() { + if (this.props.piece.id && this.props.currentUser.is_judge && this.state.average) { // Judge sees shortlisting, average and per-jury notes return (
    @@ -352,9 +355,7 @@ let PrizePieceRatings = React.createClass({ - - {this.props.piece.selected ? this.getLoanButton() : null} - + {this.getSelectedActionButton()}

    @@ -369,17 +370,23 @@ let PrizePieceRatings = React.createClass({ size='md' step={0.5} rating={this.state.average} - ratingAmount={5}/> + ratingAmount={5} />

    {this.state.ratings.map((item, i) => { - let note = item.note ? + let note = item.note ? (
    note: {item.note} -
    : null; +
    + ) : null; + return ( -
    -
    +
    +
    + ratingAmount={5} /> {item.user} {note} @@ -399,8 +406,7 @@ let PrizePieceRatings = React.createClass({
    ); - } - else if (this.props.currentUser && this.props.currentUser.is_jury) { + } else if (this.props.currentUser.is_jury) { // Jury can set rating and note return (
    {return {'piece_id': this.props.piece.id}; }} + id={() => ({ 'piece_id': this.props.piece.id })} label={getLangText('Jury note')} - defaultValue={this.props.piece && this.props.piece.note_from_user ? this.props.piece.note_from_user.note : null} + defaultValue={this.props.piece.note_from_user || null} placeholder={getLangText('Enter your comments ...')} editable={true} successMessage={getLangText('Jury note saved')} url={ApiUrls.notes} - currentUser={this.props.currentUser}/> + currentUser={this.props.currentUser} /> ); + } else { + return null; } - return null; } }); let PrizePieceDetails = React.createClass({ propTypes: { - piece: React.PropTypes.object + piece: React.PropTypes.object.isRequired }, render() { const { piece } = this.props; - if (piece && - piece.prize && - piece.prize.name && - Object.keys(piece.extra_data).length !== 0) { + if (piece.prize && piece.prize.name && Object.keys(piece.extra_data).length) { return ( - - {Object.keys(piece.extra_data).sort().map((data) => { - // Remove leading number (for sorting), if any, and underscores with spaces - let label = data.replace(/^\d-/, '').replace(/_/g, ' '); - const value = piece.extra_data[data] || 'N/A'; + + {Object + .keys(piece.extra_data) + .sort() + .map((data) => { + // Remove leading number (for sorting), if any, and underscores with spaces + const label = data.replace(/^\d-/, '').replace(/_/g, ' '); + const value = piece.extra_data[data] || 'N/A'; - return ( - - - - ); - })} + return ( + + + + ); + }) + } {}} setIsUploadReady={() => {}} @@ -479,9 +488,10 @@ let PrizePieceDetails = React.createClass({ ); + } else { + return null; } - return null; } }); -export default PieceContainer; +export default PrizePieceContainer; diff --git a/js/components/whitelabel/prize/simple_prize/components/prize_landing.js b/js/components/whitelabel/prize/simple_prize/components/prize_landing.js index e26a05b5..82a21eab 100644 --- a/js/components/whitelabel/prize/simple_prize/components/prize_landing.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_landing.js @@ -46,7 +46,7 @@ let Landing = React.createClass({ // if user is already logged in, redirect him to piece list if(this.state.currentUser && this.state.currentUser.email) { // FIXME: hack to redirect out of the dispatch cycle - window.setTimeout(() => this.history.replaceState(null, '/collection'), 0); + window.setTimeout(() => this.history.replace('/collection'), 0); } }, diff --git a/js/components/whitelabel/prize/simple_prize/components/prize_piece_list.js b/js/components/whitelabel/prize/simple_prize/components/prize_piece_list.js index 972b3fac..23cdbb23 100644 --- a/js/components/whitelabel/prize/simple_prize/components/prize_piece_list.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_piece_list.js @@ -48,9 +48,8 @@ let PrizePieceList = React.createClass({ }, getButtonSubmit() { - const { currentUser } = this.state; - if (this.state.prize && this.state.prize.active && - !currentUser.is_jury && !currentUser.is_admin && !currentUser.is_judge){ + const { currentUser, prize } = this.state; + if (prize && prize.active && !currentUser.is_jury && !currentUser.is_admin && !currentUser.is_judge) { return ( + } + handleSuccess={this.handleSuccess} + title={getLangText('REQUEST LOAN')}> + + + ); + } +}); + +export default SluiceSelectedPrizeActionButton; + diff --git a/js/components/whitelabel/prize/sluice/components/sluice_detail/sluice_piece_container.js b/js/components/whitelabel/prize/sluice/components/sluice_detail/sluice_piece_container.js new file mode 100644 index 00000000..2d9debca --- /dev/null +++ b/js/components/whitelabel/prize/sluice/components/sluice_detail/sluice_piece_container.js @@ -0,0 +1,23 @@ +'use strict'; + +import React from 'react'; + +import SluiceSelectedPrizeActionButton from '../sluice_buttons/sluice_selected_prize_action_button'; + +import PrizePieceContainer from '../../../simple_prize/components/ascribe_detail/prize_piece_container'; + +const SluicePieceContainer = React.createClass({ + propTypes: { + params: React.PropTypes.object + }, + + render() { + return ( + + ); + } +}); + +export default SluicePieceContainer; diff --git a/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js b/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js index 26a186ca..ed8da83b 100644 --- a/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js +++ b/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js @@ -25,63 +25,72 @@ let WalletPieceContainer = React.createClass({ currentUser: React.PropTypes.object.isRequired, loadPiece: React.PropTypes.func.isRequired, handleDeleteSuccess: React.PropTypes.func.isRequired, - submitButtonType: React.PropTypes.func.isRequired + submitButtonType: React.PropTypes.func.isRequired, + children: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]) }, - render() { - if(this.props.piece && this.props.piece.id) { + const { + children, + currentUser, + handleDeleteSuccess, + loadPiece, + piece, + submitButtonType } = this.props; + + if (piece && piece.id) { return (
    -

    {this.props.piece.title}

    - - +

    {piece.title}

    + +
    } subheader={ -
    - - -
    -
    - }> +
    + + +
    +
    + }> + piece={piece} + currentUser={currentUser} + loadPiece={loadPiece} + handleDeleteSuccess={handleDeleteSuccess} + submitButtonType={submitButtonType}/> 0}> + show={piece.loan_history && piece.loan_history.length > 0}> + history={piece.loan_history}/> + show={!!(currentUser.username || piece.public_note)}> {return {'id': this.props.piece.id}; }} + id={() => {return {'id': piece.id}; }} label={getLangText('Personal note (private)')} - defaultValue={this.props.piece.private_note || null} + defaultValue={piece.private_note || null} placeholder={getLangText('Enter your comments ...')} editable={true} successMessage={getLangText('Private note saved')} url={ApiUrls.note_private_piece} - currentUser={this.props.currentUser}/> + currentUser={currentUser}/> - - {this.props.children} + {children} ); - } - else { + } else { return (
    diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js b/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js index 9d88408f..ebdef4f4 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js @@ -52,8 +52,9 @@ let CylandAccordionListItem = React.createClass({ }, handleSubmitSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); let notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js b/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js index d211d3e8..5fc3901f 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js @@ -40,7 +40,7 @@ let CylandPieceContainer = React.createClass({ getInitialState() { return mergeOptions( - PieceStore.getState(), + PieceStore.getInitialState(), UserStore.getState(), PieceListStore.getState() ); @@ -51,14 +51,17 @@ let CylandPieceContainer = React.createClass({ UserStore.listen(this.onChange); PieceListStore.listen(this.onChange); - // Every time we enter the piece detail page, just reset the piece - // store as it will otherwise display wrong/old data once the user loads - // the piece detail a second time - PieceActions.updatePiece({}); - this.loadPiece(); }, + // We need this for when the user clicks on a notification while being in another piece view + componentWillReceiveProps(nextProps) { + if (this.props.params.pieceId !== nextProps.params.pieceId) { + PieceActions.flushPiece(); + this.loadPiece(); + } + }, + componentWillUnmount() { PieceStore.unlisten(this.onChange); UserStore.unlisten(this.onChange); @@ -70,31 +73,34 @@ let CylandPieceContainer = React.createClass({ }, loadPiece() { - PieceActions.fetchOne(this.props.params.pieceId); + PieceActions.fetchPiece(this.props.params.pieceId); }, handleDeleteSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); // since we're deleting a piece, we just need to close // all editions dialogs and not reload them EditionListActions.closeAllEditionLists(); EditionListActions.clearAllEditionSelections(); - let notification = new GlobalNotificationModel(response.notification, 'success'); + const notification = new GlobalNotificationModel(response.notification, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); - this.history.pushState(null, '/collection'); + this.history.push('/collection'); }, render() { - if(this.state.piece && this.state.piece.id) { - setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', ')); + const { piece } = this.state; + + if (piece.id) { + setDocumentTitle(`${piece.artist_name}, ${piece.title}`); return ( ); - } - else { + } else { return (
    diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js b/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js index 93cc515a..c41caa76 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js @@ -23,9 +23,10 @@ import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_ let CylandAdditionalDataForm = React.createClass({ propTypes: { - handleSuccess: React.PropTypes.func, piece: React.PropTypes.object.isRequired, + disabled: React.PropTypes.bool, + handleSuccess: React.PropTypes.func, isInline: React.PropTypes.bool }, @@ -42,13 +43,13 @@ let CylandAdditionalDataForm = React.createClass({ }, handleSuccess() { - let notification = new GlobalNotificationModel(getLangText('Further details successfully updated'), 'success', 10000); + const notification = new GlobalNotificationModel(getLangText('Further details successfully updated'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, getFormData() { - let extradata = {}; - let formRefs = this.refs.form.refs; + const extradata = {}; + const formRefs = this.refs.form.refs; // Put additional fields in extra data object Object @@ -71,10 +72,13 @@ let CylandAdditionalDataForm = React.createClass({ }, render() { - let { piece, isInline, disabled, handleSuccess, location } = this.props; - let buttons, spinner, heading; + const { disabled, handleSuccess, isInline, piece } = this.props; - if(!isInline) { + let buttons; + let spinner; + let heading; + + if (!isInline) { buttons = (
    ); @@ -101,13 +105,15 @@ let CylandAdditionalDataForm = React.createClass({ ); } - if(piece && piece.id) { + if (piece.id) { + const { extra_data: extraData = {} } = piece; + return (
    + expanded={!disabled || !!extraData.artist_bio}> + defaultValue={extraData.artist_bio} + placeholder={getLangText('Enter the artist\'s biography...')} /> + expanded={!disabled || !!extraData.artist_contact_information}> + convertLinks + defaultValue={extraData.artist_contact_information} + placeholder={getLangText('Enter the artist\'s contact information...')} /> + expanded={!disabled || !!extraData.conceptual_overview}> + defaultValue={extraData.conceptual_overview} + placeholder={getLangText('Enter a conceptual overview...')} /> + expanded={!disabled || !!extraData.medium}> + defaultValue={extraData.medium} + placeholder={getLangText('Enter the medium (and other technical specifications)...')} /> + expanded={!disabled || !!extraData.size_duration}> + defaultValue={extraData.size_duration} + placeholder={getLangText('Enter the size / duration...')} /> + expanded={!disabled || !!extraData.display_instructions}> + defaultValue={extraData.display_instructions} + placeholder={getLangText('Enter the display instructions...')} /> + expanded={!disabled || !!extraData.additional_details}> + defaultValue={extraData.additional_details} + placeholder={getLangText('Enter additional details...')} /> - +
    ); } diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_landing.js b/js/components/whitelabel/wallet/components/cyland/cyland_landing.js index 21f8835a..0a8dadd1 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_landing.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_landing.js @@ -49,7 +49,7 @@ let CylandLanding = React.createClass({ // if user is already logged in, redirect him to piece list if(this.state.currentUser && this.state.currentUser.email) { // FIXME: hack to redirect out of the dispatch cycle - window.setTimeout(() => this.history.replaceState(null, '/collection'), 0); + window.setTimeout(() => this.history.replace('/collection'), 0); } }, diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js index fabed011..88088e9d 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js @@ -52,7 +52,7 @@ let CylandRegisterPiece = React.createClass({ return mergeOptions( UserStore.getState(), PieceListStore.getState(), - PieceStore.getState(), + PieceStore.getInitialState(), WhitelabelStore.getState(), { step: 0 @@ -67,7 +67,7 @@ let CylandRegisterPiece = React.createClass({ UserActions.fetchCurrentUser(); WhitelabelActions.fetchWhitelabel(); - let queryParams = this.props.location.query; + const queryParams = this.props.location.query; // Since every step of this register process is atomic, // we may need to enter the process at step 1 or 2. @@ -76,8 +76,8 @@ let CylandRegisterPiece = React.createClass({ // // We're using 'in' here as we want to know if 'piece_id' is present in the url, // we don't care about the value. - if(queryParams && 'piece_id' in queryParams) { - PieceActions.fetchOne(queryParams.piece_id); + if ('piece_id' in queryParams) { + PieceActions.fetchPiece(queryParams.piece_id); } }, @@ -92,64 +92,50 @@ let CylandRegisterPiece = React.createClass({ this.setState(state); }, - handleRegisterSuccess(response){ - + handleRegisterSuccess(response) { this.refreshPieceList(); - // also start loading the piece for the next step - if(response && response.piece) { - PieceActions.updatePiece({}); + // Also load the newly registered piece for the next step + if (response && response.piece) { PieceActions.updatePiece(response.piece); } - this.incrementStep(); - - this.refs.slidesContainer.nextSlide({ piece_id: response.piece.id }); + this.nextSlide({ piece_id: response.piece.id }); }, handleAdditionalDataSuccess() { - // We need to refetch the piece again after submitting the additional data - // since we want it's otherData to be displayed when the user choses to click + // since we want its otherData to be displayed when the user choses to click // on the browsers back button. - PieceActions.fetchOne(this.state.piece.id); + PieceActions.fetchPiece(this.state.piece.id); this.refreshPieceList(); - this.incrementStep(); - - this.refs.slidesContainer.nextSlide(); + this.nextSlide(); }, handleLoanSuccess(response) { - let notification = new GlobalNotificationModel(response.notification, 'success', 10000); + const notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); this.refreshPieceList(); - PieceActions.fetchOne(this.state.piece.id); - - this.history.pushState(null, `/pieces/${this.state.piece.id}`); + this.history.push(`/pieces/${this.state.piece.id}`); }, - // We need to increase the step to lock the forms that are already filled out - incrementStep() { - // also increase step - let newStep = this.state.step + 1; + nextSlide(queryParams) { + // We need to increase the step to lock the forms that are already filled out this.setState({ - step: newStep + step: this.state.step + 1 }); + + this.refs.slidesContainer.nextSlide(queryParams); }, refreshPieceList() { - PieceListActions.fetchPieceList( - this.state.page, - this.state.pageSize, - this.state.searchTerm, - this.state.orderBy, - this.state.orderAsc, - this.state.filterBy - ); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); }, render() { @@ -157,8 +143,7 @@ let CylandRegisterPiece = React.createClass({ const { currentUser, piece, step, whitelabel } = this.state; const today = new Moment(); - const datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain = new Moment(); - datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain.add(1000, 'years'); + const datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain = new Moment().add(1000, 'years'); const loanHeading = getLangText('Loan to Cyland archive'); const loanButtons = ( @@ -201,7 +186,7 @@ let CylandRegisterPiece = React.createClass({ submitMessage={getLangText('Submit')} isFineUploaderActive={true} handleSuccess={this.handleRegisterSuccess} - location={location}/> + location={location} />
    diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js index ce8cebf5..46763e2b 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js @@ -53,8 +53,9 @@ let IkonotvAccordionListItem = React.createClass({ }, handleSubmitSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); let notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js index d3479562..4f6a88a1 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js @@ -106,28 +106,33 @@ let IkonotvContractNotifications = React.createClass({ handleConfirm() { let contractAgreement = this.state.contractAgreementListNotifications[0].contract_agreement; - OwnershipFetcher.confirmContractAgreement(contractAgreement).then( - () => this.handleConfirmSuccess() - ); + OwnershipFetcher + .confirmContractAgreement(contractAgreement) + .then(this.handleConfirmSuccess); }, handleConfirmSuccess() { let notification = new GlobalNotificationModel(getLangText('You have accepted the conditions'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); - this.history.pushState(null, '/collection'); + + // Flush contract notifications and refetch + NotificationActions.flushContractAgreementListNotifications(); + NotificationActions.fetchContractAgreementListNotifications(); + + this.history.push('/collection'); }, handleDeny() { let contractAgreement = this.state.contractAgreementListNotifications[0].contract_agreement; - OwnershipFetcher.denyContractAgreement(contractAgreement).then( - () => this.handleDenySuccess() - ); + OwnershipFetcher + .denyContractAgreement(contractAgreement) + .then(this.handleDenySuccess); }, handleDenySuccess() { let notification = new GlobalNotificationModel(getLangText('You have denied the conditions'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); - this.history.pushState(null, '/collection'); + this.history.push('/collection'); }, getCopyrightAssociationForm(){ diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js index df58b7c7..a1280cc8 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js @@ -41,7 +41,7 @@ let IkonotvPieceContainer = React.createClass({ getInitialState() { return mergeOptions( - PieceStore.getState(), + PieceStore.getInitialState(), UserStore.getState(), PieceListStore.getState() ); @@ -52,19 +52,14 @@ let IkonotvPieceContainer = React.createClass({ UserStore.listen(this.onChange); PieceListStore.listen(this.onChange); - // Every time we enter the piece detail page, just reset the piece - // store as it will otherwise display wrong/old data once the user loads - // the piece detail a second time - PieceActions.updatePiece({}); - this.loadPiece(); }, - // We need this for when the user clicks on a notification while being in another piece view + // We need this for when the user clicks on a notification while being in another piece view componentWillReceiveProps(nextProps) { - if(this.props.params.pieceId !== nextProps.params.pieceId) { - PieceActions.updatePiece({}); - PieceActions.fetchOne(nextProps.params.pieceId); + if (this.props.params.pieceId !== nextProps.params.pieceId) { + PieceActions.flushPiece(); + this.loadPiece(); } }, @@ -79,25 +74,28 @@ let IkonotvPieceContainer = React.createClass({ }, loadPiece() { - PieceActions.fetchOne(this.props.params.pieceId); + PieceActions.fetchPiece(this.props.params.pieceId); }, handleDeleteSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); // since we're deleting a piece, we just need to close // all editions dialogs and not reload them EditionListActions.closeAllEditionLists(); EditionListActions.clearAllEditionSelections(); - let notification = new GlobalNotificationModel(response.notification, 'success'); + const notification = new GlobalNotificationModel(response.notification, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); - this.history.pushState(null, '/collection'); + this.history.push('/collection'); }, render() { + const { piece } = this.state; + let furtherDetails = ( ); - if(this.state.piece.extra_data && Object.keys(this.state.piece.extra_data).length > 0 && this.state.piece.acl) { + if (piece.extra_data && Object.keys(piece.extra_data).length && piece.acl) { furtherDetails = ( + disabled={!piece.acl.acl_edit} /> + disabled={!piece.acl.acl_edit} /> ); } - if(this.state.piece && this.state.piece.id) { - setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', ')); + if (piece.id) { + setDocumentTitle(`${piece.artist_name}, ${piece.title}`); + return ( ); - } - else { + } else { return (
    diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artist_details_form.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artist_details_form.js index 7aec7ff4..49ead8ec 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artist_details_form.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artist_details_form.js @@ -20,11 +20,10 @@ import { getLangText } from '../../../../../../utils/lang_utils'; let IkonotvArtistDetailsForm = React.createClass({ propTypes: { - handleSuccess: React.PropTypes.func, piece: React.PropTypes.object.isRequired, disabled: React.PropTypes.bool, - + handleSuccess: React.PropTypes.func, isInline: React.PropTypes.bool }, @@ -35,8 +34,8 @@ let IkonotvArtistDetailsForm = React.createClass({ }, getFormData() { - let extradata = {}; - let formRefs = this.refs.form.refs; + const extradata = {}; + const formRefs = this.refs.form.refs; // Put additional fields in extra data object Object @@ -53,21 +52,23 @@ let IkonotvArtistDetailsForm = React.createClass({ }, handleSuccess() { - let notification = new GlobalNotificationModel('Artist details successfully updated', 'success', 10000); + const notification = new GlobalNotificationModel(getLangText('Artist details successfully updated'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, render() { - let buttons, spinner, heading; - let { isInline, handleSuccess } = this.props; - + const { disabled, isInline, handleSuccess, piece } = this.props; - if(!isInline) { + let buttons; + let spinner; + let heading; + + if (!isInline) { buttons = ( ); @@ -75,7 +76,7 @@ let IkonotvArtistDetailsForm = React.createClass({ spinner = (

    - +

    ); @@ -89,13 +90,15 @@ let IkonotvArtistDetailsForm = React.createClass({ ); } - if(this.props.piece && this.props.piece.id && this.props.piece.extra_data) { + if (piece.id) { + const { extra_data: extraData = {} } = piece; + return ( + expanded={!disabled || !!extraData.artist_website}> + convertLinks + defaultValue={extraData.artist_website} + placeholder={getLangText('The artist\'s website if present...')} /> + expanded={!disabled || !!extraData.gallery_website}> + convertLinks + defaultValue={extraData.gallery_website} + placeholder={getLangText('The website of any related Gallery or Museum')} /> + expanded={!disabled || !!extraData.additional_websites}> + convertLinks + defaultValue={extraData.additional_websites} + placeholder={getLangText('Enter additional Websites/Publications if any')} /> + expanded={!disabled || !!extraData.conceptual_overview}> + defaultValue={extraData.conceptual_overview} + placeholder={getLangText('Enter a short bio about the Artist')} /> ); @@ -150,4 +155,4 @@ let IkonotvArtistDetailsForm = React.createClass({ } }); -export default IkonotvArtistDetailsForm; \ No newline at end of file +export default IkonotvArtistDetailsForm; diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artwork_details_form.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artwork_details_form.js index 97b1adc7..28c67c94 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artwork_details_form.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artwork_details_form.js @@ -20,11 +20,10 @@ import { getLangText } from '../../../../../../utils/lang_utils'; let IkonotvArtworkDetailsForm = React.createClass({ propTypes: { - handleSuccess: React.PropTypes.func, piece: React.PropTypes.object.isRequired, disabled: React.PropTypes.bool, - + handleSuccess: React.PropTypes.func, isInline: React.PropTypes.bool }, @@ -35,8 +34,8 @@ let IkonotvArtworkDetailsForm = React.createClass({ }, getFormData() { - let extradata = {}; - let formRefs = this.refs.form.refs; + const extradata = {}; + const formRefs = this.refs.form.refs; // Put additional fields in extra data object Object @@ -53,20 +52,23 @@ let IkonotvArtworkDetailsForm = React.createClass({ }, handleSuccess() { - let notification = new GlobalNotificationModel('Artwork details successfully updated', 'success', 10000); + const notification = new GlobalNotificationModel(getLangText('Artwork details successfully updated'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, render() { - let buttons, spinner, heading; - let { isInline, handleSuccess } = this.props; + const { disabled, isInline, handleSuccess, piece } = this.props; - if(!isInline) { + let buttons; + let spinner; + let heading; + + if (!isInline) { buttons = ( ); @@ -74,7 +76,7 @@ let IkonotvArtworkDetailsForm = React.createClass({ spinner = (

    - +

    ); @@ -88,13 +90,15 @@ let IkonotvArtworkDetailsForm = React.createClass({ ); } - if(this.props.piece && this.props.piece.id && this.props.piece.extra_data) { + if (piece.id && piece.extra_data) { + const { extra_data: extraData = {} } = piece; + return (
    + expanded={!disabled || !!extraData.medium}> + defaultValue={extraData.medium} + placeholder={getLangText('The medium of the file (i.e. photo, video, other, ...)')} /> + expanded={!disabled || !!extraData.size_duration}> + defaultValue={extraData.size_duration} + placeholder={getLangText('Size in centimeters. Duration in minutes.')} /> + expanded={!disabled || !!extraData.copyright}> + defaultValue={extraData.copyright} + placeholder={getLangText('Which copyright is attached to this work?')} /> + expanded={!disabled || !!extraData.courtesy_of}> + defaultValue={extraData.courtesy_of} + placeholder={getLangText('The current owner of the artwork')} /> + expanded={!disabled || !!extraData.copyright_of_photography}> + defaultValue={extraData.copyright_of_photography} + placeholder={getLangText('Who should be attributed for the photography?')} /> + expanded={!disabled || !!extraData.additional_details}> + defaultValue={extraData.additional_details} + placeholder={getLangText('Insert artwork overview')} /> ); @@ -166,4 +170,4 @@ let IkonotvArtworkDetailsForm = React.createClass({ } }); -export default IkonotvArtworkDetailsForm; \ No newline at end of file +export default IkonotvArtworkDetailsForm; diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_piece_list.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_piece_list.js index 0b51bdbd..5b489d09 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_piece_list.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_piece_list.js @@ -1,15 +1,18 @@ 'use strict'; import React from 'react'; + import PieceList from '../../../../piece_list'; import UserActions from '../../../../../actions/user_actions'; import UserStore from '../../../../../stores/user_store'; +import NotificationStore from '../../../../../stores/notification_store'; import IkonotvAccordionListItem from './ikonotv_accordion_list/ikonotv_accordion_list_item'; -import { getLangText } from '../../../../../utils/lang_utils'; import { setDocumentTitle } from '../../../../../utils/dom_utils'; +import { mergeOptions } from '../../../../../utils/general_utils'; +import { getLangText } from '../../../../../utils/lang_utils'; let IkonotvPieceList = React.createClass({ @@ -18,20 +21,33 @@ let IkonotvPieceList = React.createClass({ }, getInitialState() { - return UserStore.getState(); + return mergeOptions( + NotificationStore.getState(), + UserStore.getState() + ); }, componentDidMount() { + NotificationStore.listen(this.onChange); UserStore.listen(this.onChange); + UserActions.fetchCurrentUser(); }, componentWillUnmount() { + NotificationStore.unlisten(this.onChange); UserStore.unlisten(this.onChange); }, onChange(state) { this.setState(state); + + }, + + redirectIfNoContractNotifications() { + const { contractAgreementListNotifications } = this.state; + + return contractAgreementListNotifications && !contractAgreementListNotifications.length; }, render() { @@ -41,6 +57,7 @@ let IkonotvPieceList = React.createClass({
    + piece={this.state.piece} />
    ); + } else { + return null; } - return null; }, getSlideArtworkDetails() { @@ -188,21 +175,21 @@ let IkonotvRegisterPiece = React.createClass({ + piece={this.state.piece} />
    ); + } else { + return null; } - return null; }, getSlideLoan() { if (this.canSubmit()) { const { piece, whitelabel } = this.state; - let today = new Moment(); - let endDate = new Moment(); - endDate.add(2, 'years'); + const today = new Moment(); + const endDate = new Moment().add(2, 'years'); return (
    @@ -225,8 +212,9 @@ let IkonotvRegisterPiece = React.createClass({
    ); + } else { + return null; } - return null; }, render() { @@ -252,7 +240,7 @@ let IkonotvRegisterPiece = React.createClass({ submitMessage={getLangText('Register')} isFineUploaderActive={true} handleSuccess={this.handleRegisterSuccess} - location={this.props.location}/> + location={this.props.location} />
    diff --git a/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js b/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js index 1dcdd4e5..4d4f8918 100644 --- a/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js +++ b/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js @@ -30,7 +30,7 @@ let MarketAclButtonList = React.createClass({ componentDidMount() { UserStore.listen(this.onChange); - UserActions.fetchCurrentUser(); + UserActions.fetchCurrentUser.defer(); }, componentWillUnmount() { diff --git a/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js b/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js index d8ef4c41..c839dea0 100644 --- a/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js +++ b/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js @@ -3,6 +3,11 @@ import React from 'react'; import classNames from 'classnames'; +import EditionActions from '../../../../../../actions/edition_actions'; + +import WhitelabelActions from '../../../../../../actions/whitelabel_actions'; +import WhitelabelStore from '../../../../../../stores/whitelabel_store'; + import MarketAdditionalDataForm from '../market_forms/market_additional_data_form'; import AclFormFactory from '../../../../../ascribe_forms/acl_form_factory'; @@ -11,10 +16,7 @@ import ConsignForm from '../../../../../ascribe_forms/form_consign'; import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper'; import AclProxy from '../../../../../acl_proxy'; - -import PieceActions from '../../../../../../actions/piece_actions'; -import WhitelabelActions from '../../../../../../actions/whitelabel_actions'; -import WhitelabelStore from '../../../../../../stores/whitelabel_store'; +import AscribeSpinner from '../../../../../ascribe_spinner'; import ApiUrls from '../../../../../../constants/api_urls'; @@ -26,8 +28,9 @@ let MarketSubmitButton = React.createClass({ availableAcls: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object, editions: React.PropTypes.array.isRequired, - handleSuccess: React.PropTypes.func.isRequired, + className: React.PropTypes.string, + handleSuccess: React.PropTypes.func }, getInitialState() { @@ -50,22 +53,21 @@ let MarketSubmitButton = React.createClass({ canEditionBeSubmitted(edition) { if (edition && edition.extra_data && edition.other_data) { - const { extra_data, other_data } = edition; + const { + extra_data: { + artist_bio: artistBio, + display_instructions: displayInstructions, + technology_details: technologyDetails, + work_description: workDescription + }, + other_data: otherData } = edition; - if (extra_data.artist_bio && extra_data.work_description && - extra_data.technology_details && extra_data.display_instructions && - other_data.length > 0) { - return true; - } + return artistBio && displayInstructions && technologyDetails && workDescription && otherData.length; } return false; }, - getFormDataId() { - return getAclFormDataId(false, this.props.editions); - }, - getAggregateEditionDetails() { const { editions } = this.props; @@ -82,13 +84,20 @@ let MarketSubmitButton = React.createClass({ }); }, - handleAdditionalDataSuccess(pieceId) { - // Fetch newly updated piece to update the views - PieceActions.fetchOne(pieceId); + getFormDataId() { + return getAclFormDataId(false, this.props.editions); + }, + handleAdditionalDataSuccess() { this.refs.consignModal.show(); }, + refreshEdition() { + if (this.props.editions.length === 1) { + EditionActions.fetchEdition(this.props.editions[0].bitcoin_id); + } + }, + render() { const { availableAcls, currentUser, className, editions, handleSuccess } = this.props; const { whitelabel: { name: whitelabelName = 'Market', user: whitelabelAdminEmail } } = this.state; @@ -101,6 +110,10 @@ let MarketSubmitButton = React.createClass({ senderName: currentUser.username }); + // If only a single piece is selected, all the edition's extra_data and other_data will + // be the same, so we just take the first edition's + const { extra_data: extraData, other_data: otherData } = solePieceId ? editions[0] : {}; + const triggerButton = (