'use strict'; import React from 'react'; import { History } from 'react-router'; import Moment from 'moment'; import ReactError from '../../mixins/react_error'; import { ResourceNotFoundError } from '../../models/errors'; import PieceActions from '../../actions/piece_actions'; import PieceStore from '../../stores/piece_store'; import PieceListActions from '../../actions/piece_list_actions'; import PieceListStore from '../../stores/piece_list_store'; import UserActions from '../../actions/user_actions'; import UserStore from '../../stores/user_store'; import EditionListActions from '../../actions/edition_list_actions'; import Piece from './piece'; import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph'; import FurtherDetails from './further_details'; import DetailProperty from './detail_property'; import LicenseDetail from './license_detail'; import HistoryIterator from './history_iterator'; import AclButtonList from './../ascribe_buttons/acl_button_list'; import CreateEditionsForm from '../ascribe_forms/create_editions_form'; import CreateEditionsButton from '../ascribe_buttons/create_editions_button'; import DeleteButton from '../ascribe_buttons/delete_button'; import AclInformation from '../ascribe_buttons/acl_information'; import AclProxy from '../acl_proxy'; import ListRequestActions from '../ascribe_forms/list_form_request_actions'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; import Note from './note'; import ApiUrls from '../../constants/api_urls'; import AscribeSpinner from '../ascribe_spinner'; import { mergeOptions } from '../../utils/general_utils'; import { getLangText } from '../../utils/lang_utils'; import { setDocumentTitle } from '../../utils/dom_utils'; /** * This is the component that implements resource/data specific functionality */ let PieceContainer = React.createClass({ propTypes: { furtherDetailsType: React.PropTypes.func, params: React.PropTypes.object }, mixins: [History, ReactError], getDefaultProps() { return { furtherDetailsType: FurtherDetails }; }, getInitialState() { return mergeOptions( UserStore.getState(), PieceListStore.getState(), PieceStore.getState(), { showCreateEditionsDialog: false } ); }, componentDidMount() { UserStore.listen(this.onChange); 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; if (pieceError && pieceError.status === 404) { this.throws(new ResourceNotFoundError(getLangText("Oops, the piece you're looking for doesn't exist."))); } }, componentWillUnmount() { PieceStore.unlisten(this.onChange); UserStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange); }, onChange(state) { /* ATTENTION: THIS IS JUST A TEMPORARY USABILITY FIX THAT ESSENTIALLY REMOVES THE LOAN BUTTON FROM THE PIECE DETAIL PAGE SO THAT USERS DO NOT CONFUSE A PIECE WITH AN EDITION. IT SHOULD BE REMOVED AND REPLACED WITH A BETTER SOLUTION ASAP! 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') { let pieceState = mergeOptions({}, state.piece); pieceState.acl.acl_loan = false; this.setState({ piece: pieceState }); } else { this.setState(state); } }, loadPiece() { PieceActions.fetchOne(this.props.params.pieceId); }, toggleCreateEditionsDialog() { this.setState({ showCreateEditionsDialog: !this.state.showCreateEditionsDialog }); }, 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); this.toggleCreateEditionsDialog(); }, handleDeleteSuccess(response) { PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, this.state.orderBy, this.state.orderAsc, this.state.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'); GlobalNotificationActions.appendGlobalNotification(notification); this.history.pushState(null, '/collection'); }, getCreateEditionsDialog() { if(this.state.piece.num_editions < 1 && this.state.showCreateEditionsDialog) { return ( <div style={{marginTop: '1em'}}> <CreateEditionsForm pieceId={this.state.piece.id} handleSuccess={this.handleEditionCreationSuccess} /> <hr/> </div> ); } else { return (<hr/>); } }, handlePollingSuccess(pieceId, numEditions) { // we need to refresh the num_editions property of the actual piece we're looking at PieceActions.updateProperty({ key: 'num_editions', value: numEditions }); // as well as its representation in the collection // 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); let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, getId() { return {'id': this.state.piece.id}; }, getActions() { const { piece, currentUser } = this.state; if (piece && piece.notifications && piece.notifications.length > 0) { return ( <ListRequestActions pieceOrEditions={piece} currentUser={currentUser} handleSuccess={this.loadPiece} notifications={piece.notifications}/>); } else { return ( <AclProxy show={currentUser && currentUser.email && Object.keys(piece.acl).length > 1}> {/* `acl_view` is always available in `edition.acl`, therefore if it has no more than 1 key, we're hiding the `DetailProperty` actions as otherwise `AclInformation` would show up */} <DetailProperty label={getLangText('ACTIONS')}> <AclButtonList className="ascribe-button-list" availableAcls={piece.acl} pieceOrEditions={piece} handleSuccess={this.loadPiece}> <CreateEditionsButton label={getLangText('CREATE EDITIONS')} className="btn-sm" piece={piece} toggleCreateEditionsDialog={this.toggleCreateEditionsDialog} onPollingSuccess={this.handlePollingSuccess}/> <DeleteButton handleSuccess={this.handleDeleteSuccess} piece={piece}/> <AclInformation aim="button" verbs={['acl_share', 'acl_transfer', 'acl_create_editions', 'acl_loan', 'acl_delete', 'acl_consign']} aclObject={piece.acl}/> </AclButtonList> </DetailProperty> </AclProxy> ); } }, 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(', ')); return ( <Piece piece={this.state.piece} loadPiece={this.loadPiece} header={ <div className="ascribe-detail-header"> <hr style={{marginTop: 0}}/> <h1 className="ascribe-detail-title">{this.state.piece.title}</h1> <DetailProperty label="BY" value={this.state.piece.artist_name} /> <DetailProperty label="DATE" value={Moment(this.state.piece.date_created, 'YYYY-MM-DD').year() } /> {this.state.piece.num_editions > 0 ? <DetailProperty label="EDITIONS" value={ this.state.piece.num_editions } /> : null} <hr/> </div> } subheader={ <div className="ascribe-detail-header"> <DetailProperty label={getLangText('REGISTREE')} value={ this.state.piece.user_registered } /> <DetailProperty label={getLangText('ID')} value={ this.state.piece.bitcoin_id } ellipsis={true} /> <LicenseDetail license={this.state.piece.license_type} /> </div> } buttons={this.getActions()}> {this.getCreateEditionsDialog()} <CollapsibleParagraph title={getLangText('Loan History')} show={this.state.piece.loan_history && this.state.piece.loan_history.length > 0}> <HistoryIterator history={this.state.piece.loan_history} /> </CollapsibleParagraph> <CollapsibleParagraph title={getLangText('Notes')} show={!!(this.state.currentUser.username || this.state.piece.acl.acl_edit || this.state.piece.public_note)}> <Note id={this.getId} label={getLangText('Personal note (private)')} defaultValue={this.state.piece.private_note || null} show = {!!this.state.currentUser.username} placeholder={getLangText('Enter your comments ...')} editable={true} successMessage={getLangText('Private note saved')} url={ApiUrls.note_private_piece} currentUser={this.state.currentUser}/> <Note id={this.getId} label={getLangText('Personal note (public)')} defaultValue={this.state.piece.public_note || null} placeholder={getLangText('Enter your comments ...')} editable={!!this.state.piece.acl.acl_edit} show={!!(this.state.piece.public_note || this.state.piece.acl.acl_edit)} successMessage={getLangText('Public note saved')} url={ApiUrls.note_public_piece} currentUser={this.state.currentUser}/> </CollapsibleParagraph> <CollapsibleParagraph title={getLangText('Further Details')} show={this.state.piece.acl.acl_edit || Object.keys(this.state.piece.extra_data).length > 0 || this.state.piece.other_data.length > 0} defaultExpanded={true}> <FurtherDetailsType editable={this.state.piece.acl.acl_edit} pieceId={this.state.piece.id} extraData={this.state.piece.extra_data} otherData={this.state.piece.other_data} handleSuccess={this.loadPiece} /> </CollapsibleParagraph> </Piece> ); } else { return ( <div className="fullpage-spinner"> <AscribeSpinner color='dark-blue' size='lg'/> </div> ); } } }); export default PieceContainer;