diff --git a/js/components/ascribe_detail/detail_property.js b/js/components/ascribe_detail/detail_property.js new file mode 100644 index 00000000..781af6c8 --- /dev/null +++ b/js/components/ascribe_detail/detail_property.js @@ -0,0 +1,55 @@ +'use strict'; + +import React from 'react'; + +let DetailProperty = React.createClass({ + propTypes: { + label: React.PropTypes.string, + value: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.element + ]), + separator: React.PropTypes.string, + labelClassName: React.PropTypes.string, + valueClassName: React.PropTypes.string + }, + + getDefaultProps() { + return { + separator: ':', + labelClassName: 'col-xs-5 col-sm-4 col-md-3 col-lg-3', + valueClassName: 'col-xs-7 col-sm-8 col-md-9 col-lg-9' + }; + }, + + render() { + let value = this.props.value; + if (this.props.children){ + value = ( +
+
+ { this.props.value } +
+
+ { this.props.children } +
+
); + } + return ( +
+
+
+
{ this.props.label + this.props.separator}
+
+
+ {value} +
+
+
+ ); + } +}); + + + +export default DetailProperty; diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 5a25401c..8aef5de6 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -7,21 +7,23 @@ import Row from 'react-bootstrap/lib/Row'; import Col from 'react-bootstrap/lib/Col'; import Button from 'react-bootstrap/lib/Button'; import Glyphicon from 'react-bootstrap/lib/Glyphicon'; -import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin'; import UserActions from '../../actions/user_actions'; import UserStore from '../../stores/user_store'; import CoaActions from '../../actions/coa_actions'; import CoaStore from '../../stores/coa_store'; -import MediaPlayer from './../ascribe_media/media_player'; +import MediaContainer from './media_container'; 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 InputTextAreaToggable from './../ascribe_forms/input_textarea_toggable'; +import EditionHeader from './header'; + import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata'; import RequestActionForm from './../ascribe_forms/form_request_action'; @@ -35,7 +37,6 @@ import GlobalNotificationActions from '../../actions/global_notification_actions import apiUrls from '../../constants/api_urls'; import AppConstants from '../../constants/application_constants'; -import classNames from 'classnames'; import { getCookie } from '../../utils/fetch_api_utils'; @@ -72,10 +73,10 @@ let Edition = React.createClass({ + content={this.props.edition}/> - + @@ -142,106 +143,6 @@ let Edition = React.createClass({ } }); -let MediaContainer = React.createClass({ - propTypes: { - edition: React.PropTypes.object - }, - - render() { - let thumbnail = this.props.edition.thumbnail; - let mimetype = this.props.edition.digital_work.mime; - let embed = null; - let extraData = null; - - if (this.props.edition.digital_work.encoding_urls) { - extraData = this.props.edition.digital_work.encoding_urls.map(e => { return { url: e.url, type: e.label }; }); - } - - if (['video', 'audio'].indexOf(mimetype) > -1){ - embed = ( - - Embed - - } - panel={ -
-                            {''
-                            }
-                        
- }/> - ); - } - return ( -
- -

- - {embed} -

-
- ); - } -}); - -let CollapsibleButton = React.createClass({ - - propTypes: { - button: React.PropTypes.object, - children: React.PropTypes.oneOfType([ - React.PropTypes.object, - React.PropTypes.array - ]) - }, - - getInitialState() { - return {expanded: false}; - }, - handleToggle(e){ - e.preventDefault(); - this.setState({expanded: !this.state.expanded}); - }, - render() { - let isVisible = (this.state.expanded) ? '' : 'invisible'; - return ( - - - {this.props.button} - -
- {this.props.panel} -
-
- ); - } -}); - - -let EditionHeader = React.createClass({ - propTypes: { - edition: React.PropTypes.object - }, - - render() { - var titleHtml =
{this.props.edition.title}
; - return ( -
- - - -
-
- ); - } -}); - let EditionSummary = React.createClass({ propTypes: { @@ -259,7 +160,7 @@ let EditionSummary = React.createClass({ let notification = new GlobalNotificationModel(response.notification, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); }, - render() { + getStatus(){ let status = null; if (this.props.edition.status.length > 0){ let statusStr = this.props.edition.status.join().replace(/_/, ' '); @@ -281,6 +182,9 @@ let EditionSummary = React.createClass({ ); } } + return status; + }, + getActions(){ let actions = null; if (this.props.edition.request_action && this.props.edition.request_action.length > 0){ actions = ( @@ -300,16 +204,18 @@ let EditionSummary = React.createClass({
); } - + return actions; + }, + render() { return (
- {status} + {this.getStatus()}
- {actions} + {this.getActions()}
); @@ -318,54 +224,6 @@ let EditionSummary = React.createClass({ }); -let EditionDetailProperty = React.createClass({ - propTypes: { - label: React.PropTypes.string, - value: React.PropTypes.oneOfType([ - React.PropTypes.string, - React.PropTypes.element - ]), - separator: React.PropTypes.string, - labelClassName: React.PropTypes.string, - valueClassName: React.PropTypes.string - }, - - getDefaultProps() { - return { - separator: ':', - labelClassName: 'col-xs-5 col-sm-4 col-md-3 col-lg-3', - valueClassName: 'col-xs-7 col-sm-8 col-md-9 col-lg-9' - }; - }, - - render() { - let value = this.props.value; - if (this.props.children){ - value = ( -
-
- { this.props.value } -
-
- { this.props.children } -
-
); - } - return ( -
-
-
-
{ this.props.label + this.props.separator}
-
-
- {value} -
-
-
- ); - } -}); - let EditionDetailHistoryIterator = React.createClass({ propTypes: { history: React.PropTypes.array diff --git a/js/components/edition_container.js b/js/components/ascribe_detail/edition_container.js similarity index 89% rename from js/components/edition_container.js rename to js/components/ascribe_detail/edition_container.js index dfefa69e..6eebaeb8 100644 --- a/js/components/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -2,10 +2,10 @@ import React from 'react'; -import EditionActions from '../actions/edition_actions'; -import EditionStore from '../stores/edition_store'; +import EditionActions from '../../actions/edition_actions'; +import EditionStore from '../../stores/edition_store'; -import Edition from './ascribe_detail/edition'; +import Edition from './edition'; /** * This is the component that implements resource/data specific functionality diff --git a/js/components/ascribe_detail/further_details.js b/js/components/ascribe_detail/further_details.js new file mode 100644 index 00000000..c1024cbc --- /dev/null +++ b/js/components/ascribe_detail/further_details.js @@ -0,0 +1,174 @@ +'use strict'; + +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 Property from './../ascribe_forms/property'; + +import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata'; + +import ReactS3FineUploader from './../ascribe_uploader/react_s3_fine_uploader'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import apiUrls from '../../constants/api_urls'; +import AppConstants from '../../constants/application_constants'; + +import { getCookie } from '../../utils/fetch_api_utils'; + +let FurtherDetails = React.createClass({ + propTypes: { + content: React.PropTypes.object, + handleSuccess: React.PropTypes.func + }, + + getInitialState() { + return { + loading: false + }; + }, + + showNotification(){ + this.props.handleSuccess(); + let notification = new GlobalNotificationModel('Details updated', 'success'); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + submitKey(key){ + this.setState({ + otherDataKey: key + }); + }, + + setIsUploadReady(isReady) { + this.setState({ + isUploadReady: isReady + }); + }, + + isReadyForFormSubmission(files) { + files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled'); + if(files.length > 0 && files[0].status === 'upload successful') { + return true; + } else { + return false; + } + }, + + render() { + let editable = this.props.content.acl.indexOf('edit') > -1; + return (); + //return ( + // + // + // + // + // + // + // + // + //); + } +}); + +let FileUploader = React.createClass({ + propTypes: { + content: React.PropTypes.object, + setIsUploadReady: React.PropTypes.func, + submitKey: React.PropTypes.func, + isReadyForFormSubmission: React.PropTypes.func, + editable: React.PropTypes.bool + }, + + render() { + // Essentially there a three cases important to the fileuploader + // + // 1. there is no other_data => do not show the fileuploader at all + // 2. there is other_data, but user has no edit rights => show fileuploader but without action buttons + // 3. both other_data and editable are defined or true => show fileuploade with all action buttons + if (!this.props.editable && !this.props.content.other_data){ + return null; + } + return ( +
+ + + +
+
+ ); + } +}); + +export default FurtherDetails; diff --git a/js/components/ascribe_detail/header.js b/js/components/ascribe_detail/header.js new file mode 100644 index 00000000..e9226a85 --- /dev/null +++ b/js/components/ascribe_detail/header.js @@ -0,0 +1,26 @@ +'use strict'; + +import React from 'react'; + +import EditionDetailProperty from './detail_property'; + + +let Header = React.createClass({ + propTypes: { + content: React.PropTypes.object + }, + + render() { + var titleHtml =
{this.props.content.title}
; + return ( +
+ + + +
+
+ ); + } +}); + +export default Header; \ No newline at end of file diff --git a/js/components/ascribe_detail/media_container.js b/js/components/ascribe_detail/media_container.js new file mode 100644 index 00000000..65d7b96b --- /dev/null +++ b/js/components/ascribe_detail/media_container.js @@ -0,0 +1,62 @@ +'use strict'; + +import React from 'react'; + +import Button from 'react-bootstrap/lib/Button'; +import Glyphicon from 'react-bootstrap/lib/Glyphicon'; + +import MediaPlayer from './../ascribe_media/media_player'; + +import CollapsibleButton from './../ascribe_collapsible/collapsible_button'; + + +let MediaContainer = React.createClass({ + propTypes: { + content: React.PropTypes.object + }, + + render() { + let thumbnail = this.props.content.thumbnail; + let mimetype = this.props.content.digital_work.mime; + let embed = null; + let extraData = null; + + if (this.props.content.digital_work.encoding_urls) { + extraData = this.props.content.digital_work.encoding_urls.map(e => { return { url: e.url, type: e.label }; }); + } + + if (['video', 'audio'].indexOf(mimetype) > -1){ + embed = ( + + Embed + + } + panel={ +
+                            {''
+                            }
+                        
+ }/> + ); + } + return ( +
+ +

+ + {embed} +

+
+ ); + } +}); + +export default MediaContainer; diff --git a/js/components/ascribe_detail/piece.js b/js/components/ascribe_detail/piece.js new file mode 100644 index 00000000..73f9eb67 --- /dev/null +++ b/js/components/ascribe_detail/piece.js @@ -0,0 +1,71 @@ +'use strict'; + +import React from 'react'; + +import Row from 'react-bootstrap/lib/Row'; +import Col from 'react-bootstrap/lib/Col'; + +import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph'; + +import FurtherDetails from './further_details'; +//import UserActions from '../../actions/user_actions'; +//import UserStore from '../../stores/user_store'; + +import MediaContainer from './media_container'; + +import Header from './header'; + +/** + * This is the component that implements display-specific functionality + */ +let Piece = React.createClass({ + propTypes: { + piece: React.PropTypes.object, + loadPiece: React.PropTypes.func + }, + + //getInitialState() { + // return UserStore.getState(); + //}, + // + //componentDidMount() { + // UserStore.listen(this.onChange); + // UserActions.fetchCurrentUser(); + //}, + // + //componentWillUnmount() { + // UserStore.unlisten(this.onChange); + //}, + // + //onChange(state) { + // this.setState(state); + //}, + + render() { + + return ( + + + + + +
+ -1 + || Object.keys(this.props.piece.extra_data).length > 0 + || this.props.piece.other_data !== null}> + + + + + + ); + } +}); + +export default Piece; diff --git a/js/components/ascribe_detail/piece_container.js b/js/components/ascribe_detail/piece_container.js new file mode 100644 index 00000000..dfd36622 --- /dev/null +++ b/js/components/ascribe_detail/piece_container.js @@ -0,0 +1,57 @@ +'use strict'; + +import React from 'react'; + +import PieceActions from '../../actions/piece_actions'; +import PieceStore from '../../stores/piece_store'; + +import Piece from './piece'; + +/** + * This is the component that implements resource/data specific functionality + */ +let PieceContainer = React.createClass({ + getInitialState() { + return PieceStore.getState(); + }, + + onChange(state) { + this.setState(state); + }, + + componentDidMount() { + PieceStore.listen(this.onChange); + PieceActions.fetchOne(this.props.params.pieceId); + }, + + 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 + PieceActions.updatePiece({}); + + PieceStore.unlisten(this.onChange); + }, + + + loadPiece() { + PieceActions.fetchOne(this.props.params.pieceId); + }, + + render() { + if('title' in this.state.piece) { + return ( + + ); + } else { + return ( +

Loading

+ ); + } + } +}); + +export default PieceContainer; diff --git a/js/components/register_piece.js b/js/components/register_piece.js index cda4bf4b..00d37432 100644 --- a/js/components/register_piece.js +++ b/js/components/register_piece.js @@ -91,7 +91,7 @@ let RegisterPiece = React.createClass( { this.state.orderBy, this.state.orderAsc); - this.transitionTo('edition', {editionId: response.piece.bitcoin_id}); + this.transitionTo('piece', {editionId: response.piece.id}); }, getFormData(){ diff --git a/js/constants/api_urls.js b/js/constants/api_urls.js index de8d4abf..873dadd0 100644 --- a/js/constants/api_urls.js +++ b/js/constants/api_urls.js @@ -30,7 +30,7 @@ let apiUrls = { 'ownership_unconsigns': AppConstants.apiEndpoint + 'ownership/unconsigns/', 'ownership_unconsigns_deny': AppConstants.apiEndpoint + 'ownership/unconsigns/deny/', 'ownership_unconsigns_request': AppConstants.apiEndpoint + 'ownership/unconsigns/request/', - 'piece': AppConstants.apiEndpoint + 'pieces/${piece_id}', + 'piece': AppConstants.apiEndpoint + 'pieces/${piece_id}/', 'piece_extradata': AppConstants.apiEndpoint + 'pieces/${piece_id}/extradata/', 'piece_first_edition_id': AppConstants.apiEndpoint + 'pieces/${piece_id}/edition_index/', 'pieces_list': AppConstants.apiEndpoint + 'pieces/', diff --git a/js/fetchers/piece_fetcher.js b/js/fetchers/piece_fetcher.js index c7629d89..481b79fe 100644 --- a/js/fetchers/piece_fetcher.js +++ b/js/fetchers/piece_fetcher.js @@ -5,11 +5,10 @@ import requests from '../utils/requests'; let PieceFetcher = { /** - * Fetch one user from the API. - * If no arg is supplied, load the current user + * Fetch a piece from the API. */ - fetchOne() { - return requests.get('piece'); + fetchOne(id) { + return requests.get('piece', {'piece_id': id}); } }; diff --git a/js/routes.js b/js/routes.js index 7dc34687..db8ed171 100644 --- a/js/routes.js +++ b/js/routes.js @@ -5,7 +5,8 @@ import Router from 'react-router'; import AscribeApp from './components/ascribe_app'; import PieceList from './components/piece_list'; -import EditionContainer from './components/edition_container'; +import PieceContainer from './components/ascribe_detail/piece_container'; +import EditionContainer from './components/ascribe_detail/edition_container'; import LoginContainer from './components/login_container'; import SignupContainer from './components/signup_container'; @@ -26,6 +27,7 @@ let routes = ( +