diff --git a/js/actions/piece_list_actions.js b/js/actions/piece_list_actions.js index eb9a5403..188935f7 100644 --- a/js/actions/piece_list_actions.js +++ b/js/actions/piece_list_actions.js @@ -4,12 +4,12 @@ import alt from '../alt'; import PieceListFetcher from '../fetchers/piece_list_fetcher'; - class PieceListActions { constructor() { this.generateActions( 'updatePieceList', - 'updatePieceListRequestActions' + 'updatePieceListRequestActions', + 'addFirstEditionToPiece' ); } @@ -32,6 +32,7 @@ class PieceListActions { .catch((err) => reject(err)); }); } + fetchPieceRequestActions() { PieceListFetcher .fetchRequestActions() @@ -40,6 +41,16 @@ class PieceListActions { }); } + fetchFirstEditionForPiece(pieceId) { + return new Promise((resolve, reject) => { + PieceListFetcher.fetchFirstEditionForPiece(pieceId) + .then((firstEdition) => { + this.actions.addFirstEditionToPiece({pieceId, firstEdition}); + resolve(); + }) + .catch((err) => reject(err)); + }); + } } export default alt.createActions(PieceListActions); diff --git a/js/components/ascribe_accordion_list/accordion_list_item.js b/js/components/ascribe_accordion_list/accordion_list_item.js index e9ab9aeb..69d4bae5 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item.js +++ b/js/components/ascribe_accordion_list/accordion_list_item.js @@ -3,28 +3,36 @@ import React from 'react'; import Router from 'react-router'; +import PieceListStore from '../../stores/piece_list_store'; +import PieceListActions from '../../actions/piece_list_actions'; + import Glyphicon from 'react-bootstrap/lib/Glyphicon'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import Tooltip from 'react-bootstrap/lib/Tooltip'; -import requests from '../../utils/requests'; +import AccordionListItemEditionWidget from './accordion_list_item_edition_widget'; import { getLangText } from '../../utils/lang_utils'; let AccordionListItem = React.createClass({ - mixins: [Router.Navigation], - propTypes: { className: React.PropTypes.string, content: React.PropTypes.object, children: React.PropTypes.object }, - handleClick(event){ - requests.get('piece_first_edition_id', {'piece_id': this.props.content.id}) - .then((res) => this.transitionTo('edition', {editionId: res.bitcoin_id})); - console.log(event.target); + mixins: [Router.Navigation], + + componentDidMount() { + if(this.props.content.num_editions !== 0) { + PieceListActions.fetchFirstEditionForPiece(this.props.content.id); + } }, + + onChange(state) { + this.setState(state); + }, + getGlyphicon(){ if (this.props.content.requestAction){ return ( @@ -35,6 +43,7 @@ let AccordionListItem = React.createClass({ } return null; }, + render() { return (
@@ -53,9 +62,12 @@ let AccordionListItem = React.createClass({

{getLangText('by %s', this.props.content.artist_name)}

{this.props.content.date_created.split('-')[0]} - + + {/* {getLangText('%s license', this.props.content.license_type.code)} - + */} +
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 new file mode 100644 index 00000000..2bc86016 --- /dev/null +++ b/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js @@ -0,0 +1,93 @@ +'use strict'; + +import React from 'react'; + +import EditionListActions from '../../actions/edition_list_actions'; +import EditionListStore from '../../stores/edition_list_store'; + +import { getLangText } from '../../utils/lang_utils'; +import { mergeOptions } from '../../utils/general_utils'; + +let AccordionListItemEditionWidget = React.createClass({ + propTypes: { + piece: React.PropTypes.object.isRequired + }, + + getInitialState() { + return EditionListStore.getState(); + }, + + componentDidMount() { + EditionListStore.listen(this.onChange); + }, + + componentWillUnmount() { + EditionListStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + /** + * 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; + + if(isEditionListOpen) { + EditionListActions.toggleEditionList(pieceId); + } else { + EditionListActions.toggleEditionList(pieceId); + EditionListActions.fetchEditionList(pieceId); + } + }, + + /** + * Depending on the state of isEditionListOpenForPieceId we either want to display + * a glyphicon arrow pointing upwards or downwards + */ + getGlyphicon() { + let pieceId = this.props.piece.id; + let isEditionListOpen = this.state.isEditionListOpenForPieceId[pieceId] ? this.state.isEditionListOpenForPieceId[pieceId].show : false; + + if(isEditionListOpen) { + return ( + + ); + } else { + return ( + + ); + } + }, + + render() { + let piece = this.props.piece; + let numEditions = piece.num_editions; + + if(numEditions === 1) { + let firstEditionId = piece && piece.firstEdition ? ', ' + piece.firstEdition.bitcoin_id : ''; + let editionMapping = piece && piece.firstEdition ? piece.firstEdition.edition_number + '/' + piece.num_editions : ''; + + return ( + + {this.getGlyphicon()} {editionMapping + ' ' + getLangText('Edition') + firstEditionId} + + ); + } else { + return ( + + {this.getGlyphicon()} {numEditions + ' ' + getLangText('Editions')} + + ); + } + } +}); + +export default AccordionListItemEditionWidget; \ No newline at end of file diff --git a/js/components/ascribe_accordion_list/accordion_list_item_table.js b/js/components/ascribe_accordion_list/accordion_list_item_table.js index ac4e6591..9f92c006 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_table.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_table.js @@ -3,7 +3,7 @@ import React from 'react'; import Table from '../ascribe_table/table'; -import TableItem from '../ascribe_table/table_item'; +import TableItemSelectable from '../ascribe_table/table_item_selectable'; import { ColumnModel } from '../ascribe_table/models/table_models'; @@ -16,7 +16,8 @@ let AccordionListItemTable = React.createClass({ show: React.PropTypes.bool, changeOrder: React.PropTypes.func, orderBy: React.PropTypes.string, - orderAsc: React.PropTypes.bool + orderAsc: React.PropTypes.bool, + selectItem: React.PropTypes.func }, render() { @@ -32,9 +33,11 @@ let AccordionListItemTable = React.createClass({ orderAsc={this.props.orderAsc}> {this.props.itemList.map((item, i) => { return ( - + key={i} + selectItem={this.props.selectItem} + parentId={this.props.parentId}/> ); })} 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 1f83d218..1a34464d 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 @@ -69,16 +69,6 @@ let AccordionListItemTableEditions = React.createClass({ return selectedEditions; }, - toggleTable() { - let isEditionListOpen = this.state.isEditionListOpenForPieceId[this.props.parentId] ? this.state.isEditionListOpenForPieceId[this.props.parentId].show : false; - if(isEditionListOpen) { - EditionListActions.toggleEditionList(this.props.parentId); - } else { - EditionListActions.toggleEditionList(this.props.parentId); - EditionListActions.fetchEditionList(this.props.parentId); - } - }, - loadFurtherEditions() { // trigger loading animation this.setState({ @@ -187,14 +177,14 @@ let AccordionListItemTableEditions = React.createClass({ ) ]; - let loadingSpinner = ; + let loadingSpinner = ; return (
- {getLangText('Hide editions')} : {getLangText('Show editions')} {show && typeof editionsForPiece === 'undefined' ? loadingSpinner : null}} /> + message={show && typeof editionsForPiece !== 'undefined' ? {getLangText('Hide editions')} : {getLangText('Show editions')} {show && typeof editionsForPiece === 'undefined' ? loadingSpinner : null}} /> */} + changeOrder={this.changeEditionListOrder} + selectItem={this.selectItem}/>
- {this.props.title}{text} + {text} {this.props.title}
{this.props.children} diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 881cede9..8857b188 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -39,6 +39,7 @@ import apiUrls from '../../constants/api_urls'; import AppConstants from '../../constants/application_constants'; import { getCookie } from '../../utils/fetch_api_utils'; +import { getLangText } from '../../utils/lang_utils'; let Link = Router.Link; /** diff --git a/js/components/ascribe_detail/edition_container.js b/js/components/ascribe_detail/edition_container.js index bd280ab3..45c91bd0 100644 --- a/js/components/ascribe_detail/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -17,10 +17,10 @@ let EditionContainer = React.createClass({ onChange(state) { this.setState(state); - let isEncoding = state.edition.digital_work.isEncoding; - if (isEncoding !== undefined && isEncoding !== 100) { + let isEncoding = state.edition.digital_work ? state.edition.digital_work.isEncoding : null; + if (typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { let timerId = window.setInterval(() => EditionActions.fetchOne(this.props.params.editionId), 10000); - this.setState({timerId: timerId}) + this.setState({timerId: timerId}); } }, diff --git a/js/components/ascribe_forms/form.js b/js/components/ascribe_forms/form.js index 4bda9c97..414c1aab 100644 --- a/js/components/ascribe_forms/form.js +++ b/js/components/ascribe_forms/form.js @@ -18,7 +18,8 @@ let Form = React.createClass({ children: React.PropTypes.oneOfType([ React.PropTypes.object, React.PropTypes.array - ]) + ]), + className: React.PropTypes.string }, getInitialState() { @@ -141,10 +142,16 @@ let Form = React.createClass({ }); }, render() { + let className = 'ascribe-form'; + + if(this.props.className) { + className += ' ' + this.props.className; + } + return (
{this.getErrors()} diff --git a/js/components/ascribe_forms/form_login.js b/js/components/ascribe_forms/form_login.js index 3402ba4c..fc44b08b 100644 --- a/js/components/ascribe_forms/form_login.js +++ b/js/components/ascribe_forms/form_login.js @@ -9,7 +9,7 @@ import ButtonSubmitOrClose from '../ascribe_buttons/button_submit_close'; import SignupModal from '../ascribe_modal/modal_signup'; import PasswordResetRequestModal from '../ascribe_modal/modal_password_request_reset'; -import { getLangText } from '../../utils/lang_utils.js' +import { getLangText } from '../../utils/lang_utils.js'; let LoginForm = React.createClass({ mixins: [FormMixin], diff --git a/js/components/ascribe_forms/form_property_header.js b/js/components/ascribe_forms/form_property_header.js new file mode 100644 index 00000000..870ac763 --- /dev/null +++ b/js/components/ascribe_forms/form_property_header.js @@ -0,0 +1,19 @@ +'use strict'; + +import React from 'react'; + +let FormPropertyHeader = React.createClass({ + propTypes: { + children: React.PropTypes.arrayOf(React.PropTypes.element) + }, + + render() { + return ( +
+ {this.props.children} +
+ ); + } +}); + +export default FormPropertyHeader; \ No newline at end of file diff --git a/js/components/ascribe_forms/property_collapsible.js b/js/components/ascribe_forms/property_collapsible.js new file mode 100644 index 00000000..d2cd0f95 --- /dev/null +++ b/js/components/ascribe_forms/property_collapsible.js @@ -0,0 +1,90 @@ +'use strict'; + +import React from 'react'; + +import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin'; +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; +import Tooltip from 'react-bootstrap/lib/Tooltip'; + +import classNames from 'classnames'; + + +let PropertyCollapsile = React.createClass({ + propTypes: { + children: React.PropTypes.arrayOf(React.PropTypes.element), + checkboxLabel: React.PropTypes.string, + tooltip: React.PropTypes.string + }, + + mixins: [CollapsibleMixin], + + getInitialState() { + return { + show: false + }; + }, + + getCollapsibleDOMNode(){ + return React.findDOMNode(this.refs.panel); + }, + + getCollapsibleDimensionValue(){ + return React.findDOMNode(this.refs.panel).scrollHeight; + }, + + handleFocus() { + this.refs.checkboxCollapsible.getDOMNode().checked = !this.refs.checkboxCollapsible.getDOMNode().checked; + this.setState({ + show: this.refs.checkboxCollapsible.getDOMNode().checked + }); + }, + + renderChildren() { + if(this.state.show) { + return (
+ {this.props.children} +
); + } else { + return null; + } + }, + + render() { + let tooltip = ; + if (this.props.tooltip){ + tooltip = ( + + {this.props.tooltip} + ); + } + + let style = this.state.show ? {} : {paddingBottom: 0}; + + return ( +
+ +
+ + {/* PLEASE LEAVE THE SPACE BETWEEN LABEL and this.props.label */} + {this.props.checkboxLabel} +
+
+ {this.renderChildren()} +
+ ); + } +}); + +export default PropertyCollapsile; \ No newline at end of file diff --git a/js/components/ascribe_media/media_player.js b/js/components/ascribe_media/media_player.js index a4c84513..3d032bef 100644 --- a/js/components/ascribe_media/media_player.js +++ b/js/components/ascribe_media/media_player.js @@ -133,25 +133,6 @@ let Video = React.createClass({ } }); - -let EncodingStatus = React.createClass({ - propTypes: { - encodingStatus: React.PropTypes.number.isRequired - }, - - render() { - return ( - - ); - } -}); - - let resourceMap = { 'image': Image, 'video': Video, @@ -169,7 +150,7 @@ let MediaPlayer = React.createClass({ }, render() { - if (this.props.encodingStatus !== undefined && this.props.encodingStatus !== 100) { + if (this.props.mimetype === 'video' && this.props.encodingStatus !== undefined && this.props.encodingStatus !== 100) { return (

Please be patient, the video is been encoded

diff --git a/js/components/ascribe_table/table_item.js b/js/components/ascribe_table/table_item.js index a0e6e852..12232a31 100644 --- a/js/components/ascribe_table/table_item.js +++ b/js/components/ascribe_table/table_item.js @@ -12,12 +12,14 @@ let TableItem = React.createClass({ propTypes: { columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(ColumnModel)), columnContent: React.PropTypes.object, - className: React.PropTypes.string + className: React.PropTypes.string, + onClick: React.PropTypes.func }, render() { return ( diff --git a/js/components/ascribe_table/table_item_checkbox.js b/js/components/ascribe_table/table_item_checkbox.js index cb368369..9ade00f4 100644 --- a/js/components/ascribe_table/table_item_checkbox.js +++ b/js/components/ascribe_table/table_item_checkbox.js @@ -7,18 +7,13 @@ let TableItemCheckbox = React.createClass({ propTypes: { editionId: React.PropTypes.number, pieceId: React.PropTypes.number, - selectItem: React.PropTypes.func, selected: React.PropTypes.bool }, - selectItem() { - this.props.selectItem(this.props.pieceId, this.props.editionId); - }, - render() { return ( - + ); } diff --git a/js/components/ascribe_table/table_item_selectable.js b/js/components/ascribe_table/table_item_selectable.js index cc53c5f0..36144181 100644 --- a/js/components/ascribe_table/table_item_selectable.js +++ b/js/components/ascribe_table/table_item_selectable.js @@ -19,7 +19,7 @@ let TableItemSelectable = React.createClass({ }, selectItem() { - this.props.selectItem(this.props.parentId, this.props.columnContent.edition_number); + this.props.selectItem(this.props.parentId, this.props.columnContent.id); }, render() { diff --git a/js/components/ascribe_table/table_item_wrapper.js b/js/components/ascribe_table/table_item_wrapper.js index 35299641..6eb7b3b4 100644 --- a/js/components/ascribe_table/table_item_wrapper.js +++ b/js/components/ascribe_table/table_item_wrapper.js @@ -11,14 +11,15 @@ let TableItemWrapper = React.createClass({ propTypes: { columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(ColumnModel)), columnContent: React.PropTypes.object, - columnWidth: React.PropTypes.number.isRequired + columnWidth: React.PropTypes.number.isRequired, + onClick: React.PropTypes.func }, mixins: [Router.Navigation], render() { return ( - + {this.props.columnList.map((column, i) => { let TypeElement = column.displayType; diff --git a/js/components/global_notification.js b/js/components/global_notification.js index 82646976..aefe525a 100644 --- a/js/components/global_notification.js +++ b/js/components/global_notification.js @@ -7,66 +7,116 @@ import GlobalNotificationStore from '../stores/global_notification_store'; import Row from 'react-bootstrap/lib/Row'; import Col from 'react-bootstrap/lib/Col'; +import { mergeOptions } from '../utils/general_utils'; + let GlobalNotification = React.createClass({ + getInitialState() { + return mergeOptions( + { + containerWidth: 0 + }, + this.extractFirstElem(GlobalNotificationStore.getState().notificationQue) + ); + }, + componentDidMount() { GlobalNotificationStore.listen(this.onChange); + + // init container width + this.handleContainerResize(); + + // we're using an event listener on window here, + // as it was not possible to listen to the resize events of a dom node + window.addEventListener('resize', this.handleContainerResize); }, componentWillUnmount() { GlobalNotificationStore.unlisten(this.onChange); - }, - - getInititalState() { - return this.extractFirstElem(GlobalNotificationStore.getState().notificationQue); + window.removeEventListener('resize', this.handleContainerResize); }, extractFirstElem(l) { - return l.length > 0 ? l[0] : null; + if(l.length > 0) { + return { + show: true, + message: l[0] + }; + } else { + return { + show: false, + message: '' + }; + } }, onChange(state) { let notification = this.extractFirstElem(state.notificationQue); - if(notification) { + if(notification.show) { this.setState(notification); } else { - this.replaceState(null); + this.setState({ + show: false + }); } }, + handleContainerResize() { + this.setState({ + containerWidth: this.refs.notificationWrapper.getDOMNode().offsetWidth + }); + }, + render() { - let notificationClass = 'ascribe-global-notification '; - let message = this.state && this.state.message ? this.state.message : null; + let notificationClass = 'ascribe-global-notification'; + let textClass; - if(message) { - let colors = { - warning: '#f0ad4e', - success: '#5cb85c', - info: 'rgba(2, 182, 163, 1)', - danger: '#d9534f' - }; + if(this.state.containerWidth > 768) { + notificationClass = 'ascribe-global-notification-bubble'; - let text = (
{message ? message : null}
); + if(this.state.show) { + notificationClass += ' ascribe-global-notification-bubble-on'; + } else { + notificationClass += ' ascribe-global-notification-bubble-off'; + } - return ( + } else { + notificationClass = 'ascribe-global-notification'; + + if(this.state.show) { + notificationClass += ' ascribe-global-notification-on'; + } else { + notificationClass += ' ascribe-global-notification-off'; + } + + } + + if(this.state.message) { + switch(this.state.message.type) { + case 'success': + textClass = 'ascribe-global-notification-success'; + break; + case 'danger': + textClass = 'ascribe-global-notification-danger'; + break; + default: + console.warn('Could not find a matching type in global_notification.js'); + } + } + + + return ( +
-
- {text} +
+
{this.state.message.message}
- ); - } else { - return ( - - -
- - - ); - } +
+ ); } }); diff --git a/js/components/header.js b/js/components/header.js index a023068b..ef8566e4 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -19,11 +19,12 @@ import MenuItem from 'react-bootstrap/lib/MenuItem'; import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink'; import NavItemLink from 'react-router-bootstrap/lib/NavItemLink'; +import HeaderNotificationDebug from './header_notification_debug'; + import { mergeOptions } from '../utils/general_utils'; import { getLangText } from '../utils/lang_utils'; -let Link = Router.Link; let Header = React.createClass({ mixins: [Router.Navigation], @@ -43,11 +44,13 @@ let Header = React.createClass({ UserStore.unlisten(this.onChange); WhitelabelStore.unlisten(this.onChange); }, + handleLogout(){ UserActions.logoutCurrentUser(); Alt.flush(); this.transitionTo('login'); }, + getLogo(){ let logo = ( @@ -102,15 +105,14 @@ let Header = React.createClass({
- {this.getLogo()} - } + this.getLogo() + } toggleNavKey={0} fixedTop={true}> - +