refactored piece container and detail

This commit is contained in:
diminator 2015-08-11 17:12:12 +02:00
parent d5efd79d14
commit 22727c2703
9 changed files with 312 additions and 193 deletions

View File

@ -23,7 +23,7 @@ const CollapsibleParagraph = React.createClass({
getInitialState() {
return {
expanded: false
expanded: this.props.defaultExpanded
};
},

View File

@ -18,8 +18,8 @@ let DetailProperty = React.createClass({
getDefaultProps() {
return {
separator: ':',
labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-2',
valueClassName: 'col-xs-9 col-sm-9 col-md-10 col-lg-10'
labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-2 col-xs-height col-bottom ascribe-detail-property-label',
valueClassName: 'col-xs-9 col-sm-9 col-md-10 col-lg-10 col-xs-height col-bottom ascribe-detail-property-value'
};
},
@ -52,11 +52,11 @@ let DetailProperty = React.createClass({
return (
<div className="row ascribe-detail-property">
<div className="row-same-height">
<div className={this.props.labelClassName + ' col-xs-height col-bottom ascribe-detail-property-label'}>
{ this.props.label + this.props.separator}
<div className={this.props.labelClassName}>
{ this.props.label } { this.props.separator}
</div>
<div
className={this.props.valueClassName + ' col-xs-height col-bottom ascribe-detail-property-value'}
className={this.props.valueClassName}
style={styles}>
{value}
</div>

View File

@ -19,7 +19,33 @@ const EMBED_IFRAME_HEIGHT = {
let MediaContainer = React.createClass({
propTypes: {
content: React.PropTypes.object
content: React.PropTypes.object,
refreshObject: React.PropTypes.func
},
getInitialState() {
return {timerId: null};
},
componentDidMount() {
if (!this.props.content.digital_work) {
return;
}
let 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});
}
},
componentWillUpdate() {
if (this.props.content.digital_work.isEncoding === 100) {
window.clearInterval(this.state.timerId);
}
},
componentWillUnmount() {
window.clearInterval(this.state.timerId);
},
render() {

View File

@ -1,38 +1,14 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import DetailProperty from './detail_property';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import EditionListActions from '../../actions/edition_list_actions';
import PieceActions from '../../actions/piece_actions';
import MediaContainer from './media_container';
import EditionDetailProperty from './detail_property';
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 GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
/**
* This is the component that implements display-specific functionality
@ -40,97 +16,16 @@ import { mergeOptions } from '../../utils/general_utils';
let Piece = React.createClass({
propTypes: {
piece: React.PropTypes.object,
header: React.PropTypes.object,
subheader: React.PropTypes.object,
buttons: React.PropTypes.object,
loadPiece: React.PropTypes.func,
children: React.PropTypes.object
},
mixins: [Router.Navigation],
getInitialState() {
return mergeOptions(
UserStore.getState(),
PieceListStore.getState(),
{
showCreateEditionsDialog: false
}
);
},
componentDidMount() {
UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
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.transitionTo('pieces');
},
getCreateEditionsDialog() {
if(this.props.piece.num_editions < 1 && this.state.showCreateEditionsDialog) {
return (
<div style={{marginTop: '1em'}}>
<CreateEditionsForm
pieceId={this.props.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);
updateObject() {
return PieceActions.fetchOne(this.props.piece.id);
},
render() {
@ -138,38 +33,14 @@ let Piece = React.createClass({
<Row>
<Col md={6}>
<MediaContainer
refreshObject={this.updateObject}
content={this.props.piece}/>
</Col>
<Col md={6} className="ascribe-edition-details">
<div className="ascribe-detail-header">
<h1 className="ascribe-detail-title">{this.props.piece.title}</h1>
<hr/>
<EditionDetailProperty label="BY" value={this.props.piece.artist_name} />
<EditionDetailProperty label="DATE" value={ this.props.piece.date_created.slice(0, 4) } />
{this.props.piece.num_editions > 0 ? <EditionDetailProperty label="EDITIONS" value={ this.props.piece.num_editions } /> : null}
<hr/>
</div>
<div className="ascribe-detail-header">
<DetailProperty label={getLangText('REGISTREE')} value={ this.props.piece.user_registered } />
</div>
{this.props.header}
{this.props.subheader}
{this.props.buttons}
<AclButtonList
className="text-center ascribe-button-list"
availableAcls={this.props.piece.acl}
editions={this.props.piece}
handleSuccess={this.props.loadPiece}>
<CreateEditionsButton
label={getLangText('CREATE EDITIONS')}
className="btn-sm"
piece={this.props.piece}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.handlePollingSuccess}/>
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
piece={this.props.piece}/>
</AclButtonList>
{this.getCreateEditionsDialog()}
{this.props.children}
</Col>

View File

@ -1,37 +1,59 @@
'use strict';
import React from 'react';
import Router from 'react-router';
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 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 GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import AppConstants from '../../constants/application_constants';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
/**
* This is the component that implements resource/data specific functionality
*/
let PieceContainer = React.createClass({
getInitialState() {
return PieceStore.getState();
},
onChange(state) {
this.setState(state);
if (!state.piece.digital_work) {
return;
}
let isEncoding = state.piece.digital_work.isEncoding;
if (state.piece.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) {
let timerId = window.setInterval(() => PieceActions.fetchOne(this.props.params.pieceId), 10000);
this.setState({timerId: timerId});
}
mixins: [Router.Navigation],
getInitialState() {
return mergeOptions(
UserStore.getState(),
PieceListStore.getState(),
PieceStore.getState(),
{
showCreateEditionsDialog: false
}
);
},
componentDidMount() {
UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
UserActions.fetchCurrentUser();
PieceStore.listen(this.onChange);
PieceActions.fetchOne(this.props.params.pieceId);
},
@ -42,21 +64,121 @@ let PieceContainer = React.createClass({
// as it will otherwise display wrong/old data once the user loads
// the piece detail a second time
PieceActions.updatePiece({});
window.clearInterval(this.state.timerId);
PieceStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange);
},
onChange(state) {
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.transitionTo('pieces');
},
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);
},
render() {
if('title' in this.state.piece) {
return (
<Piece
piece={this.state.piece}
loadPiece={this.loadPiece}>
loadPiece={this.loadPiece}
header={
<div className="ascribe-detail-header">
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1>
<hr/>
<DetailProperty label="BY" value={this.state.piece.artist_name} />
<DetailProperty label="DATE" value={ this.state.piece.date_created.slice(0, 4) } />
{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 } />
</div>
}
buttons={
<AclButtonList
className="text-center ascribe-button-list"
availableAcls={this.state.piece.acl}
editions={this.state.piece}
handleSuccess={this.state.loadPiece}>
<CreateEditionsButton
label={getLangText('CREATE EDITIONS')}
className="btn-sm"
piece={this.state.piece}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.handlePollingSuccess}/>
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
piece={this.state.piece}/>
</AclButtonList>
}>
{this.getCreateEditionsDialog()}
<CollapsibleParagraph
title="Further Details"
show={this.state.piece.acl.acl_edit

View File

@ -24,7 +24,7 @@ import { mergeOptions } from '../utils/general_utils';
let PieceList = React.createClass({
propTypes: {
accordionListItemType: React.PropTypes.object,
accordionListItemType: React.PropTypes.func,
redirectTo: React.PropTypes.string,
customSubmitButton: React.PropTypes.element
},

View File

@ -1,13 +1,15 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import StarRating from 'react-star-rating';
import AccordionListItemPiece from '../../../../ascribe_accordion_list/accordion_list_item_piece';
import PieceListActions from '../../../../../actions/piece_list_actions';
import PieceListStore from '../../../../../stores/piece_list_store';
import WhitelabelStore from '../../../../../stores/whitelabel_store';
import UserStore from '../../../../../stores/user_store';
import GlobalNotificationModel from '../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../actions/global_notification_actions';
@ -15,11 +17,14 @@ import GlobalNotificationActions from '../../../../../actions/global_notificatio
import AclProxy from '../../../../acl_proxy';
import SubmitToPrizeButton from './../ascribe_buttons/submit_to_prize_button';
import { getLangText } from '../../../../../utils/lang_utils';
import { mergeOptions } from '../../../../../utils/general_utils';
let Link = Router.Link;
let AccordionListItemWallet = React.createClass({
let AccordionListItemPrize = React.createClass({
propTypes: {
className: React.PropTypes.string,
content: React.PropTypes.object,
@ -32,18 +37,18 @@ let AccordionListItemWallet = React.createClass({
getInitialState() {
return mergeOptions(
PieceListStore.getState(),
WhitelabelStore.getState()
UserStore.getState()
);
},
componentDidMount() {
PieceListStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
UserStore.listen(this.onChange);
},
componentWillUnmount() {
PieceListStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
},
onChange(state) {
@ -58,6 +63,51 @@ let AccordionListItemWallet = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification);
},
getPrizeButtons() {
if (this.state.currentUser && this.state.currentUser.is_jury){
if (this.props.content.ratings && this.props.content.ratings.rating){
// jury and rating available
let rating = parseInt(this.props.content.ratings.rating, 10);
return (
<div className="pull-right">
<Link to='piece' params={{pieceId: this.props.content.id}}>
<StarRating
ref='rating'
name="prize-rating"
caption="Your rating"
step={1}
size='sm'
rating={rating}
ratingAmount={5} />
</Link>
</div>);
}
else {
// jury and no rating yet
return (
<div className="react-rating-caption pull-right">
<Link to='piece' params={{pieceId: this.props.content.id}}>
Submit your rating
</Link>
</div>
);
}
}
// participant
return (
<div>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_submit_to_prize">
<SubmitToPrizeButton
className="pull-right"
piece={this.props.content}
handleSuccess={this.handleSubmitPrizeSuccess}/>
</AclProxy>
</div>
);
},
render() {
return (
@ -68,22 +118,11 @@ let AccordionListItemWallet = React.createClass({
<div className="pull-left">
<span>{this.props.content.date_created.split('-')[0]}</span>
</div>}
buttons={
<div>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_submit_to_prize">
<SubmitToPrizeButton
className="pull-right"
piece={this.props.content}
handleSuccess={this.handleSubmitPrizeSuccess}/>
</AclProxy>
</div>}
>
buttons={this.getPrizeButtons()}>
{this.props.children}
</AccordionListItemPiece>
);
}
});
export default AccordionListItemWallet;
export default AccordionListItemPrize;

View File

@ -7,6 +7,9 @@ import StarRating from 'react-star-rating';
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 PrizeRatingActions from '../../actions/prize_rating_actions';
import PrizeRatingStore from '../../stores/prize_rating_store';
@ -19,6 +22,10 @@ import Property from '../../../../../components/ascribe_forms/property';
import InputTextAreaToggable from '../../../../../components/ascribe_forms/input_textarea_toggable';
import CollapsibleParagraph from '../../../../../components/ascribe_collapsible/collapsible_paragraph';
import DetailProperty from '../../../../ascribe_detail/detail_property';
import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils';
/**
* This is the component that implements resource/data specific functionality
*/
@ -27,10 +34,6 @@ let PieceContainer = React.createClass({
return PieceStore.getState();
},
onChange(state) {
this.setState(state);
},
componentDidMount() {
PieceStore.listen(this.onChange);
PieceActions.fetchOne(this.props.params.pieceId);
@ -42,10 +45,12 @@ let PieceContainer = React.createClass({
// as it will otherwise display wrong/old data once the user loads
// the piece detail a second time
PieceActions.updatePiece({});
PieceStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
loadPiece() {
PieceActions.fetchOne(this.props.params.pieceId);
@ -56,7 +61,19 @@ let PieceContainer = React.createClass({
return (
<Piece
piece={this.state.piece}
loadPiece={this.loadPiece}>
loadPiece={this.loadPiece}
header={
<div className="ascribe-detail-header">
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1>
<hr/>
<DetailProperty label="BY" value={this.state.piece.artist_name} />
<DetailProperty label="DATE" value={ this.state.piece.date_created.slice(0, 4) } />
<hr/>
</div>
}
subheader={
<PrizePieceRatings piece={this.state.piece}/>
}>
<PrizePieceDetails piece={this.state.piece}/>
</Piece>
);
@ -70,23 +87,22 @@ let PieceContainer = React.createClass({
}
});
let PrizePieceDetails = React.createClass({
let PrizePieceRatings = React.createClass({
propTypes: {
piece: React.PropTypes.object
},
getInitialState() {
return PrizeRatingStore.getState();
},
onChange(state) {
this.setState(state);
return mergeOptions(
PieceListStore.getState(),
PrizeRatingStore.getState()
);
},
componentDidMount() {
PrizeRatingStore.listen(this.onChange);
PrizeRatingActions.fetchOne(this.props.piece.id);
PieceListStore.listen(this.onChange);
},
componentWillUnmount() {
@ -96,11 +112,52 @@ let PrizePieceDetails = React.createClass({
// the piece detail a second time
PrizeRatingActions.updateRating({});
PrizeRatingStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
if (this.refs.rating) {
this.refs.rating.state.ratingCache = {
pos: this.refs.rating.state.pos,
rating: this.state.currentRating,
caption: this.refs.rating.props.caption,
name: this.refs.rating.props.name
};
}
},
onRatingClick(event, args) {
event.preventDefault();
PrizeRatingActions.createRating(this.props.piece.id, args.rating);
PrizeRatingActions.createRating(this.props.piece.id, args.rating).then(
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy)
);
},
render(){
return (
<DetailProperty
labelClassName='col-xs-3 col-sm-3 col-md-2 col-lg-2 col-xs-height col-middle ascribe-detail-property-label'
label={
<span>YOUR VOTE</span>
}
value={
<StarRating
ref='rating'
name="prize-rating"
caption=""
step={1}
size='md'
rating={this.state.currentRating}
onRatingClick={this.onRatingClick}
ratingAmount={5} />}
/>);
}
});
let PrizePieceDetails = React.createClass({
propTypes: {
piece: React.PropTypes.object
},
render() {

View File

@ -387,4 +387,8 @@ hr {
.rating-container .rating-stars {
width: 25px;
color: #000;
}
.react-rating-caption {
font-size: 1em;
}