1
0
mirror of https://github.com/ascribe/onion.git synced 2024-12-22 17:33:14 +01:00

Merge with master

This commit is contained in:
Brett Sun 2016-02-08 11:43:02 +01:00
commit d95bdc0913
162 changed files with 5748 additions and 2620 deletions

8
.gitignore vendored
View File

@ -17,9 +17,13 @@ pids
logs logs
results results
node_modules/* build/*
build gemini-coverage/*
gemini-report/*
test/gemini/screenshots/*
node_modules/*
.DS_Store .DS_Store
.env .env

View File

@ -1,17 +1,18 @@
Introduction Introduction
============ ============
Onion is the web client for Ascribe. The idea is to have a well documented, Onion is the web client for Ascribe. The idea is to have a well documented, modern, easy to test, easy to hack, JavaScript application.
easy to test, easy to hack, JavaScript application.
The code is JavaScript ECMA 6. The code is JavaScript 2015 / ECMAScript 6.
Getting started Getting started
=============== ===============
Install some nice extension for Chrom(e|ium): Install some nice extension for Chrom(e|ium):
- [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) - [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi)
- [Alt Developer Tools](https://github.com/goatslacker/alt-devtool)
```bash ```bash
git clone git@github.com:ascribe/onion.git git clone git@github.com:ascribe/onion.git
@ -32,22 +33,41 @@ Additionally, to work on the white labeling functionality, you need to edit your
127.0.0.1 lumenus.localhost.com 127.0.0.1 lumenus.localhost.com
127.0.0.1 portfolioreview.localhost.com 127.0.0.1 portfolioreview.localhost.com
127.0.0.1 23vivi.localhost.com 127.0.0.1 23vivi.localhost.com
127.0.0.1 polline.localhost.com
127.0.0.1 artcity.localhost.com
``` ```
JavaScript Code Conventions JavaScript Code Conventions
=========================== ===========================
For this project, we're using: For this project, we're using:
* 4 Spaces * 4 Spaces
* We use ES6 * ES6
* We don't use ES6's class declaration for React components because it does not support Mixins as well as Autobinding ([Blog post about it](http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding)) * We don't use ES6's class declaration for React components because it does not support Mixins as well as Autobinding ([Blog post about it](http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding))
* We don't use camel case for file naming but in everything Javascript related * We don't use camel case for file naming but in everything Javascript related
* We use `let` instead of `var`: [SA Post](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword) * We use `momentjs` instead of Javascript's `Date` object, as the native `Date` interface previously introduced bugs and we're including `momentjs` for other dependencies anyway
* We don't use Javascript's `Date` object, as its interface introduced bugs previously and we're including `momentjs` for other dependencies anyways
Make sure to check out the [style guide](https://github.com/ascribe/javascript).
Linting
-------
We use [ESLint](https://github.com/eslint/eslint) with our own [custom ruleset](.eslintrc).
SCSS Code Conventions
=====================
Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor.
Some interesting links:
* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom)
Branch names Branch names
===================== ============
To allow Github and JIRA to track branches while still allowing us to switch branches quickly using a ticket's number (and keep our peace of mind), we have the following rules for naming branches: To allow Github and JIRA to track branches while still allowing us to switch branches quickly using a ticket's number (and keep our peace of mind), we have the following rules for naming branches:
@ -61,22 +81,21 @@ AD-<JIRA-ticket-id>-brief-and-sane-description-of-the-ticket
where `brief-and-sane-description-of-the-ticket` does not need to equal to the issue or ticket's title. where `brief-and-sane-description-of-the-ticket` does not need to equal to the issue or ticket's title.
Example Example
------------- -------
**JIRA ticket name:** `AD-1242 - Frontend caching for simple endpoints to measure perceived page load <more useless information>` **JIRA ticket name:** `AD-1242 - Frontend caching for simple endpoints to measure perceived page load <more useless information>`
**Github branch name:** `AD-1242-caching-solution-for-stores` **Github branch name:** `AD-1242-caching-solution-for-stores`
SCSS Code Conventions
=====================
Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor.
Some interesting links:
* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom)
Testing Testing
=============== =======
Unit Testing
------------
We're using Facebook's jest to do testing as it integrates nicely with react.js as well. We're using Facebook's jest to do testing as it integrates nicely with react.js as well.
Tests are always created per directory by creating a `__tests__` folder. To test a specific file, a `<file_name>_tests.js` file needs to be created. Tests are always created per directory by creating a `__tests__` folder. To test a specific file, a `<file_name>_tests.js` file needs to be created.
@ -86,7 +105,24 @@ This is due to the fact that jest's function mocking and ES6 module syntax are [
Therefore, to require a module in your test file, you need to use CommonJS's `require` syntax. Except for this, all tests can be written in ES6 syntax. Therefore, to require a module in your test file, you need to use CommonJS's `require` syntax. Except for this, all tests can be written in ES6 syntax.
## Workflow Visual Regression Testing
-------------------------
We're using [Gemini](https://github.com/gemini-testing/gemini) for visual regression tests because it supports both PhantomJS2 and SauceLabs.
See the [helper docs](test/gemini/README.md) for information on installing Gemini, its dependencies, and running and writing tests.
Integration Testing
-------------------
We're using [Sauce Labs](https://saucelabs.com/home) with [WD.js](https://github.com/admc/wd) for integration testing across browser grids with Selenium.
See the [helper docs](test/integration/README.md) for information on each part of the test stack and how to run and write tests.
Workflow
========
Generally, when you're runing `gulp serve`, all tests are being run. Generally, when you're runing `gulp serve`, all tests are being run.
If you want to test exclusively (without having the obnoxious ES6Linter warnings), you can just run `gulp jest:watch`. If you want to test exclusively (without having the obnoxious ES6Linter warnings), you can just run `gulp jest:watch`.
@ -137,9 +173,16 @@ A: Easily by starting the your gulp process with the following command:
ONION_BASE_URL='/' ONION_SERVER_URL='http://localhost.com:8000/' gulp serve ONION_BASE_URL='/' ONION_SERVER_URL='http://localhost.com:8000/' gulp serve
``` ```
Or, by adding these two your environment variables:
```
ONION_BASE_URL='/'
ONION_SERVER_URL='http://localhost.com:8000/'
```
Q: I want to know all dependencies that get bundled into the live build. Q: I want to know all dependencies that get bundled into the live build.
A: ```browserify -e js/app.js --list > webapp-dependencies.txt``` A: ```browserify -e js/app.js --list > webapp-dependencies.txt```
Reading list Reading list
============ ============
@ -152,7 +195,6 @@ Start here
- [alt.js](http://alt.js.org/) - [alt.js](http://alt.js.org/)
- [alt.js readme](https://github.com/goatslacker/alt) - [alt.js readme](https://github.com/goatslacker/alt)
Moar stuff Moar stuff
---------- ----------

View File

@ -97,7 +97,8 @@ gulp.task('browser-sync', function() {
proxy: 'http://localhost:4000', proxy: 'http://localhost:4000',
port: 3000, port: 3000,
open: false, // does not open the browser-window anymore (handled manually) open: false, // does not open the browser-window anymore (handled manually)
ghostMode: false ghostMode: false,
notify: false // stop showing the browsersync pop up
}); });
}); });

View File

@ -0,0 +1,13 @@
'use strict';
import { alt } from '../alt';
class ErrorQueueActions {
constructor() {
this.generateActions(
'shiftErrorQueue'
);
}
}
export default alt.createActions(ErrorQueueActions);

View File

@ -8,9 +8,8 @@ class EventActions {
this.generateActions( this.generateActions(
'applicationWillBoot', 'applicationWillBoot',
'applicationDidBoot', 'applicationDidBoot',
'profileDidLoad', 'userDidAuthenticate',
//'userDidLogin', 'userDidLogout',
//'userDidLogout',
'routeDidChange' 'routeDidChange'
); );
} }

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { altUser } from '../alt'; import { alt } from '../alt';
class UserActions { class UserActions {
@ -15,4 +15,4 @@ class UserActions {
} }
} }
export default altUser.createActions(UserActions); export default alt.createActions(UserActions);

View File

@ -4,5 +4,4 @@ import Alt from 'alt';
export let alt = new Alt(); export let alt = new Alt();
export let altThirdParty = new Alt(); export let altThirdParty = new Alt();
export let altUser = new Alt();
export let altWhitelabel = new Alt(); export let altWhitelabel = new Alt();

90
js/components/app_base.js Normal file
View File

@ -0,0 +1,90 @@
'use strict';
import React from 'react';
import classNames from 'classnames';
import { History } from 'react-router';
import UserActions from '../actions/user_actions';
import UserStore from '../stores/user_store';
import WhitelabelActions from '../actions/whitelabel_actions';
import WhitelabelStore from '../stores/whitelabel_store';
import GlobalNotification from './global_notification';
import AppConstants from '../constants/application_constants';
import { mergeOptions } from '../utils/general_utils';
export default function AppBase(App) {
return React.createClass({
displayName: 'AppBase',
propTypes: {
children: React.PropTypes.element.isRequired,
history: React.PropTypes.object.isRequired,
location: React.PropTypes.object.isRequired,
routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
},
getInitialState() {
return mergeOptions(
UserStore.getState(),
WhitelabelStore.getState()
);
},
mixins: [History],
componentDidMount() {
UserStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
UserActions.fetchCurrentUser();
WhitelabelActions.fetchWhitelabel();
this.history.locationQueue.push(this.props.location);
},
componentWillReceiveProps(nextProps) {
const { locationQueue } = this.history;
locationQueue.unshift(nextProps.location);
// Limit the number of locations to keep in memory to avoid too much memory usage
if (locationQueue.length > AppConstants.locationThreshold) {
locationQueue.length = AppConstants.locationThreshold;
}
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
WhitelabelActions.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
const { routes } = this.props;
const { currentUser, whitelabel } = this.state;
// The second element of the routes prop given to us by react-router is always the
// active second-level component object (ie. after App).
const activeRoute = routes[1];
return (
<div>
<App
{...this.props}
activeRoute={activeRoute}
currentUser={currentUser}
whitelabel={whitelabel} />
<GlobalNotification />
<div id="modal" className="container" />
</div>
);
}
});
};

View File

@ -0,0 +1,34 @@
'use strict';
import React from 'react';
import { omitFromObject } from '../utils/general_utils';
const AppRouteWrapper = React.createClass({
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]).isRequired
},
render() {
const propsToPropagate = omitFromObject(this.props, ['children']);
let childrenWithProps = this.props.children;
// If there are more props given, propagate them into the child routes by cloning the routes
if (Object.keys(propsToPropagate).length) {
childrenWithProps = React.Children.map(this.props.children, (child) => {
return React.cloneElement(child, propsToPropagate);
});
}
return (
<div className="container ascribe-body">
{childrenWithProps}
</div>
);
}
});
export default AppRouteWrapper;

View File

@ -1,14 +1,17 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import { Link } from 'react-router';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let AccordionList = React.createClass({ let AccordionList = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string,
children: React.PropTypes.arrayOf(React.PropTypes.element).isRequired, children: React.PropTypes.arrayOf(React.PropTypes.element).isRequired,
loadingElement: React.PropTypes.element, loadingElement: React.PropTypes.element.isRequired,
className: React.PropTypes.string,
count: React.PropTypes.number, count: React.PropTypes.number,
itemList: React.PropTypes.arrayOf(React.PropTypes.object), itemList: React.PropTypes.arrayOf(React.PropTypes.object),
search: React.PropTypes.string, search: React.PropTypes.string,
@ -22,7 +25,7 @@ let AccordionList = React.createClass({
render() { render() {
const { search } = this.props; const { search } = this.props;
if(this.props.itemList && this.props.itemList.length > 0) { if (this.props.itemList && this.props.itemList.length > 0) {
return ( return (
<div className={this.props.className}> <div className={this.props.className}>
{this.props.children} {this.props.children}
@ -36,7 +39,7 @@ let AccordionList = React.createClass({
</p> </p>
<p className="text-center"> <p className="text-center">
{getLangText('To register one, click')}&nbsp; {getLangText('To register one, click')}&nbsp;
<a href="register_piece">{getLangText('here')}</a>! <Link to="/register_piece">{getLangText('here')}</Link>!
</p> </p>
</div> </div>
); );

View File

@ -21,16 +21,16 @@ let AccordionListItem = React.createClass({
}, },
render() { render() {
const { linkData, const {
className, linkData,
thumbnail, className,
heading, thumbnail,
subheading, heading,
subsubheading, subheading,
buttons, subsubheading,
badge, buttons,
children } = this.props; badge,
children } = this.props;
return ( return (
<div className="row"> <div className="row">

View File

@ -34,11 +34,10 @@ let AccordionListItemPiece = React.createClass({
}, },
getLinkData() { getLinkData() {
let { piece } = this.props; const { piece } = this.props;
if(piece && piece.first_edition) { if (piece && piece.first_edition) {
return `/editions/${piece.first_edition.bitcoin_id}`; return `/editions/${piece.first_edition.bitcoin_id}`;
} else { } else {
return `/pieces/${piece.id}`; return `/pieces/${piece.id}`;
} }

View File

@ -1,4 +1,4 @@
'use strict' 'use strict';
import React from 'react'; import React from 'react';

View File

@ -7,20 +7,19 @@ import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip'; import Tooltip from 'react-bootstrap/lib/Tooltip';
import AccordionListItemPiece from './accordion_list_item_piece';
import AccordionListItemEditionWidget from './accordion_list_item_edition_widget';
import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import WhitelabelStore from '../../stores/whitelabel_store';
import EditionListActions from '../../actions/edition_list_actions'; import EditionListActions from '../../actions/edition_list_actions';
import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions'; import GlobalNotificationActions from '../../actions/global_notification_actions';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import AccordionListItemPiece from './accordion_list_item_piece';
import AccordionListItemEditionWidget from './accordion_list_item_edition_widget';
import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import AclProxy from '../acl_proxy'; import AclProxy from '../acl_proxy';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
@ -29,50 +28,51 @@ import { mergeOptions } from '../../utils/general_utils';
let AccordionListItemWallet = React.createClass({ let AccordionListItemWallet = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, content: React.PropTypes.object.isRequired,
content: React.PropTypes.object, whitelabel: React.PropTypes.object.isRequired,
thumbnailPlaceholder: React.PropTypes.func,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element React.PropTypes.element
]) ]),
className: React.PropTypes.string,
thumbnailPlaceholder: React.PropTypes.func
}, },
getInitialState() { getInitialState() {
return mergeOptions( return mergeOptions(
PieceListStore.getState(),
{ {
showCreateEditionsDialog: false showCreateEditionsDialog: false
}, }
PieceListStore.getState(),
WhitelabelStore.getState()
); );
}, },
componentDidMount() { componentDidMount() {
PieceListStore.listen(this.onChange); PieceListStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
}, },
componentWillUnmount() { componentWillUnmount() {
PieceListStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
this.setState(state); this.setState(state);
}, },
getGlyphicon(){ getGlyphicon() {
if ((this.props.content.notifications && this.props.content.notifications.length > 0)){ if (this.props.content.notifications && this.props.content.notifications.length) {
return ( return (
<OverlayTrigger <OverlayTrigger
delay={500} delay={500}
placement="left" placement="left"
overlay={<Tooltip>{getLangText('You have actions pending')}</Tooltip>}> overlay={<Tooltip>{getLangText('You have actions pending')}</Tooltip>}>
<Glyphicon glyph='bell' color="green"/> <Glyphicon glyph='bell' color="green" />
</OverlayTrigger>); </OverlayTrigger>
);
} else {
return null;
} }
return null;
}, },
toggleCreateEditionsDialog() { toggleCreateEditionsDialog() {
@ -93,7 +93,7 @@ let AccordionListItemWallet = React.createClass({
PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy });
EditionListActions.toggleEditionList(pieceId); EditionListActions.toggleEditionList(pieceId);
const notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); const notification = new GlobalNotificationModel(getLangText('Editions successfully created'), 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
@ -111,13 +111,15 @@ let AccordionListItemWallet = React.createClass({
}, },
getLicences() { getLicences() {
const { content, whitelabel } = this.props;
// convert this to acl_view_licences later // convert this to acl_view_licences later
if (this.state.whitelabel && this.state.whitelabel.name === 'Creative Commons France') { if (whitelabel.name === 'Creative Commons France') {
return ( return (
<span> <span>
<span>, </span> <span>, </span>
<a href={this.props.content.license_type.url} target="_blank"> <a href={content.license_type.url} target="_blank">
{getLangText('%s license', this.props.content.license_type.code)} {getLangText('%s license', content.license_type.code)}
</a> </a>
</span> </span>
); );

View File

@ -2,36 +2,42 @@
import React from 'react'; import React from 'react';
import Header from '../components/header'; import AppBase from './app_base';
import Footer from '../components/footer'; import AppRouteWrapper from './app_route_wrapper';
import GlobalNotification from './global_notification'; import Footer from './footer';
import Header from './header';
let AscribeApp = React.createClass({ let AscribeApp = React.createClass({
propTypes: { propTypes: {
children: React.PropTypes.oneOfType([ activeRoute: React.PropTypes.object.isRequired,
React.PropTypes.arrayOf(React.PropTypes.element), children: React.PropTypes.element.isRequired,
React.PropTypes.element routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
]),
routes: React.PropTypes.arrayOf(React.PropTypes.object) // Provided from AppBase
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object
}, },
render() { render() {
let { children, routes } = this.props; const { activeRoute, children, currentUser, routes, whitelabel } = this.props;
return ( return (
<div className="container ascribe-default-app"> <div className="ascribe-app ascribe-default-app">
<Header routes={routes} /> <Header
{/* Routes are injected here */} currentUser={currentUser}
<div className="ascribe-body"> routes={routes}
whitelabel={whitelabel} />
<AppRouteWrapper
currentUser={currentUser}
whitelabel={whitelabel}>
{/* Routes are injected here */}
{children} {children}
</div> </AppRouteWrapper>
<Footer /> <Footer activeRoute={activeRoute} />
<GlobalNotification />
<div id="modal" className="container"></div>
</div> </div>
); );
} }
}); });
export default AscribeApp; export default AppBase(AscribeApp);

View File

@ -2,9 +2,6 @@
import React from 'react/addons'; import React from 'react/addons';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import ConsignButton from './acls/consign_button'; import ConsignButton from './acls/consign_button';
import EmailButton from './acls/email_button'; import EmailButton from './acls/email_button';
import LoanButton from './acls/loan_button'; import LoanButton from './acls/loan_button';
@ -12,50 +9,44 @@ import LoanRequestButton from './acls/loan_request_button';
import TransferButton from './acls/transfer_button'; import TransferButton from './acls/transfer_button';
import UnconsignButton from './acls/unconsign_button'; import UnconsignButton from './acls/unconsign_button';
import { mergeOptions } from '../../utils/general_utils'; import { selectFromObject } from '../../utils/general_utils';
let AclButtonList = React.createClass({ let AclButtonList = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, availableAcls: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired,
handleSuccess: React.PropTypes.func.isRequired,
pieceOrEditions: React.PropTypes.oneOfType([ pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object, React.PropTypes.object,
React.PropTypes.array React.PropTypes.array
]).isRequired, ]).isRequired,
availableAcls: React.PropTypes.object.isRequired,
buttonsStyle: React.PropTypes.object, buttonsStyle: React.PropTypes.object,
handleSuccess: React.PropTypes.func.isRequired,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element React.PropTypes.element
]) ]),
className: React.PropTypes.string
}, },
getInitialState() { getInitialState() {
return mergeOptions( return {
UserStore.getState(), buttonListSize: 0
{ }
buttonListSize: 0
}
);
}, },
componentDidMount() { componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser.defer();
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
}, },
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if(prevProps.availableAcls && prevProps.availableAcls !== this.props.availableAcls) { if (prevProps.availableAcls && prevProps.availableAcls !== this.props.availableAcls) {
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
} }
}, },
componentWillUnmount() { componentWillUnmount() {
UserStore.unlisten(this.onChange);
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
}, },
@ -65,10 +56,6 @@ let AclButtonList = React.createClass({
}); });
}, },
onChange(state) {
this.setState(state);
},
renderChildren() { renderChildren() {
const { children } = this.props; const { children } = this.props;
const { buttonListSize } = this.state; const { buttonListSize } = this.state;
@ -79,42 +66,29 @@ let AclButtonList = React.createClass({
}, },
render() { render() {
const { className, const {
buttonsStyle, availableAcls,
availableAcls, buttonsStyle,
pieceOrEditions, className,
handleSuccess } = this.props; currentUser,
handleSuccess,
pieceOrEditions } = this.props;
const { currentUser } = this.state; const buttonProps = selectFromObject(this.props, [
'availableAcls',
'currentUser',
'handleSuccess',
'pieceOrEditions'
]);
return ( return (
<div className={className}> <div className={className}>
<span ref="buttonList" style={buttonsStyle}> <span ref="buttonList" style={buttonsStyle}>
<EmailButton <EmailButton {...buttonProps} />
availableAcls={availableAcls} <TransferButton {...buttonProps} />
pieceOrEditions={pieceOrEditions} <ConsignButton {...buttonProps} />
currentUser={currentUser} <UnconsignButton {...buttonProps} />
handleSuccess={handleSuccess} /> <LoanButton {...buttonProps} />
<TransferButton
availableAcls={availableAcls}
pieceOrEditions={pieceOrEditions}
currentUser={currentUser}
handleSuccess={handleSuccess}/>
<ConsignButton
availableAcls={availableAcls}
pieceOrEditions={pieceOrEditions}
currentUser={currentUser}
handleSuccess={handleSuccess} />
<UnconsignButton
availableAcls={availableAcls}
pieceOrEditions={pieceOrEditions}
currentUser={currentUser}
handleSuccess={handleSuccess} />
<LoanButton
availableAcls={availableAcls}
pieceOrEditions={pieceOrEditions}
currentUser={currentUser}
handleSuccess={handleSuccess} />
{this.renderChildren()} {this.renderChildren()}
</span> </span>
</div> </div>

View File

@ -14,7 +14,7 @@ import AppConstants from '../../../constants/application_constants';
import { AclInformationText } from '../../../constants/acl_information_text'; import { AclInformationText } from '../../../constants/acl_information_text';
export default function ({ action, displayName, title, tooltip }) { export default function AclButton({ action, displayName, title, tooltip }) {
if (AppConstants.aclList.indexOf(action) < 0) { if (AppConstants.aclList.indexOf(action) < 0) {
console.warn('Your specified aclName did not match a an acl class.'); console.warn('Your specified aclName did not match a an acl class.');
} }
@ -24,23 +24,20 @@ export default function ({ action, displayName, title, tooltip }) {
propTypes: { propTypes: {
availableAcls: React.PropTypes.object.isRequired, availableAcls: React.PropTypes.object.isRequired,
buttonAcceptName: React.PropTypes.string,
buttonAcceptClassName: React.PropTypes.string,
currentUser: React.PropTypes.object,
email: React.PropTypes.string,
pieceOrEditions: React.PropTypes.oneOfType([ pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object, React.PropTypes.object,
React.PropTypes.array React.PropTypes.array
]).isRequired, ]).isRequired,
handleSuccess: React.PropTypes.func.isRequired,
className: React.PropTypes.string buttonAcceptName: React.PropTypes.string,
buttonAcceptClassName: React.PropTypes.string,
currentUser: React.PropTypes.object,
email: React.PropTypes.string,
handleSuccess: React.PropTypes.func
}, },
sanitizeAction() { sanitizeAction() {
if (this.props.buttonAcceptName) { return this.props.buttonAcceptName || AclInformationText.titles[action];
return this.props.buttonAcceptName;
}
return AclInformationText.titles[action];
}, },
render() { render() {

View File

@ -15,10 +15,12 @@ let UnConsignRequestButton = React.createClass({
propTypes: { propTypes: {
currentUser: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired,
edition: React.PropTypes.object.isRequired, edition: React.PropTypes.object.isRequired,
handleSuccess: React.PropTypes.func.isRequired
handleSuccess: React.PropTypes.func
}, },
render: function () { render: function () {
const { currentUser, edition, handleSuccess } = this.props;
return ( return (
<ModalWrapper <ModalWrapper
trigger={ trigger={
@ -26,17 +28,18 @@ let UnConsignRequestButton = React.createClass({
REQUEST UNCONSIGN REQUEST UNCONSIGN
</Button> </Button>
} }
handleSuccess={this.props.handleSuccess} handleSuccess={handleSuccess}
title='Request to Un-Consign'> title='Request to Un-Consign'>
<UnConsignRequestForm <UnConsignRequestForm
url={ApiUrls.ownership_unconsigns_request} url={ApiUrls.ownership_unconsigns_request}
id={{'bitcoin_id': this.props.edition.bitcoin_id}} id={{'bitcoin_id': edition.bitcoin_id}}
message={`${getLangText('Hi')}, message={`${getLangText('Hi')},
${getLangText('I request you to un-consign')} \" ${this.props.edition.title} \". ${getLangText('I request you to un-consign')} \" ${edition.title} \".
${getLangText('Truly yours')}, ${getLangText('Truly yours')},
${this.props.currentUser.username}`}/> ${currentUser.username}`
} />
</ModalWrapper> </ModalWrapper>
); );
} }

View File

@ -8,23 +8,23 @@ import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col'; import Col from 'react-bootstrap/lib/Col';
import Glyphicon from 'react-bootstrap/lib/Glyphicon'; import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import HistoryIterator from './history_iterator'; import EditionActions from '../../actions/edition_actions';
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 DetailProperty from './detail_property'; import DetailProperty from './detail_property';
import LicenseDetail from './license_detail';
import FurtherDetails from './further_details';
import EditionActionPanel from './edition_action_panel'; import EditionActionPanel from './edition_action_panel';
import AclProxy from '../acl_proxy'; import FurtherDetails from './further_details';
import HistoryIterator from './history_iterator';
import LicenseDetail from './license_detail';
import MediaContainer from './media_container';
import Note from './note'; import Note from './note';
import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph';
import Form from '../ascribe_forms/form';
import Property from '../ascribe_forms/property';
import AclProxy from '../acl_proxy';
import ApiUrls from '../../constants/api_urls'; import ApiUrls from '../../constants/api_urls';
import AscribeSpinner from '../ascribe_spinner'; import AscribeSpinner from '../ascribe_spinner';
@ -36,11 +36,13 @@ import { getLangText } from '../../utils/lang_utils';
*/ */
let Edition = React.createClass({ let Edition = React.createClass({
propTypes: { propTypes: {
currentUser: React.PropTypes.object.isRequired,
edition: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object.isRequired,
actionPanelButtonListType: React.PropTypes.func, actionPanelButtonListType: React.PropTypes.func,
furtherDetailsType: React.PropTypes.func,
edition: React.PropTypes.object,
coaError: React.PropTypes.object, coaError: React.PropTypes.object,
currentUser: React.PropTypes.object, furtherDetailsType: React.PropTypes.func,
loadEdition: React.PropTypes.func loadEdition: React.PropTypes.func
}, },
@ -57,56 +59,56 @@ let Edition = React.createClass({
currentUser, currentUser,
edition, edition,
furtherDetailsType: FurtherDetailsType, furtherDetailsType: FurtherDetailsType,
loadEdition } = this.props; loadEdition,
whitelabel } = this.props;
return ( return (
<Row> <Row>
<Col md={6} className="ascribe-print-col-left"> <Col md={6} className="ascribe-print-col-left">
<MediaContainer <MediaContainer
content={edition} content={edition}
currentUser={currentUser} /> currentUser={currentUser}
refreshObject={loadEdition} />
</Col> </Col>
<Col md={6} className="ascribe-edition-details ascribe-print-col-right"> <Col md={6} className="ascribe-edition-details ascribe-print-col-right">
<div className="ascribe-detail-header"> <div className="ascribe-detail-header">
<hr className="hidden-print" style={{marginTop: 0}}/> <hr className="hidden-print" style={{marginTop: 0}} />
<h1 className="ascribe-detail-title">{edition.title}</h1> <h1 className="ascribe-detail-title">{edition.title}</h1>
<DetailProperty label="CREATED BY" value={edition.artist_name} /> <DetailProperty label="CREATED BY" value={edition.artist_name} />
<DetailProperty label="DATE" value={Moment(edition.date_created, 'YYYY-MM-DD').year()} /> <DetailProperty label="DATE" value={Moment(edition.date_created, 'YYYY-MM-DD').year()} />
<hr/> <hr />
</div> </div>
<EditionSummary <EditionSummary
actionPanelButtonListType={actionPanelButtonListType} actionPanelButtonListType={actionPanelButtonListType}
edition={edition} edition={edition}
currentUser={currentUser} currentUser={currentUser}
handleSuccess={loadEdition}/> handleSuccess={loadEdition}
whitelabel={whitelabel} />
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Certificate of Authenticity')} title={getLangText('Certificate of Authenticity')}
show={edition.acl.acl_coa === true}> show={edition.acl.acl_coa === true}>
<CoaDetails <CoaDetails
coa={edition.coa} coa={edition.coa}
coaError={coaError} coaError={coaError}
editionId={edition.bitcoin_id}/> editionId={edition.bitcoin_id} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Provenance/Ownership History')} title={getLangText('Provenance/Ownership History')}
show={edition.ownership_history && edition.ownership_history.length > 0}> show={edition.ownership_history && edition.ownership_history.length}>
<HistoryIterator <HistoryIterator history={edition.ownership_history} />
history={edition.ownership_history} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Consignment History')} title={getLangText('Consignment History')}
show={edition.consign_history && edition.consign_history.length > 0}> show={edition.consign_history && edition.consign_history.length > 0}>
<HistoryIterator <HistoryIterator history={edition.consign_history} />
history={edition.consign_history} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Loan History')} title={getLangText('Loan History')}
show={edition.loan_history && edition.loan_history.length > 0}> show={edition.loan_history && edition.loan_history.length > 0}>
<HistoryIterator <HistoryIterator history={edition.loan_history} />
history={edition.loan_history} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
@ -120,7 +122,7 @@ let Edition = React.createClass({
editable={true} editable={true}
successMessage={getLangText('Private note saved')} successMessage={getLangText('Private note saved')}
url={ApiUrls.note_private_edition} url={ApiUrls.note_private_edition}
currentUser={currentUser}/> currentUser={currentUser} />
<Note <Note
id={() => {return {'bitcoin_id': edition.bitcoin_id}; }} id={() => {return {'bitcoin_id': edition.bitcoin_id}; }}
label={getLangText('Personal note (public)')} label={getLangText('Personal note (public)')}
@ -130,13 +132,11 @@ let Edition = React.createClass({
show={!!edition.public_note || !!edition.acl.acl_edit} show={!!edition.public_note || !!edition.acl.acl_edit}
successMessage={getLangText('Public edition note saved')} successMessage={getLangText('Public edition note saved')}
url={ApiUrls.note_public_edition} url={ApiUrls.note_public_edition}
currentUser={currentUser}/> currentUser={currentUser} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Further Details')} title={getLangText('Further Details')}
show={edition.acl.acl_edit || show={edition.acl.acl_edit || Object.keys(edition.extra_data).length || edition.other_data.length}>
Object.keys(edition.extra_data).length > 0 ||
edition.other_data.length > 0}>
<FurtherDetailsType <FurtherDetailsType
editable={edition.acl.acl_edit} editable={edition.acl.acl_edit}
pieceId={edition.parent} pieceId={edition.parent}
@ -144,10 +144,8 @@ let Edition = React.createClass({
otherData={edition.other_data} otherData={edition.other_data}
handleSuccess={loadEdition} /> handleSuccess={loadEdition} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph title={getLangText('SPOOL Details')}>
title={getLangText('SPOOL Details')}> <SpoolDetails edition={edition} />
<SpoolDetails
edition={edition} />
</CollapsibleParagraph> </CollapsibleParagraph>
</Col> </Col>
</Row> </Row>
@ -158,60 +156,56 @@ let Edition = React.createClass({
let EditionSummary = React.createClass({ let EditionSummary = React.createClass({
propTypes: { propTypes: {
currentUser: React.PropTypes.object.isRequired,
edition: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object.isRequired,
actionPanelButtonListType: React.PropTypes.func, actionPanelButtonListType: React.PropTypes.func,
edition: React.PropTypes.object,
currentUser: React.PropTypes.object,
handleSuccess: React.PropTypes.func handleSuccess: React.PropTypes.func
}, },
handleSuccess() { getStatus() {
this.props.handleSuccess(); const { status } = this.props.edition;
},
getStatus(){ return status.length ? (
let status = null; <DetailProperty
if (this.props.edition.status.length > 0){ label="STATUS"
let statusStr = this.props.edition.status.join(', ').replace(/_/g, ' '); value={status.join(', ').replace(/_/g, ' ')} />
status = <DetailProperty label="STATUS" value={ statusStr }/>; ) : null;
if (this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer){
status = (
<DetailProperty label="STATUS" value={ statusStr } />
);
}
}
return status;
}, },
render() { render() {
let { actionPanelButtonListType, edition, currentUser } = this.props; const { actionPanelButtonListType, currentUser, edition, handleSuccess, whitelabel } = this.props;
return ( return (
<div className="ascribe-detail-header"> <div className="ascribe-detail-header">
<DetailProperty <DetailProperty
label={getLangText('EDITION')} label={getLangText('EDITION')}
value={ edition.edition_number + ' ' + getLangText('of') + ' ' + edition.num_editions} /> value={edition.edition_number + ' ' + getLangText('of') + ' ' + edition.num_editions} />
<DetailProperty <DetailProperty
label={getLangText('ID')} label={getLangText('ID')}
value={ edition.bitcoin_id } value={edition.bitcoin_id}
ellipsis={true} /> ellipsis={true} />
<DetailProperty <DetailProperty
label={getLangText('OWNER')} label={getLangText('OWNER')}
value={ edition.owner } /> value={edition.owner} />
<LicenseDetail license={edition.license_type}/> <LicenseDetail license={edition.license_type} />
{this.getStatus()} {this.getStatus()}
{/* {/*
`acl_view` is always available in `edition.acl`, therefore if it has `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 no more than 1 key, we're hiding the `DetailProperty` actions as otherwise
`AclInformation` would show up `AclInformation` would show up
*/} */}
<AclProxy show={currentUser && currentUser.email && Object.keys(edition.acl).length > 1}> <AclProxy show={currentUser.email && Object.keys(edition.acl).length > 1}>
<DetailProperty <DetailProperty
label={getLangText('ACTIONS')} label={getLangText('ACTIONS')}
className="hidden-print"> className="hidden-print">
<EditionActionPanel <EditionActionPanel
actionPanelButtonListType={actionPanelButtonListType} actionPanelButtonListType={actionPanelButtonListType}
edition={edition}
currentUser={currentUser} currentUser={currentUser}
handleSuccess={this.handleSuccess} /> edition={edition}
handleSuccess={handleSuccess}
whitelabel={whitelabel} />
</DetailProperty> </DetailProperty>
</AclProxy> </AclProxy>
<hr/> <hr/>
@ -360,4 +354,5 @@ let SpoolDetails = React.createClass({
} }
}); });
export default Edition; export default Edition;

View File

@ -36,9 +36,11 @@ import { getLangText } from '../../utils/lang_utils';
*/ */
let EditionActionPanel = React.createClass({ let EditionActionPanel = React.createClass({
propTypes: { propTypes: {
currentUser: React.PropTypes.object.isRequired,
edition: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object.isRequired,
actionPanelButtonListType: React.PropTypes.func, actionPanelButtonListType: React.PropTypes.func,
edition: React.PropTypes.object,
currentUser: React.PropTypes.object,
handleSuccess: React.PropTypes.func handleSuccess: React.PropTypes.func
}, },
@ -87,39 +89,42 @@ let EditionActionPanel = React.createClass({
handleSuccess(response) { handleSuccess(response) {
this.refreshCollection(); this.refreshCollection();
this.props.handleSuccess();
if (response){ if (response) {
let notification = new GlobalNotificationModel(response.notification, 'success'); const notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
} }
if (typeof this.props.handleSuccess === 'function') {
this.props.handleSuccess();
}
}, },
render() { render() {
const { const {
actionPanelButtonListType: ActionPanelButtonListType, actionPanelButtonListType: ActionPanelButtonListType,
currentUser,
edition, edition,
currentUser } = this.props; whitelabel } = this.props;
if (edition && if (edition.notifications && edition.notifications.length) {
edition.notifications &&
edition.notifications.length > 0){
return ( return (
<ListRequestActions <ListRequestActions
pieceOrEditions={[edition]}
currentUser={currentUser} currentUser={currentUser}
handleSuccess={this.handleSuccess} notifications={edition.notifications}
notifications={edition.notifications}/>); pieceOrEditions={[edition]}
} handleSuccess={this.handleSuccess} />);
} else {
else {
return ( return (
<Row> <Row>
<Col md={12}> <Col md={12}>
<ActionPanelButtonListType <ActionPanelButtonListType
className="ascribe-button-list"
availableAcls={edition.acl} availableAcls={edition.acl}
className="ascribe-button-list"
currentUser={currentUser}
handleSuccess={this.handleSuccess}
pieceOrEditions={[edition]} pieceOrEditions={[edition]}
handleSuccess={this.handleSuccess}> whitelabel={whitelabel}>
<AclProxy <AclProxy
aclObject={edition.acl} aclObject={edition.acl}
aclName="acl_withdraw_transfer"> aclName="acl_withdraw_transfer">

View File

@ -9,16 +9,12 @@ import { ResourceNotFoundError } from '../../models/errors';
import EditionActions from '../../actions/edition_actions'; import EditionActions from '../../actions/edition_actions';
import EditionStore from '../../stores/edition_store'; import EditionStore from '../../stores/edition_store';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import Edition from './edition'; import Edition from './edition';
import AscribeSpinner from '../ascribe_spinner'; import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils'; import { setDocumentTitle } from '../../utils/dom_utils';
import { mergeOptions } from '../../utils/general_utils';
/** /**
@ -28,24 +24,26 @@ let EditionContainer = React.createClass({
propTypes: { propTypes: {
actionPanelButtonListType: React.PropTypes.func, actionPanelButtonListType: React.PropTypes.func,
furtherDetailsType: React.PropTypes.func, furtherDetailsType: React.PropTypes.func,
// Provided from AscribeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object.isRequired,
// Provided from router
location: React.PropTypes.object,
params: React.PropTypes.object params: React.PropTypes.object
}, },
mixins: [History, ReactError], mixins: [History, ReactError],
getInitialState() { getInitialState() {
return mergeOptions( return EditionStore.getInitialState();
EditionStore.getInitialState(),
UserStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
EditionStore.listen(this.onChange); EditionStore.listen(this.onChange);
UserStore.listen(this.onChange);
this.loadEdition(); this.loadEdition();
UserActions.fetchCurrentUser();
}, },
// This is done to update the container when the user clicks on the prev or next // This is done to update the container when the user clicks on the prev or next
@ -68,19 +66,10 @@ let EditionContainer = React.createClass({
componentWillUnmount() { componentWillUnmount() {
window.clearInterval(this.state.timerId); window.clearInterval(this.state.timerId);
EditionStore.unlisten(this.onChange); EditionStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
this.setState(state); this.setState(state);
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.fetchEdition(this.props.params.editionId), 10000);
this.setState({timerId: timerId});
}
}
}, },
loadEdition(editionId = this.props.params.editionId) { loadEdition(editionId = this.props.params.editionId) {
@ -88,8 +77,8 @@ let EditionContainer = React.createClass({
}, },
render() { render() {
const { edition, currentUser, coaMeta } = this.state; const { actionPanelButtonListType, currentUser, furtherDetailsType, whitelabel } = this.props;
const { actionPanelButtonListType, furtherDetailsType } = this.props; const { edition, coaMeta } = this.state;
if (edition.id) { if (edition.id) {
setDocumentTitle(`${edition.artist_name}, ${edition.title}`); setDocumentTitle(`${edition.artist_name}, ${edition.title}`);
@ -97,11 +86,12 @@ let EditionContainer = React.createClass({
return ( return (
<Edition <Edition
actionPanelButtonListType={actionPanelButtonListType} actionPanelButtonListType={actionPanelButtonListType}
furtherDetailsType={furtherDetailsType}
edition={edition}
coaError={coaMeta.err} coaError={coaMeta.err}
currentUser={currentUser} currentUser={currentUser}
loadEdition={this.loadEdition} /> edition={edition}
furtherDetailsType={furtherDetailsType}
loadEdition={this.loadEdition}
whitelabel={whitelabel} />
); );
} else { } else {
return ( return (

View File

@ -14,19 +14,24 @@ import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
const { func, bool, number, object, string, arrayOf } = React.PropTypes;
let FurtherDetailsFileuploader = React.createClass({ let FurtherDetailsFileuploader = React.createClass({
propTypes: { propTypes: {
pieceId: React.PropTypes.number.isRequired, pieceId: number.isRequired,
areAssetsDownloadable: React.PropTypes.bool, editable: bool,
editable: React.PropTypes.bool, label: string,
isReadyForFormSubmission: React.PropTypes.func, otherData: arrayOf(object),
label: React.PropTypes.string,
multiple: React.PropTypes.bool, // Props for ReactS3FineUploader
otherData: React.PropTypes.arrayOf(React.PropTypes.object), areAssetsDownloadable: bool,
onValidationFailed: React.PropTypes.func, isReadyForFormSubmission: func,
setIsUploadReady: React.PropTypes.func, submitFile: func, // TODO: rename to onSubmitFile
submitFile: React.PropTypes.func, onValidationFailed: func,
multiple: bool,
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
showErrorPrompt: bool,
validation: ReactS3FineUploader.propTypes.validation validation: ReactS3FineUploader.propTypes.validation
}, },
@ -40,36 +45,57 @@ let FurtherDetailsFileuploader = React.createClass({
}, },
render() { render() {
const {
editable,
isReadyForFormSubmission,
multiple,
onValidationFailed,
otherData,
pieceId,
setIsUploadReady,
showErrorPrompt,
submitFile,
validation } = this.props;
// Essentially there a three cases important to the fileuploader // Essentially there a three cases important to the fileuploader
// //
// 1. there is no other_data => do not show the fileuploader at all (where otherData is now an array) // 1. there is no other_data => do not show the fileuploader at all (where otherData is now an array)
// 2. there is other_data, but user has no edit rights => show fileuploader but without action buttons // 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 fileuploader with all action buttons // 3. both other_data and editable are defined or true => show fileuploader with all action buttons
if (!this.props.editable && (!this.props.otherData || this.props.otherData.length === 0)) { if (!editable && (!otherData || otherData.length === 0)) {
return null; return null;
} }
let otherDataIds = this.props.otherData ? this.props.otherData.map((data) => data.id).join() : null; let otherDataIds = otherData ? otherData.map((data) => data.id).join() : null;
return ( return (
<Property <Property
name="other_data_key" name="other_data_key"
label={this.props.label}> label={this.props.label}>
<ReactS3FineUploader <ReactS3FineUploader
areAssetsDownloadable
areAssetsEditable={editable}
createBlobRoutine={{
url: ApiUrls.blob_otherdatas,
pieceId: pieceId
}}
deleteFile={{
enabled: true,
method: 'DELETE',
endpoint: AppConstants.serverUrl + 's3/delete',
customHeaders: {
'X-CSRFToken': getCookie(AppConstants.csrftoken)
}
}}
isReadyForFormSubmission={isReadyForFormSubmission}
keyRoutine={{ keyRoutine={{
url: AppConstants.serverUrl + 's3/key/', url: AppConstants.serverUrl + 's3/key/',
fileClass: 'otherdata', fileClass: 'otherdata',
pieceId: this.props.pieceId pieceId: pieceId
}} }}
createBlobRoutine={{ multiple={multiple}
url: ApiUrls.blob_otherdatas, onValidationFailed={onValidationFailed}
pieceId: this.props.pieceId setIsUploadReady={setIsUploadReady}
}}
validation={this.props.validation}
submitFile={this.props.submitFile}
onValidationFailed={this.props.onValidationFailed}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
session={{ session={{
endpoint: AppConstants.serverUrl + 'api/blob/otherdatas/fineuploader_session/', endpoint: AppConstants.serverUrl + 'api/blob/otherdatas/fineuploader_session/',
customHeaders: { customHeaders: {
@ -89,17 +115,9 @@ let FurtherDetailsFileuploader = React.createClass({
'X-CSRFToken': getCookie(AppConstants.csrftoken) 'X-CSRFToken': getCookie(AppConstants.csrftoken)
} }
}} }}
deleteFile={{ submitFile={submitFile}
enabled: true, showErrorPrompt={showErrorPrompt}
method: 'DELETE', validation={validation} />
endpoint: AppConstants.serverUrl + 's3/delete',
customHeaders: {
'X-CSRFToken': getCookie(AppConstants.csrftoken)
}
}}
areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={this.props.editable}
multiple={this.props.multiple} />
</Property> </Property>
); );
} }

View File

@ -22,12 +22,14 @@ const EMBED_IFRAME_HEIGHT = {
video: 315, video: 315,
audio: 62 audio: 62
}; };
const ENCODE_UPDATE_TIME = 5000;
let MediaContainer = React.createClass({ let MediaContainer = React.createClass({
propTypes: { propTypes: {
content: React.PropTypes.object, content: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object, refreshObject: React.PropTypes.func.isRequired,
refreshObject: React.PropTypes.func
currentUser: React.PropTypes.object
}, },
getInitialState() { getInitialState() {
@ -37,14 +39,16 @@ let MediaContainer = React.createClass({
}, },
componentDidMount() { componentDidMount() {
if (!this.props.content.digital_work) { const { content: { digital_work: digitalWork }, refreshObject } = this.props;
return;
}
const isEncoding = this.props.content.digital_work.isEncoding; if (digitalWork) {
if (this.props.content.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { const isEncoding = digitalWork.isEncoding;
let timerId = window.setInterval(this.props.refreshObject, 10000);
this.setState({timerId: timerId}); if (digitalWork.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) {
this.setState({
timerId: window.setInterval(refreshObject, ENCODE_UPDATE_TIME)
});
}
} }
}, },
@ -105,7 +109,7 @@ let MediaContainer = React.createClass({
{'<iframe width="560" height="' + height + '" src="https://embed.ascribe.io/content/' {'<iframe width="560" height="' + height + '" src="https://embed.ascribe.io/content/'
+ content.bitcoin_id + '" frameborder="0" allowfullscreen></iframe>'} + content.bitcoin_id + '" frameborder="0" allowfullscreen></iframe>'}
</pre> </pre>
}/> } />
); );
} }
return ( return (
@ -136,7 +140,7 @@ let MediaContainer = React.createClass({
If it turns out that `fileExtension` is an empty string, we're just If it turns out that `fileExtension` is an empty string, we're just
using the label 'file'. using the label 'file'.
*/} */}
{getLangText('Download')} .{fileExtension || 'file'} <Glyphicon glyph="cloud-download"/> {getLangText('Download')} .{fileExtension || 'file'} <Glyphicon glyph="cloud-download" />
</Button> </Button>
</AclProxy> </AclProxy>
{embed} {embed}

View File

@ -2,63 +2,67 @@
import React from 'react'; import React from 'react';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import Form from './../ascribe_forms/form'; import Form from './../ascribe_forms/form';
import Property from './../ascribe_forms/property'; import Property from './../ascribe_forms/property';
import InputTextAreaToggable from './../ascribe_forms/input_textarea_toggable'; import InputTextAreaToggable from './../ascribe_forms/input_textarea_toggable';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let Note = React.createClass({ let Note = React.createClass({
propTypes: { propTypes: {
url: React.PropTypes.string, currentUser: React.PropTypes.object.isRequired,
id: React.PropTypes.func, id: React.PropTypes.func.isRequired,
label: React.PropTypes.string, url: React.PropTypes.string.isRequired,
currentUser: React.PropTypes.object,
defaultValue: React.PropTypes.string, defaultValue: React.PropTypes.string,
editable: React.PropTypes.bool, editable: React.PropTypes.bool,
show: React.PropTypes.bool, label: React.PropTypes.string,
placeholder: React.PropTypes.string, placeholder: React.PropTypes.string,
show: React.PropTypes.bool,
successMessage: React.PropTypes.string successMessage: React.PropTypes.string
}, },
getDefaultProps() { getDefaultProps() {
return { return {
editable: true, editable: true,
show: true,
placeholder: getLangText('Enter a note'), placeholder: getLangText('Enter a note'),
show: true,
successMessage: getLangText('Note saved') successMessage: getLangText('Note saved')
}; };
}, },
showNotification(){ showNotification() {
let notification = new GlobalNotificationModel(this.props.successMessage, 'success'); const notification = new GlobalNotificationModel(this.props.successMessage, 'success');
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
render() { render() {
if ((!!this.props.currentUser.username && this.props.editable || !this.props.editable ) && this.props.show) { const { currentUser, defaultValue, editable, id, label, placeholder, show, url } = this.props;
if ((!!currentUser.username && editable || !editable ) && show) {
return ( return (
<Form <Form
url={this.props.url} url={url}
getFormData={this.props.id} getFormData={id}
handleSuccess={this.showNotification} handleSuccess={this.showNotification}
disabled={!this.props.editable}> disabled={!editable}>
<Property <Property
name='note' name='note'
label={this.props.label}> label={label}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
defaultValue={this.props.defaultValue} defaultValue={defaultValue}
placeholder={this.props.placeholder}/> placeholder={placeholder} />
</Property> </Property>
<hr /> <hr />
</Form> </Form>
); );
} else {
return null;
} }
return null;
} }
}); });

View File

@ -15,19 +15,19 @@ import MediaContainer from './media_container';
*/ */
let Piece = React.createClass({ let Piece = React.createClass({
propTypes: { propTypes: {
piece: React.PropTypes.object, piece: React.PropTypes.object.isRequired,
buttons: React.PropTypes.object,
currentUser: React.PropTypes.object, currentUser: React.PropTypes.object,
header: React.PropTypes.object, header: React.PropTypes.object,
subheader: React.PropTypes.object, subheader: React.PropTypes.object,
buttons: React.PropTypes.object,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element React.PropTypes.element
]) ])
}, },
updatePiece() {
updateObject() {
return PieceActions.fetchPiece(this.props.piece.id); return PieceActions.fetchPiece(this.props.piece.id);
}, },
@ -40,7 +40,7 @@ let Piece = React.createClass({
<MediaContainer <MediaContainer
content={piece} content={piece}
currentUser={currentUser} currentUser={currentUser}
refreshObject={this.updateObject} /> refreshObject={this.updatePiece} />
</Col> </Col>
<Col md={6} className="ascribe-edition-details ascribe-print-col-right"> <Col md={6} className="ascribe-edition-details ascribe-print-col-right">
{header} {header}

View File

@ -7,39 +7,35 @@ import Moment from 'moment';
import ReactError from '../../mixins/react_error'; import ReactError from '../../mixins/react_error';
import { ResourceNotFoundError } from '../../models/errors'; import { ResourceNotFoundError } from '../../models/errors';
import EditionListActions from '../../actions/edition_list_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import PieceActions from '../../actions/piece_actions'; import PieceActions from '../../actions/piece_actions';
import PieceStore from '../../stores/piece_store'; import PieceStore from '../../stores/piece_store';
import PieceListActions from '../../actions/piece_list_actions'; import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store'; 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 FurtherDetails from './further_details';
import DetailProperty from './detail_property'; import DetailProperty from './detail_property';
import LicenseDetail from './license_detail';
import HistoryIterator from './history_iterator'; import HistoryIterator from './history_iterator';
import LicenseDetail from './license_detail';
import Note from './note';
import Piece from './piece';
import AclButtonList from './../ascribe_buttons/acl_button_list'; import AclButtonList from './../ascribe_buttons/acl_button_list';
import CreateEditionsForm from '../ascribe_forms/create_editions_form'; import AclInformation from '../ascribe_buttons/acl_information';
import CreateEditionsButton from '../ascribe_buttons/create_editions_button'; import CreateEditionsButton from '../ascribe_buttons/create_editions_button';
import DeleteButton from '../ascribe_buttons/delete_button'; import DeleteButton from '../ascribe_buttons/delete_button';
import AclInformation from '../ascribe_buttons/acl_information'; import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph';
import AclProxy from '../acl_proxy';
import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import ListRequestActions from '../ascribe_forms/list_form_request_actions'; import ListRequestActions from '../ascribe_forms/list_form_request_actions';
import GlobalNotificationModel from '../../models/global_notification_model'; import AclProxy from '../acl_proxy';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import Note from './note';
import ApiUrls from '../../constants/api_urls'; import ApiUrls from '../../constants/api_urls';
import AscribeSpinner from '../ascribe_spinner'; import AscribeSpinner from '../ascribe_spinner';
@ -54,6 +50,13 @@ import { setDocumentTitle } from '../../utils/dom_utils';
let PieceContainer = React.createClass({ let PieceContainer = React.createClass({
propTypes: { propTypes: {
furtherDetailsType: React.PropTypes.func, furtherDetailsType: React.PropTypes.func,
// Provided from AscribeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object,
params: React.PropTypes.object params: React.PropTypes.object
}, },
@ -67,7 +70,6 @@ let PieceContainer = React.createClass({
getInitialState() { getInitialState() {
return mergeOptions( return mergeOptions(
UserStore.getState(),
PieceListStore.getState(), PieceListStore.getState(),
PieceStore.getInitialState(), PieceStore.getInitialState(),
{ {
@ -77,12 +79,10 @@ let PieceContainer = React.createClass({
}, },
componentDidMount() { componentDidMount() {
UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange); PieceListStore.listen(this.onChange);
PieceStore.listen(this.onChange); PieceStore.listen(this.onChange);
this.loadPiece(); this.loadPiece();
UserActions.fetchCurrentUser();
}, },
// This is done to update the container when the user clicks on the prev or next // This is done to update the container when the user clicks on the prev or next
@ -105,7 +105,6 @@ let PieceContainer = React.createClass({
componentWillUnmount() { componentWillUnmount() {
PieceStore.unlisten(this.onChange); PieceStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange);
}, },
@ -207,15 +206,17 @@ let PieceContainer = React.createClass({
}, },
getActions() { getActions() {
const { piece, currentUser } = this.state; const { piece } = this.state;
const { currentUser } = this.props;
if (piece.notifications && piece.notifications.length > 0) { if (piece.notifications && piece.notifications.length > 0) {
return ( return (
<ListRequestActions <ListRequestActions
pieceOrEditions={piece}
currentUser={currentUser} currentUser={currentUser}
handleSuccess={this.loadPiece} handleSuccess={this.loadPiece}
notifications={piece.notifications} />); notifications={piece.notifications}
pieceOrEditions={piece} />
);
} else { } else {
return ( return (
<AclProxy <AclProxy
@ -229,8 +230,9 @@ let PieceContainer = React.createClass({
label={getLangText('ACTIONS')} label={getLangText('ACTIONS')}
className="hidden-print"> className="hidden-print">
<AclButtonList <AclButtonList
className="ascribe-button-list"
availableAcls={piece.acl} availableAcls={piece.acl}
className="ascribe-button-list"
currentUser={currentUser}
pieceOrEditions={piece} pieceOrEditions={piece}
handleSuccess={this.loadPiece}> handleSuccess={this.loadPiece}>
<CreateEditionsButton <CreateEditionsButton
@ -255,8 +257,8 @@ let PieceContainer = React.createClass({
}, },
render() { render() {
const { furtherDetailsType: FurtherDetailsType } = this.props; const { currentUser, furtherDetailsType: FurtherDetailsType } = this.props;
const { currentUser, piece } = this.state; const { piece } = this.state;
if (piece.id) { if (piece.id) {
setDocumentTitle(`${piece.artist_name}, ${piece.title}`); setDocumentTitle(`${piece.artist_name}, ${piece.title}`);

View File

@ -20,16 +20,17 @@ import { getAclFormMessage, getAclFormDataId } from '../../utils/form_utils';
let AclFormFactory = React.createClass({ let AclFormFactory = React.createClass({
propTypes: { propTypes: {
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired, action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
autoFocusProperty: React.PropTypes.string,
currentUser: React.PropTypes.object,
email: React.PropTypes.string,
message: React.PropTypes.string,
labels: React.PropTypes.object,
pieceOrEditions: React.PropTypes.oneOfType([ pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object, React.PropTypes.object,
React.PropTypes.array React.PropTypes.array
]).isRequired, ]).isRequired,
autoFocusProperty: React.PropTypes.string,
currentUser: React.PropTypes.object,
email: React.PropTypes.string,
handleSuccess: React.PropTypes.func, handleSuccess: React.PropTypes.func,
message: React.PropTypes.string,
labels: React.PropTypes.object,
showNotification: React.PropTypes.bool showNotification: React.PropTypes.bool
}, },
@ -105,7 +106,7 @@ let AclFormFactory = React.createClass({
message={formMessage} message={formMessage}
id={this.getFormDataId()} id={this.getFormDataId()}
url={this.isPiece() ? ApiUrls.ownership_loans_pieces url={this.isPiece() ? ApiUrls.ownership_loans_pieces
: ApiUrls.ownership_loans_editions} : ApiUrls.ownership_loans_editions}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} /> handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
); );
} else if (action === 'acl_loan_request') { } else if (action === 'acl_loan_request') {
@ -122,7 +123,7 @@ let AclFormFactory = React.createClass({
message={formMessage} message={formMessage}
id={this.getFormDataId()} id={this.getFormDataId()}
url={this.isPiece() ? ApiUrls.ownership_shares_pieces url={this.isPiece() ? ApiUrls.ownership_shares_pieces
: ApiUrls.ownership_shares_editions} : ApiUrls.ownership_shares_editions}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} /> handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
); );
} else { } else {

View File

@ -156,7 +156,7 @@ let Form = React.createClass({
for(let ref in this.refs) { for(let ref in this.refs) {
if(this.refs[ref] && typeof this.refs[ref].handleSuccess === 'function'){ if(this.refs[ref] && typeof this.refs[ref].handleSuccess === 'function'){
this.refs[ref].handleSuccess(); this.refs[ref].handleSuccess(response);
} }
} }
this.setState({ this.setState({

View File

@ -123,8 +123,7 @@ let ConsignForm = React.createClass({
<Property <Property
name='contract_agreement' name='contract_agreement'
label={getLangText('Consign Contract')} label={getLangText('Consign Contract')}
className="ascribe-property-collapsible-toggle" className="ascribe-property-collapsible-toggle">
style={{paddingBottom: 0}}>
<InputContractAgreementCheckbox <InputContractAgreementCheckbox
createPublicContractAgreement={createPublicContractAgreement} createPublicContractAgreement={createPublicContractAgreement}
email={email} /> email={email} />

View File

@ -15,30 +15,30 @@ import { getLangText } from '../../utils/lang_utils';
let CopyrightAssociationForm = React.createClass({ let CopyrightAssociationForm = React.createClass({
propTypes: { propTypes: {
currentUser: React.PropTypes.object currentUser: React.PropTypes.object.isRequired
}, },
handleSubmitSuccess(){ handleSubmitSuccess() {
let notification = getLangText('Copyright association updated'); const notification = new GlobalNotificationModel(getLangText('Copyright association updated'), 'success', 10000);
notification = new GlobalNotificationModel(notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
getProfileFormData(){ getProfileFormData() {
return {email: this.props.currentUser.email}; return { email: this.props.currentUser.email };
}, },
render() { render() {
let selectedState; const { currentUser } = this.props;
let selectDefaultValue = ' -- ' + getLangText('select an association') + ' -- '; const selectDefaultValue = ' -- ' + getLangText('select an association') + ' -- ';
if (this.props.currentUser && this.props.currentUser.profile let selectedState = selectDefaultValue;
&& this.props.currentUser.profile.copyright_association) { if (currentUser.profile && currentUser.profile.copyright_association) {
selectedState = AppConstants.copyrightAssociations.indexOf(this.props.currentUser.profile.copyright_association); if (AppConstants.copyrightAssociations.indexOf(currentUser.profile.copyright_association) !== -1) {
selectedState = selectedState !== -1 ? AppConstants.copyrightAssociations[selectedState] : selectDefaultValue; selectedState = AppConstants.copyrightAssociations[selectedState];
}
} }
if (this.props.currentUser && this.props.currentUser.email){ if (currentUser.email) {
return ( return (
<Form <Form
ref='form' ref='form'
@ -48,8 +48,7 @@ let CopyrightAssociationForm = React.createClass({
<Property <Property
name="copyright_association" name="copyright_association"
className="ascribe-property-collapsible-toggle" className="ascribe-property-collapsible-toggle"
label={getLangText('Copyright Association')} label={getLangText('Copyright Association')}>
style={{paddingBottom: 0}}>
<select defaultValue={selectedState} name="contract"> <select defaultValue={selectedState} name="contract">
<option <option
name={0} name={0}
@ -72,8 +71,9 @@ let CopyrightAssociationForm = React.createClass({
<hr /> <hr />
</Form> </Form>
); );
} else {
return null;
} }
return null;
} }
}); });

View File

@ -171,7 +171,7 @@ let LoanForm = React.createClass({
editable={!gallery} editable={!gallery}
overrideForm={!!gallery}> overrideForm={!!gallery}>
<input <input
value={gallery} defaultValue={gallery}
type="text" type="text"
placeholder={getLangText('Gallery/exhibition (optional)')}/> placeholder={getLangText('Gallery/exhibition (optional)')}/>
</Property> </Property>
@ -210,8 +210,7 @@ let LoanForm = React.createClass({
<Property <Property
name='contract_agreement' name='contract_agreement'
label={getLangText('Loan Contract')} label={getLangText('Loan Contract')}
className="ascribe-property-collapsible-toggle" className="ascribe-property-collapsible-toggle">
style={{paddingBottom: 0}}>
<InputContractAgreementCheckbox <InputContractAgreementCheckbox
createPublicContractAgreement={createPublicContractAgreement} createPublicContractAgreement={createPublicContractAgreement}
email={email} /> email={email} />

View File

@ -6,7 +6,6 @@ import { History } from 'react-router';
import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions'; import GlobalNotificationActions from '../../actions/global_notification_actions';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions'; import UserActions from '../../actions/user_actions';
import Form from './form'; import Form from './form';
@ -23,8 +22,6 @@ let LoginForm = React.createClass({
propTypes: { propTypes: {
headerMessage: React.PropTypes.string, headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string, submitMessage: React.PropTypes.string,
redirectOnLoggedIn: React.PropTypes.bool,
redirectOnLoginSuccess: React.PropTypes.bool,
location: React.PropTypes.object location: React.PropTypes.object
}, },
@ -32,40 +29,26 @@ let LoginForm = React.createClass({
getDefaultProps() { getDefaultProps() {
return { return {
headerMessage: getLangText('Enter ascribe'), headerMessage: getLangText('Enter') + ' ascribe',
submitMessage: getLangText('Log in'), submitMessage: getLangText('Log in')
redirectOnLoggedIn: true,
redirectOnLoginSuccess: true
}; };
}, },
getInitialState() { handleSuccess({ success }) {
return UserStore.getState(); const notification = new GlobalNotificationModel(getLangText('Login successful'), 'success', 10000);
},
componentDidMount() {
UserStore.listen(this.onChange);
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
handleSuccess({ success }){
let notification = new GlobalNotificationModel('Login successful', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
if(success) { if (success) {
UserActions.fetchCurrentUser(true); UserActions.fetchCurrentUser(true);
} }
}, },
render() { render() {
let email = this.props.location.query.email || null; const {
headerMessage,
location: { query: { email: emailQuery } },
submitMessage } = this.props;
return ( return (
<Form <Form
className="ascribe-form-bordered" className="ascribe-form-bordered"
@ -77,7 +60,7 @@ let LoginForm = React.createClass({
<button <button
type="submit" type="submit"
className="btn btn-default btn-wide"> className="btn btn-default btn-wide">
{this.props.submitMessage} {submitMessage}
</button>} </button>}
spinner={ spinner={
<span className="btn btn-default btn-wide btn-spinner"> <span className="btn btn-default btn-wide btn-spinner">
@ -85,7 +68,7 @@ let LoginForm = React.createClass({
</span> </span>
}> }>
<div className="ascribe-form-header"> <div className="ascribe-form-header">
<h3>{this.props.headerMessage}</h3> <h3>{headerMessage}</h3>
</div> </div>
<Property <Property
name='email' name='email'
@ -93,7 +76,7 @@ let LoginForm = React.createClass({
<input <input
type="email" type="email"
placeholder={getLangText('Enter your email')} placeholder={getLangText('Enter your email')}
defaultValue={email} defaultValue={emailQuery}
required/> required/>
</Property> </Property>
<Property <Property

View File

@ -2,15 +2,13 @@
import React from 'react'; import React from 'react';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
import Form from './form'; import Form from './form';
import Property from './property'; import Property from './property';
import InputFineUploader from './input_fineuploader'; import InputFineUploader from './input_fineuploader';
import FormSubmitButton from '../ascribe_buttons/form_submit_button'; import FormSubmitButton from '../ascribe_buttons/form_submit_button';
import { FileStatus } from '../ascribe_uploader/react_s3_fine_uploader_utils';
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button'; import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
import AscribeSpinner from '../ascribe_spinner'; import AscribeSpinner from '../ascribe_spinner';
@ -20,22 +18,24 @@ import AppConstants from '../../constants/application_constants';
import { validationParts, validationTypes } from '../../constants/uploader_constants'; import { validationParts, validationTypes } from '../../constants/uploader_constants';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
let RegisterPieceForm = React.createClass({ let RegisterPieceForm = React.createClass({
propTypes: { propTypes: {
currentUser: React.PropTypes.object.isRequired,
headerMessage: React.PropTypes.string, headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string, submitMessage: React.PropTypes.string,
handleSuccess: React.PropTypes.func,
isFineUploaderActive: React.PropTypes.bool,
isFineUploaderEditable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
enableSeparateThumbnail: React.PropTypes.bool, enableSeparateThumbnail: React.PropTypes.bool,
isFineUploaderActive: React.PropTypes.bool,
isFineUploaderEditable: React.PropTypes.bool,
handleSuccess: React.PropTypes.func,
// For this form to work with SlideContainer, we sometimes have to disable it // For this form to work with SlideContainer, we sometimes have to disable it
disabled: React.PropTypes.bool, disabled: React.PropTypes.bool,
location: React.PropTypes.object, location: React.PropTypes.object,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
@ -52,26 +52,10 @@ let RegisterPieceForm = React.createClass({
}; };
}, },
getInitialState(){ getInitialState() {
return mergeOptions( return {
{ digitalWorkFile: null
digitalWorkFile: null }
},
UserStore.getState()
);
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
}, },
/** /**
@ -89,7 +73,7 @@ let RegisterPieceForm = React.createClass({
handleChangedDigitalWork(digitalWorkFile) { handleChangedDigitalWork(digitalWorkFile) {
if (digitalWorkFile && if (digitalWorkFile &&
(digitalWorkFile.status === 'deleted' || digitalWorkFile.status === 'canceled')) { (digitalWorkFile.status === FileStatus.DELETED || digitalWorkFile.status === FileStatus.CANCELED)) {
this.refs.form.refs.thumbnail_file.reset(); this.refs.form.refs.thumbnail_file.reset();
// Manually we need to set the ready state for `thumbnailKeyReady` back // Manually we need to set the ready state for `thumbnailKeyReady` back
@ -108,8 +92,8 @@ let RegisterPieceForm = React.createClass({
fineuploader.setThumbnailForFileId( fineuploader.setThumbnailForFileId(
digitalWorkFile.id, digitalWorkFile.id,
// if thumbnail was delete, we delete it from the display as well // if thumbnail was deleted, we delete it from the display as well
thumbnailFile.status !== 'deleted' ? thumbnailFile.url : null thumbnailFile.status !== FileStatus.DELETED ? thumbnailFile.url : null
); );
}, },
@ -133,16 +117,17 @@ let RegisterPieceForm = React.createClass({
}, },
render() { render() {
const { disabled, const {
handleSuccess, children,
submitMessage, currentUser,
headerMessage, disabled,
isFineUploaderActive, enableLocalHashing,
isFineUploaderEditable, handleSuccess,
location, headerMessage,
children, isFineUploaderActive,
enableLocalHashing } = this.props; isFineUploaderEditable,
const { currentUser} = this.state; location,
submitMessage } = this.props;
const profileHashLocally = currentUser && currentUser.profile ? currentUser.profile.hash_locally : false; const profileHashLocally = currentUser && currentUser.profile ? currentUser.profile.hash_locally : false;
const hashLocally = profileHashLocally && enableLocalHashing; const hashLocally = profileHashLocally && enableLocalHashing;
@ -191,7 +176,8 @@ let RegisterPieceForm = React.createClass({
disabled={!isFineUploaderEditable} disabled={!isFineUploaderEditable}
enableLocalHashing={hashLocally} enableLocalHashing={hashLocally}
uploadMethod={location.query.method} uploadMethod={location.query.method}
handleChangedFile={this.handleChangedDigitalWork}/> handleChangedFile={this.handleChangedDigitalWork}
showErrorPrompt />
</Property> </Property>
<Property <Property
name="thumbnail_file" name="thumbnail_file"

View File

@ -21,11 +21,12 @@ import { getLangText } from '../../utils/lang_utils.js';
let RequestActionForm = React.createClass({ let RequestActionForm = React.createClass({
propTypes: { propTypes: {
notifications: React.PropTypes.object.isRequired,
pieceOrEditions: React.PropTypes.oneOfType([ pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object, React.PropTypes.object,
React.PropTypes.array React.PropTypes.array
]).isRequired, ]).isRequired,
notifications: React.PropTypes.object,
currentUser: React.PropTypes.object, currentUser: React.PropTypes.object,
handleSuccess: React.PropTypes.func handleSuccess: React.PropTypes.func
}, },

View File

@ -128,8 +128,7 @@ let SendContractAgreementForm = React.createClass({
<Property <Property
name='appendix' name='appendix'
checkboxLabel={getLangText('Add appendix to the contract')} checkboxLabel={getLangText('Add appendix to the contract')}
expanded={false} expanded={false}>
style={{paddingBottom: 0}}>
<span>{getLangText('Appendix')}</span> <span>{getLangText('Appendix')}</span>
{/* We're using disabled on a form here as PropertyCollapsible currently {/* We're using disabled on a form here as PropertyCollapsible currently
does not support the disabled + overrideForm functionality */} does not support the disabled + overrideForm functionality */}

View File

@ -3,12 +3,11 @@
import React from 'react'; import React from 'react';
import { History } from 'react-router'; import { History } from 'react-router';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions'; import GlobalNotificationActions from '../../actions/global_notification_actions';
import UserActions from '../../actions/user_actions';
import Form from './form'; import Form from './form';
import Property from './property'; import Property from './property';
import InputCheckbox from './input_checkbox'; import InputCheckbox from './input_checkbox';
@ -24,8 +23,12 @@ let SignupForm = React.createClass({
headerMessage: React.PropTypes.string, headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string, submitMessage: React.PropTypes.string,
handleSuccess: React.PropTypes.func, handleSuccess: React.PropTypes.func,
children: React.PropTypes.element, location: React.PropTypes.object,
location: React.PropTypes.object children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element,
React.PropTypes.string
])
}, },
mixins: [History], mixins: [History],
@ -37,25 +40,9 @@ let SignupForm = React.createClass({
}; };
}, },
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
handleSuccess(response) { handleSuccess(response) {
if (response.user) { if (response.user) {
let notification = new GlobalNotificationModel(getLangText('Sign up successful'), 'success', 50000); const notification = new GlobalNotificationModel(getLangText('Sign up successful'), 'success', 50000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
// Refactor this to its own component // Refactor this to its own component
@ -66,18 +53,20 @@ let SignupForm = React.createClass({
}, },
getFormData() { getFormData() {
if (this.props.location.query.token){ const { token } = this.props.location.query;
return {token: this.props.location.query.token}; return token ? { token } : null;
}
return null;
}, },
render() { render() {
let tooltipPassword = getLangText('Your password must be at least 10 characters') + '.\n ' + const {
getLangText('This password is securing your digital property like a bank account') + '.\n ' + children,
getLangText('Store it in a safe place') + '!'; headerMessage,
location: { query: { email: emailQuery } },
submitMessage } = this.props;
let email = this.props.location.query.email || null; const tooltipPassword = getLangText('Your password must be at least 10 characters') + '.\n ' +
getLangText('This password is securing your digital property like a bank account') + '.\n ' +
getLangText('Store it in a safe place') + '!';
return ( return (
<Form <Form
@ -88,15 +77,16 @@ let SignupForm = React.createClass({
handleSuccess={this.handleSuccess} handleSuccess={this.handleSuccess}
buttons={ buttons={
<button type="submit" className="btn btn-default btn-wide"> <button type="submit" className="btn btn-default btn-wide">
{this.props.submitMessage} {submitMessage}
</button>} </button>
}
spinner={ spinner={
<span className="btn btn-default btn-wide btn-spinner"> <span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" /> <AscribeSpinner color="dark-blue" size="md" />
</span> </span>
}> }>
<div className="ascribe-form-header"> <div className="ascribe-form-header">
<h3>{this.props.headerMessage}</h3> <h3>{headerMessage}</h3>
</div> </div>
<Property <Property
name='email' name='email'
@ -105,7 +95,7 @@ let SignupForm = React.createClass({
type="email" type="email"
placeholder={getLangText('(e.g. andy@warhol.co.uk)')} placeholder={getLangText('(e.g. andy@warhol.co.uk)')}
autoComplete="on" autoComplete="on"
defaultValue={email} defaultValue={emailQuery}
required/> required/>
</Property> </Property>
<Property <Property
@ -128,11 +118,10 @@ let SignupForm = React.createClass({
autoComplete="on" autoComplete="on"
required/> required/>
</Property> </Property>
{this.props.children} {children}
<Property <Property
name="terms" name="terms"
className="ascribe-property-collapsible-toggle" className="ascribe-property-collapsible-toggle">
style={{paddingBottom: 0}}>
<InputCheckbox> <InputCheckbox>
<span> <span>
{' ' + getLangText('I agree to the Terms of Service of ascribe') + ' '} {' ' + getLangText('I agree to the Terms of Service of ascribe') + ' '}

View File

@ -66,8 +66,7 @@ let PieceSubmitToPrizeForm = React.createClass({
</Property> </Property>
<Property <Property
name="terms" name="terms"
className="ascribe-property-collapsible-toggle" className="ascribe-property-collapsible-toggle">
style={{paddingBottom: 0}}>
<InputCheckbox> <InputCheckbox>
<span> <span>
{' ' + getLangText('I agree to the Terms of Service the art price') + ' '} {' ' + getLangText('I agree to the Terms of Service the art price') + ' '}

View File

@ -18,26 +18,26 @@ import { getLangText } from '../../utils/lang_utils.js';
let TransferForm = React.createClass({ let TransferForm = React.createClass({
propTypes: { propTypes: {
url: React.PropTypes.string, id: React.PropTypes.object.isRequired,
id: React.PropTypes.object, url: React.PropTypes.string.isRequired,
message: React.PropTypes.string,
editions: React.PropTypes.array, handleSuccess: React.PropTypes.func,
currentUser: React.PropTypes.object, message: React.PropTypes.string
handleSuccess: React.PropTypes.func
}, },
getFormData(){ getFormData() {
return this.props.id; return this.props.id;
}, },
render() { render() {
const { handleSuccess, message, url } = this.props;
return ( return (
<Form <Form
ref='form' ref='form'
url={this.props.url} url={url}
getFormData={this.getFormData} getFormData={this.getFormData}
handleSuccess={this.props.handleSuccess} handleSuccess={handleSuccess}
buttons={ buttons={
<div className="modal-footer"> <div className="modal-footer">
<p className="pull-right"> <p className="pull-right">
@ -70,7 +70,7 @@ let TransferForm = React.createClass({
overrideForm={true}> overrideForm={true}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
defaultValue={this.props.message} defaultValue={message}
placeholder={getLangText('Enter a message...')} placeholder={getLangText('Enter a message...')}
required /> required />
</Property> </Property>

View File

@ -156,7 +156,7 @@ const InputContractAgreementCheckbox = React.createClass({
return ( return (
<div <div
className="notification-contract-pdf" className="notification-contract-pdf"
style={{paddingBottom: '1em'}}> style={{paddingBottom: '0.25em'}}>
<embed <embed
className="embed-form" className="embed-form"
src={contractUrl} src={contractUrl}

View File

@ -17,10 +17,7 @@ let InputDate = React.createClass({
}, },
getInitialState() { getInitialState() {
return { return this.getStateFromMoment(this.props.defaultValue);
value: null,
value_moment: null
};
}, },
// InputDate needs to support setting a defaultValue from outside. // InputDate needs to support setting a defaultValue from outside.
@ -28,20 +25,30 @@ let InputDate = React.createClass({
// to the outer Property // to the outer Property
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if(!this.state.value && !this.state.value_moment && nextProps.defaultValue) { if(!this.state.value && !this.state.value_moment && nextProps.defaultValue) {
this.handleChange(this.props.defaultValue); this.handleChange(nextProps.defaultValue);
} }
}, },
handleChange(date) { getStateFromMoment(date) {
let formattedDate = date.format('YYYY-MM-DD'); const state = {};
this.setState({
value: formattedDate,
value_moment: date
});
if (date) {
state.value = date.format('YYYY-MM-DD');
state.value_moment = date;
}
return state;
},
handleChange(date) {
const newState = this.getStateFromMoment(date);
this.setState(newState);
// Propagate change up by faking event
this.props.onChange({ this.props.onChange({
target: { target: {
value: formattedDate value: newState.value
} }
}); });
}, },

View File

@ -10,48 +10,35 @@ import AppConstants from '../../constants/application_constants';
import { getCookie } from '../../utils/fetch_api_utils'; import { getCookie } from '../../utils/fetch_api_utils';
const { func, bool, shape, string, number, arrayOf } = React.PropTypes; const { func, bool, shape, string, number, element, oneOf, oneOfType, arrayOf } = React.PropTypes;
const InputFineUploader = React.createClass({ const InputFineUploader = React.createClass({
propTypes: { propTypes: {
setIsUploadReady: func,
isReadyForFormSubmission: func,
submitFile: func,
fileInputElement: func,
areAssetsDownloadable: bool,
keyRoutine: shape({
url: string,
fileClass: string
}),
createBlobRoutine: shape({
url: string
}),
validation: ReactS3FineUploader.propTypes.validation,
// isFineUploaderActive is used to lock react fine uploader in case // isFineUploaderActive is used to lock react fine uploader in case
// a user is actually not logged in already to prevent him from droping files // a user is actually not logged in already to prevent him from droping files
// before login in // before login in
isFineUploaderActive: bool, isFineUploaderActive: bool,
enableLocalHashing: bool,
uploadMethod: string,
// provided by Property // provided by Property
disabled: bool, disabled: bool,
onChange: func,
// A class of a file the user has to upload // Props for ReactS3FineUploader
// Needs to be defined both in singular as well as in plural areAssetsDownloadable: bool,
fileClassToUpload: shape({ createBlobRoutine: ReactS3FineUploader.propTypes.createBlobRoutine,
singular: string, enableLocalHashing: bool,
plural: string fileClassToUpload: ReactS3FineUploader.propTypes.fileClassToUpload,
}), fileInputElement: ReactS3FineUploader.propTypes.fileInputElement,
handleChangedFile: func, isReadyForFormSubmission: func,
keyRoutine: ReactS3FineUploader.propTypes.keyRoutine,
handleChangedFile: func, // TODO: rename to onChangedFile
submitFile: func, // TODO: rename to onSubmitFile
onValidationFailed: func, onValidationFailed: func,
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
// Provided by `Property` setWarning: func,
onChange: React.PropTypes.func showErrorPrompt: bool,
uploadMethod: oneOf(['hash', 'upload']),
validation: ReactS3FineUploader.propTypes.validation,
}, },
getDefaultProps() { getDefaultProps() {
@ -96,19 +83,21 @@ const InputFineUploader = React.createClass({
render() { render() {
const { const {
areAssetsDownloadable, areAssetsDownloadable,
enableLocalHashing,
createBlobRoutine, createBlobRoutine,
enableLocalHashing,
disabled, disabled,
fileClassToUpload, fileClassToUpload,
fileInputElement, fileInputElement,
handleChangedFile,
isFineUploaderActive, isFineUploaderActive,
isReadyForFormSubmission, isReadyForFormSubmission,
keyRoutine, keyRoutine,
onValidationFailed, onValidationFailed,
setIsUploadReady, setIsUploadReady,
setWarning,
showErrorPrompt,
uploadMethod, uploadMethod,
validation, validation } = this.props;
handleChangedFile } = this.props;
let editable = isFineUploaderActive; let editable = isFineUploaderActive;
// if disabled is actually set by property, we want to override // if disabled is actually set by property, we want to override
@ -130,6 +119,8 @@ const InputFineUploader = React.createClass({
isReadyForFormSubmission={isReadyForFormSubmission} isReadyForFormSubmission={isReadyForFormSubmission}
areAssetsDownloadable={areAssetsDownloadable} areAssetsDownloadable={areAssetsDownloadable}
areAssetsEditable={editable} areAssetsEditable={editable}
setWarning={setWarning}
showErrorPrompt={showErrorPrompt}
signature={{ signature={{
endpoint: AppConstants.serverUrl + 's3/signature/', endpoint: AppConstants.serverUrl + 's3/signature/',
customHeaders: { customHeaders: {
@ -147,7 +138,7 @@ const InputFineUploader = React.createClass({
enableLocalHashing={enableLocalHashing} enableLocalHashing={enableLocalHashing}
uploadMethod={uploadMethod} uploadMethod={uploadMethod}
fileClassToUpload={fileClassToUpload} fileClassToUpload={fileClassToUpload}
handleChangedFile={handleChangedFile}/> handleChangedFile={handleChangedFile} />
); );
} }
}); });

View File

@ -4,32 +4,35 @@ import React from 'react';
import RequestActionForm from './form_request_action'; import RequestActionForm from './form_request_action';
let ListRequestActions = React.createClass({ let ListRequestActions = React.createClass({
propTypes: { propTypes: {
notifications: React.PropTypes.array.isRequired,
pieceOrEditions: React.PropTypes.oneOfType([ pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object, React.PropTypes.object,
React.PropTypes.array React.PropTypes.array
]).isRequired, ]).isRequired,
currentUser: React.PropTypes.object, currentUser: React.PropTypes.object,
handleSuccess: React.PropTypes.func.isRequired, handleSuccess: React.PropTypes.func
notifications: React.PropTypes.array.isRequired
}, },
render () { render () {
if (this.props.notifications && const { currentUser, handleSuccess, notifications, pieceOrEditions } = this.props;
this.props.notifications.length > 0) {
if (notifications.length) {
return ( return (
<div> <div>
{this.props.notifications.map((notification) => {notifications.map((notification) =>
<RequestActionForm <RequestActionForm
currentUser={this.props.currentUser} currentUser={currentUser}
pieceOrEditions={ this.props.pieceOrEditions } handleSuccess={handleSuccess}
notifications={notification} notifications={notification}
handleSuccess={this.props.handleSuccess}/>)} pieceOrEditions={pieceOrEditions} />
)}
</div> </div>
); );
} else {
return null;
} }
return null;
} }
}); });

View File

@ -72,7 +72,8 @@ const Property = React.createClass({
initialValue: null, initialValue: null,
value: null, value: null,
isFocused: false, isFocused: false,
errors: null errors: null,
hasWarning: false
}; };
}, },
@ -218,17 +219,20 @@ const Property = React.createClass({
this.setState({errors: null}); this.setState({errors: null});
}, },
setWarning(hasWarning) {
this.setState({ hasWarning });
},
getClassName() { getClassName() {
if(!this.state.expanded && !this.props.checkboxLabel){ if (!this.state.expanded && !this.props.checkboxLabel) {
return 'is-hidden'; return 'is-hidden';
} } else if (!this.props.editable) {
if(!this.props.editable){
return 'is-fixed'; return 'is-fixed';
} } else if (this.state.errors) {
if (this.state.errors){
return 'is-error'; return 'is-error';
} } else if (this.state.hasWarning) {
if(this.state.isFocused) { return 'is-warning';
} else if (this.state.isFocused) {
return 'is-focused'; return 'is-focused';
} else { } else {
return ''; return '';
@ -271,6 +275,7 @@ const Property = React.createClass({
onChange: this.handleChange, onChange: this.handleChange,
onFocus: this.handleFocus, onFocus: this.handleFocus,
onBlur: this.handleBlur, onBlur: this.handleBlur,
setWarning: this.setWarning,
disabled: !this.props.editable, disabled: !this.props.editable,
ref: 'input', ref: 'input',
name: this.props.name, name: this.props.name,
@ -294,18 +299,18 @@ const Property = React.createClass({
}, },
getCheckbox() { getCheckbox() {
const { checkboxLabel } = this.props; const { checkboxLabel, name } = this.props;
if(checkboxLabel) { if (checkboxLabel) {
return ( return (
<div <div
className="ascribe-property-collapsible-toggle" className="ascribe-property-collapsible-toggle"
onClick={this.handleCheckboxToggle}> onClick={this.handleCheckboxToggle}>
<input <input
onChange={this.handleCheckboxToggle} name={`${name}-checkbox`}
type="checkbox"
checked={this.state.expanded} checked={this.state.expanded}
ref="checkboxCollapsible"/> onChange={this.handleCheckboxToggle}
type="checkbox" />
<span className="checkbox">{' ' + checkboxLabel}</span> <span className="checkbox">{' ' + checkboxLabel}</span>
</div> </div>
); );

View File

@ -6,6 +6,10 @@ import Modal from 'react-bootstrap/lib/Modal';
let ModalWrapper = React.createClass({ let ModalWrapper = React.createClass({
propTypes: { propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]).isRequired,
title: React.PropTypes.oneOfType([ title: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element, React.PropTypes.element,
@ -14,11 +18,7 @@ let ModalWrapper = React.createClass({
handleCancel: React.PropTypes.func, handleCancel: React.PropTypes.func,
handleSuccess: React.PropTypes.func, handleSuccess: React.PropTypes.func,
trigger: React.PropTypes.element, trigger: React.PropTypes.element
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
])
}, },
getInitialState() { getInitialState() {

View File

@ -84,6 +84,7 @@ let PieceListToolbarFilterWidget = React.createClass({
if (this.props.filterParams && this.props.filterParams.length) { if (this.props.filterParams && this.props.filterParams.length) {
return ( return (
<DropdownButton <DropdownButton
id="ascribe-piece-list-toolbar-filter-widget-dropdown"
pullRight={true} pullRight={true}
title={filterIcon} title={filterIcon}
className="ascribe-piece-list-toolbar-widget"> className="ascribe-piece-list-toolbar-widget">

View File

@ -55,6 +55,7 @@ let PieceListToolbarOrderWidget = React.createClass({
if (this.props.orderParams && this.props.orderParams.length) { if (this.props.orderParams && this.props.orderParams.length) {
return ( return (
<DropdownButton <DropdownButton
id="ascribe-piece-list-toolbar-order-widget-dropdown"
pullRight={true} pullRight={true}
className="ascribe-piece-list-toolbar-widget" className="ascribe-piece-list-toolbar-widget"
title={orderIcon}> title={orderIcon}>

View File

@ -5,7 +5,6 @@ import { RouteContext } from 'react-router';
import history from '../../history'; import history from '../../history';
import UserStore from '../../stores/user_store'; import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
@ -18,11 +17,11 @@ const WHEN_ENUM = ['loggedIn', 'loggedOut'];
* *
* @param {enum/string} options.when ('loggedIn' || 'loggedOut') * @param {enum/string} options.when ('loggedIn' || 'loggedOut')
*/ */
export function AuthRedirect({to, when}) { export function AuthRedirect({ to, when }) {
// validate `when`, must be contained in `WHEN_ENUM`. // validate `when`, must be contained in `WHEN_ENUM`.
// Throw an error otherwise. // Throw an error otherwise.
if(WHEN_ENUM.indexOf(when) === -1) { if (WHEN_ENUM.indexOf(when) === -1) {
let whenValues = WHEN_ENUM.join(', '); const whenValues = WHEN_ENUM.join(', ');
throw new Error(`"when" must be one of: [${whenValues}] got "${when}" instead`); throw new Error(`"when" must be one of: [${whenValues}] got "${when}" instead`);
} }
@ -35,23 +34,22 @@ export function AuthRedirect({to, when}) {
// //
// So if when === 'loggedIn', we're checking if the user is logged in (and // So if when === 'loggedIn', we're checking if the user is logged in (and
// vice versa) // vice versa)
let exprToValidate = when === 'loggedIn' ? currentUser && currentUser.email const isLoggedIn = Object.keys(currentUser).length && currentUser.email;
: currentUser && !currentUser.email; const exprToValidate = when === 'loggedIn' ? isLoggedIn : !isLoggedIn;
// and redirect if `true`. // and redirect if `true`.
if(exprToValidate) { if (exprToValidate) {
window.setTimeout(() => history.replace({ query, pathname: to })); window.setTimeout(() => history.replace({ query, pathname: to }));
return true; return true;
// Otherwise there can also be the case that the backend // Otherwise there can also be the case that the backend
// wants to redirect the user to a specific route when the user is logged out already // wants to redirect the user to a specific route when the user is logged out already
} else if(!exprToValidate && when === 'loggedIn' && redirect) { } else if (!exprToValidate && when === 'loggedIn' && redirect) {
delete query.redirect; delete query.redirect;
window.setTimeout(() => history.replace({ query, pathname: '/' + redirect })); window.setTimeout(() => history.replace({ query, pathname: '/' + redirect }));
return true; return true;
} else if(!exprToValidate && when === 'loggedOut' && redirectAuthenticated) { } else if (!exprToValidate && when === 'loggedOut' && redirectAuthenticated) {
/* /*
* redirectAuthenticated contains an arbitrary path * redirectAuthenticated contains an arbitrary path
* eg pieces/<id>, editions/<bitcoin_id>, collection, settings, ... * eg pieces/<id>, editions/<bitcoin_id>, collection, settings, ...
@ -64,6 +62,7 @@ export function AuthRedirect({to, when}) {
window.location = AppConstants.baseUrl + redirectAuthenticated; window.location = AppConstants.baseUrl + redirectAuthenticated;
return true; return true;
} }
return false; return false;
}; };
} }
@ -81,6 +80,11 @@ export function ProxyHandler(...redirectFunctions) {
displayName: 'ProxyHandler', displayName: 'ProxyHandler',
propTypes: { propTypes: {
// Provided from AscribeApp, after the routes have been initialized
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: object location: object
}, },
@ -88,43 +92,33 @@ export function ProxyHandler(...redirectFunctions) {
// to use the `Lifecycle` widget in further down nested components // to use the `Lifecycle` widget in further down nested components
mixins: [RouteContext], mixins: [RouteContext],
getInitialState() {
return UserStore.getState();
},
componentDidMount() { componentDidMount() {
UserStore.listen(this.onChange); this.evaluateRedirectFunctions();
UserActions.fetchCurrentUser();
}, },
componentDidUpdate() { componentWillReceiveProps(nextProps) {
if(!UserStore.isLoading()) { this.evaluateRedirectFunctions(nextProps);
const { currentUser } = this.state; },
const { query } = this.props.location;
for(let i = 0; i < redirectFunctions.length; i++) { evaluateRedirectFunctions(props = this.props) {
const { currentUser, location: { query } } = props;
if (UserStore.hasLoaded() && !UserStore.isLoading()) {
for (let i = 0; i < redirectFunctions.length; i++) {
// if a redirectFunction redirects the user, // if a redirectFunction redirects the user,
// it should return `true` and therefore // it should return `true` and therefore
// stop/avoid the execution of all functions // stop/avoid the execution of all functions
// that follow // that follow
if(redirectFunctions[i](currentUser, query)) { if (redirectFunctions[i](currentUser, query)) {
break; break;
} }
} }
} }
}, },
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() { render() {
return ( return (
<Component {...this.props}/> <Component {...this.props} />
); );
} }
}); });

View File

@ -26,21 +26,23 @@ let AccountSettings = React.createClass({
whitelabel: React.PropTypes.object.isRequired whitelabel: React.PropTypes.object.isRequired
}, },
handleSuccess(){ handleSuccess() {
this.props.loadUser(true); this.props.loadUser(true);
let notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000);
const notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
getFormDataProfile(){ getFormDataProfile() {
return {'email': this.props.currentUser.email}; return { 'email': this.props.currentUser.email };
}, },
render() { render() {
let content = <AscribeSpinner color='dark-blue' size='lg'/>; const { currentUser, whitelabel } = this.props;
let content = <AscribeSpinner color='dark-blue' size='lg' />;
let profile = null; let profile = null;
if (this.props.currentUser.username) { if (currentUser.username) {
content = ( content = (
<Form <Form
url={ApiUrls.users_username} url={ApiUrls.users_username}
@ -50,7 +52,7 @@ let AccountSettings = React.createClass({
label={getLangText('Username')}> label={getLangText('Username')}>
<input <input
type="text" type="text"
defaultValue={this.props.currentUser.username} defaultValue={currentUser.username}
placeholder={getLangText('Enter your username')} placeholder={getLangText('Enter your username')}
required/> required/>
</Property> </Property>
@ -61,7 +63,7 @@ let AccountSettings = React.createClass({
editable={false}> editable={false}>
<input <input
type="text" type="text"
defaultValue={this.props.currentUser.email} defaultValue={currentUser.email}
placeholder={getLangText('Enter your username')} placeholder={getLangText('Enter your username')}
required/> required/>
</Property> </Property>
@ -70,7 +72,7 @@ let AccountSettings = React.createClass({
); );
profile = ( profile = (
<AclProxy <AclProxy
aclObject={this.props.whitelabel} aclObject={whitelabel}
aclName="acl_view_settings_account_hash"> aclName="acl_view_settings_account_hash">
<Form <Form
url={ApiUrls.users_profile} url={ApiUrls.users_profile}
@ -78,10 +80,9 @@ let AccountSettings = React.createClass({
getFormData={this.getFormDataProfile}> getFormData={this.getFormDataProfile}>
<Property <Property
name="hash_locally" name="hash_locally"
className="ascribe-property-collapsible-toggle" className="ascribe-property-collapsible-toggle">
style={{paddingBottom: 0}}>
<InputCheckbox <InputCheckbox
defaultChecked={this.props.currentUser.profile.hash_locally}> defaultChecked={currentUser.profile.hash_locally}>
<span> <span>
{' ' + getLangText('Enable hash option, e.g. slow connections or to keep piece private')} {' ' + getLangText('Enable hash option, e.g. slow connections or to keep piece private')}
</span> </span>
@ -97,9 +98,9 @@ let AccountSettings = React.createClass({
defaultExpanded={true}> defaultExpanded={true}>
{content} {content}
<AclProxy <AclProxy
aclObject={this.props.whitelabel} aclObject={whitelabel}
aclName="acl_view_settings_copyright_association"> aclName="acl_view_settings_copyright_association">
<CopyrightAssociationForm currentUser={this.props.currentUser}/> <CopyrightAssociationForm currentUser={currentUser} />
</AclProxy> </AclProxy>
{profile} {profile}
</CollapsibleParagraph> </CollapsibleParagraph>

View File

@ -8,12 +8,6 @@ import CreateContractForm from '../ascribe_forms/form_create_contract';
import ContractListStore from '../../stores/contract_list_store'; import ContractListStore from '../../stores/contract_list_store';
import ContractListActions from '../../actions/contract_list_actions'; import ContractListActions from '../../actions/contract_list_actions';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
import WhitelabelStore from '../../stores/whitelabel_store';
import WhitelabelActions from '../../actions/whitelabel_actions';
import ActionPanel from '../ascribe_panel/action_panel'; import ActionPanel from '../ascribe_panel/action_panel';
import ContractSettingsUpdateButton from './contract_settings_update_button'; import ContractSettingsUpdateButton from './contract_settings_update_button';
@ -24,30 +18,29 @@ import AclProxy from '../acl_proxy';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils'; import { setDocumentTitle } from '../../utils/dom_utils';
import { mergeOptions, truncateTextAtCharIndex } from '../../utils/general_utils'; import { truncateTextAtCharIndex } from '../../utils/general_utils';
let ContractSettings = React.createClass({ let ContractSettings = React.createClass({
propTypes: {
// Provided from AscribeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object.isRequired,
// Provided from router
location: React.PropTypes.object
},
getInitialState() { getInitialState() {
return mergeOptions( return ContractListStore.getState();
ContractListStore.getState(),
UserStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
ContractListStore.listen(this.onChange); ContractListStore.listen(this.onChange);
UserStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
UserActions.fetchCurrentUser();
ContractListActions.fetchContractList(true); ContractListActions.fetchContractList(true);
}, },
componentWillUnmount() { componentWillUnmount() {
WhitelabelStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
ContractListStore.unlisten(this.onChange); ContractListStore.unlisten(this.onChange);
}, },
@ -79,6 +72,7 @@ let ContractSettings = React.createClass({
}, },
render() { render() {
const { currentUser, location, whitelabel } = this.props;
const publicContracts = this.getPublicContracts(); const publicContracts = this.getPublicContracts();
const privateContracts = this.getPrivateContracts(); const privateContracts = this.getPrivateContracts();
let createPublicContractForm = null; let createPublicContractForm = null;
@ -88,11 +82,11 @@ let ContractSettings = React.createClass({
if (publicContracts.length === 0) { if (publicContracts.length === 0) {
createPublicContractForm = ( createPublicContractForm = (
<CreateContractForm <CreateContractForm
isPublic={true}
fileClassToUpload={{ fileClassToUpload={{
singular: 'new contract', singular: 'new contract',
plural: 'new contracts' plural: 'new contracts'
}} /> }}
isPublic={true} />
); );
} }
@ -103,7 +97,7 @@ let ContractSettings = React.createClass({
defaultExpanded={true}> defaultExpanded={true}>
<AclProxy <AclProxy
aclName="acl_edit_public_contract" aclName="acl_edit_public_contract"
aclObject={this.state.currentUser.acl}> aclObject={currentUser.acl}>
<div> <div>
{createPublicContractForm} {createPublicContractForm}
{publicContracts.map((contract, i) => { {publicContracts.map((contract, i) => {
@ -115,10 +109,9 @@ let ContractSettings = React.createClass({
buttons={ buttons={
<div className="pull-right"> <div className="pull-right">
<AclProxy <AclProxy
aclObject={this.state.whitelabel} aclObject={whitelabel}
aclName="acl_update_public_contract"> aclName="acl_update_public_contract">
<ContractSettingsUpdateButton <ContractSettingsUpdateButton contract={contract} />
contract={contract} />
</AclProxy> </AclProxy>
<a <a
className="btn btn-default btn-sm margin-left-2px" className="btn btn-default btn-sm margin-left-2px"
@ -141,14 +134,14 @@ let ContractSettings = React.createClass({
</AclProxy> </AclProxy>
<AclProxy <AclProxy
aclName="acl_edit_private_contract" aclName="acl_edit_private_contract"
aclObject={this.state.currentUser.acl}> aclObject={currentUser.acl}>
<div> <div>
<CreateContractForm <CreateContractForm
isPublic={false} fileClassToUpload={{
fileClassToUpload={{ singular: getLangText('new contract'),
singular: getLangText('new contract'), plural: getLangText('new contracts')
plural: getLangText('new contracts') }}
}} /> isPublic={false} />
{privateContracts.map((contract, i) => { {privateContracts.map((contract, i) => {
return ( return (
<ActionPanel <ActionPanel
@ -158,10 +151,9 @@ let ContractSettings = React.createClass({
buttons={ buttons={
<div className="pull-right"> <div className="pull-right">
<AclProxy <AclProxy
aclObject={this.state.whitelabel} aclObject={whitelabel}
aclName="acl_update_private_contract"> aclName="acl_update_private_contract">
<ContractSettingsUpdateButton <ContractSettingsUpdateButton contract={contract} />
contract={contract} />
</AclProxy> </AclProxy>
<a <a
className="btn btn-default btn-sm margin-left-2px" className="btn btn-default btn-sm margin-left-2px"

View File

@ -2,12 +2,8 @@
import React from 'react'; import React from 'react';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions'; import UserActions from '../../actions/user_actions';
import WhitelabelStore from '../../stores/whitelabel_store';
import WhitelabelActions from '../../actions/whitelabel_actions';
import AccountSettings from './account_settings'; import AccountSettings from './account_settings';
import BitcoinWalletSettings from './bitcoin_wallet_settings'; import BitcoinWalletSettings from './bitcoin_wallet_settings';
import APISettings from './api_settings'; import APISettings from './api_settings';
@ -24,56 +20,42 @@ let SettingsContainer = React.createClass({
propTypes: { propTypes: {
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element]) React.PropTypes.element
]),
// Provided from AscribeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object.isRequired,
// Provided from router
location: React.PropTypes.object
}, },
getInitialState() { loadUser(invalidateCache) {
return mergeOptions(
UserStore.getState(),
WhitelabelStore.getState()
);
},
componentDidMount() {
UserStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
WhitelabelStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
},
loadUser(invalidateCache){
UserActions.fetchCurrentUser(invalidateCache); UserActions.fetchCurrentUser(invalidateCache);
}, },
onChange(state) {
this.setState(state);
},
render() { render() {
const { children, currentUser, whitelabel } = this.props;
setDocumentTitle(getLangText('Account settings')); setDocumentTitle(getLangText('Account settings'));
if (this.state.currentUser && this.state.currentUser.username) { if (currentUser.username) {
return ( return (
<div className="settings-container"> <div className="settings-container">
<AccountSettings <AccountSettings
currentUser={this.state.currentUser} currentUser={currentUser}
loadUser={this.loadUser} loadUser={this.loadUser}
whitelabel={this.state.whitelabel}/> whitelabel={whitelabel} />
{this.props.children} {children}
<AclProxy <AclProxy
aclObject={this.state.whitelabel} aclObject={whitelabel}
aclName="acl_view_settings_api"> aclName="acl_view_settings_api">
<APISettings /> <APISettings />
</AclProxy> </AclProxy>
<WebhookSettings /> <WebhookSettings />
<AclProxy <AclProxy
aclObject={this.state.whitelabel} aclObject={whitelabel}
aclName="acl_view_settings_bitcoin"> aclName="acl_view_settings_bitcoin">
<BitcoinWalletSettings /> <BitcoinWalletSettings />
</AclProxy> </AclProxy>

View File

@ -1,29 +1,33 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import ProgressBar from 'react-bootstrap/lib/ProgressBar'; import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import FileDragAndDropDialog from './file_drag_and_drop_dialog'; import FileDragAndDropDialog from './file_drag_and_drop_dialog';
import FileDragAndDropErrorDialog from './file_drag_and_drop_error_dialog';
import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator'; import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator';
import { FileStatus } from '../react_s3_fine_uploader_utils';
import { getLangText } from '../../../utils/lang_utils'; import { getLangText } from '../../../utils/lang_utils';
// Taken from: https://github.com/fedosejev/react-file-drag-and-drop // Taken from: https://github.com/fedosejev/react-file-drag-and-drop
let FileDragAndDrop = React.createClass({ let FileDragAndDrop = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool,
multiple: React.PropTypes.bool,
dropzoneInactive: React.PropTypes.bool,
filesToUpload: React.PropTypes.array,
onDrop: React.PropTypes.func.isRequired, onDrop: React.PropTypes.func.isRequired,
onDragOver: React.PropTypes.func, onDragOver: React.PropTypes.func,
filesToUpload: React.PropTypes.array,
handleDeleteFile: React.PropTypes.func, handleDeleteFile: React.PropTypes.func,
handleCancelFile: React.PropTypes.func, handleCancelFile: React.PropTypes.func,
handlePauseFile: React.PropTypes.func, handlePauseFile: React.PropTypes.func,
handleResumeFile: React.PropTypes.func, handleResumeFile: React.PropTypes.func,
multiple: React.PropTypes.bool, handleRetryFiles: React.PropTypes.func,
dropzoneInactive: React.PropTypes.bool,
areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
uploadMethod: React.PropTypes.string, uploadMethod: React.PropTypes.string,
@ -34,6 +38,12 @@ let FileDragAndDrop = React.createClass({
// to -1 which is code for: aborted // to -1 which is code for: aborted
handleCancelHashing: React.PropTypes.func, handleCancelHashing: React.PropTypes.func,
showError: React.PropTypes.bool,
errorClass: React.PropTypes.shape({
name: React.PropTypes.string,
prettifiedText: React.PropTypes.string
}),
// A class of a file the user has to upload // A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural // Needs to be defined both in singular as well as in plural
fileClassToUpload: React.PropTypes.shape({ fileClassToUpload: React.PropTypes.shape({
@ -126,65 +136,93 @@ let FileDragAndDrop = React.createClass({
} }
}, },
getErrorDialog(failedFiles) {
const { errorClass } = this.props;
return (
<FileDragAndDropErrorDialog
errorClass={errorClass}
files={failedFiles}
handleRetryFiles={this.props.handleRetryFiles} />
);
},
getPreviewIterator() {
const { areAssetsDownloadable, areAssetsEditable, filesToUpload } = this.props;
return (
<FileDragAndDropPreviewIterator
files={filesToUpload}
handleDeleteFile={this.handleDeleteFile}
handleCancelFile={this.handleCancelFile}
handlePauseFile={this.handlePauseFile}
handleResumeFile={this.handleResumeFile}
areAssetsDownloadable={areAssetsDownloadable}
areAssetsEditable={areAssetsEditable}/>
);
},
getUploadDialog() {
const { enableLocalHashing, fileClassToUpload, multiple, uploadMethod } = this.props;
return (
<FileDragAndDropDialog
multipleFiles={multiple}
onClick={this.handleOnClick}
enableLocalHashing={enableLocalHashing}
uploadMethod={uploadMethod}
fileClassToUpload={fileClassToUpload} />
);
},
render: function () { render: function () {
const { const {
filesToUpload, filesToUpload,
dropzoneInactive, dropzoneInactive,
className,
hashingProgress, hashingProgress,
handleCancelHashing, handleCancelHashing,
multiple, multiple,
enableLocalHashing, showError,
uploadMethod, errorClass,
fileClassToUpload, fileClassToUpload,
areAssetsDownloadable,
areAssetsEditable,
allowedExtensions } = this.props; allowedExtensions } = this.props;
// has files only is true if there are files that do not have the status deleted or canceled // has files only is true if there are files that do not have the status deleted, canceled, or failed
let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0; const hasFiles = filesToUpload
let updatedClassName = hasFiles ? 'has-files ' : ''; .filter((file) => {
updatedClassName += dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone'; return file.status !== FileStatus.DELETED &&
updatedClassName += ' file-drag-and-drop'; file.status !== FileStatus.CANCELED &&
file.status !== FileStatus.UPLOAD_FAILED &&
file.size !== -1;
})
.length > 0;
const failedFiles = filesToUpload.filter((file) => file.status === FileStatus.UPLOAD_FAILED);
let hasError = showError && errorClass && failedFiles.length > 0;
// if !== -2: triggers a FileDragAndDrop-global spinner // if !== -2: triggers a FileDragAndDrop-global spinner
if(hashingProgress !== -2) { if (hashingProgress !== -2) {
return ( return (
<div className={className}> <div className="file-drag-and-drop-hashing-dialog">
<div className="file-drag-and-drop-hashing-dialog"> <p>{getLangText('Computing hash(es)... This may take a few minutes.')}</p>
<p>{getLangText('Computing hash(es)... This may take a few minutes.')}</p> <p>
<p> <a onClick={handleCancelHashing}> {getLangText('Cancel hashing')}</a>
<a onClick={handleCancelHashing}> {getLangText('Cancel hashing')}</a> </p>
</p> <ProgressBar
<ProgressBar now={Math.ceil(hashingProgress)}
now={Math.ceil(hashingProgress)} label="%(percent)s%"
label="%(percent)s%" className="ascribe-progress-bar"/>
className="ascribe-progress-bar"/>
</div>
</div> </div>
); );
} else { } else {
return ( return (
<div <div
className={updatedClassName} className={classNames('file-drag-and-drop', dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone', { 'has-files': hasFiles })}
onDrag={this.handleDrop} onDrag={this.handleDrop}
onDragOver={this.handleDragOver} onDragOver={this.handleDragOver}
onDrop={this.handleDrop}> onDrop={this.handleDrop}>
<FileDragAndDropDialog {hasError ? this.getErrorDialog(failedFiles) : this.getPreviewIterator()}
multipleFiles={multiple} {!hasFiles && !hasError ? this.getUploadDialog() : null}
hasFiles={hasFiles}
onClick={this.handleOnClick}
enableLocalHashing={enableLocalHashing}
uploadMethod={uploadMethod}
fileClassToUpload={fileClassToUpload} />
<FileDragAndDropPreviewIterator
files={filesToUpload}
handleDeleteFile={this.handleDeleteFile}
handleCancelFile={this.handleCancelFile}
handlePauseFile={this.handlePauseFile}
handleResumeFile={this.handleResumeFile}
areAssetsDownloadable={areAssetsDownloadable}
areAssetsEditable={areAssetsEditable}/>
{/* {/*
Opera doesn't trigger simulated click events Opera doesn't trigger simulated click events
if the targeted input has `display:none` set. if the targeted input has `display:none` set.

View File

@ -9,7 +9,6 @@ import { getCurrentQueryParams } from '../../../utils/url_utils';
let FileDragAndDropDialog = React.createClass({ let FileDragAndDropDialog = React.createClass({
propTypes: { propTypes: {
hasFiles: React.PropTypes.bool,
multipleFiles: React.PropTypes.bool, multipleFiles: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
uploadMethod: React.PropTypes.string, uploadMethod: React.PropTypes.string,
@ -36,92 +35,88 @@ let FileDragAndDropDialog = React.createClass({
render() { render() {
const { const {
hasFiles,
multipleFiles, multipleFiles,
enableLocalHashing, enableLocalHashing,
uploadMethod, uploadMethod,
fileClassToUpload, fileClassToUpload,
onClick } = this.props; onClick } = this.props;
let dialogElement;
if (hasFiles) { if (enableLocalHashing && !uploadMethod) {
return null; const currentQueryParams = getCurrentQueryParams();
} else {
let dialogElement;
if (enableLocalHashing && !uploadMethod) { const queryParamsHash = Object.assign({}, currentQueryParams);
const currentQueryParams = getCurrentQueryParams(); queryParamsHash.method = 'hash';
const queryParamsHash = Object.assign({}, currentQueryParams); const queryParamsUpload = Object.assign({}, currentQueryParams);
queryParamsHash.method = 'hash'; queryParamsUpload.method = 'upload';
const queryParamsUpload = Object.assign({}, currentQueryParams); dialogElement = (
queryParamsUpload.method = 'upload'; <div className="present-options">
<p className="file-drag-and-drop-dialog-title">{getLangText('Would you rather')}</p>
dialogElement = ( {/*
<div className="present-options"> The frontend in live is hosted under /app,
<p className="file-drag-and-drop-dialog-title">{getLangText('Would you rather')}</p> Since `Link` is appending that base url, if its defined
{/* by itself, we need to make sure to not set it at this point.
The frontend in live is hosted under /app, Otherwise it will be appended twice.
Since `Link` is appending that base url, if its defined */}
by itself, we need to make sure to not set it at this point. <Link
Otherwise it will be appended twice. to={`/${window.location.pathname.split('/').pop()}`}
*/} query={queryParamsHash}>
<Link <span className="btn btn-default btn-sm">
to={`/${window.location.pathname.split('/').pop()}`} {getLangText('Hash your work')}
query={queryParamsHash}>
<span className="btn btn-default btn-sm">
{getLangText('Hash your work')}
</span>
</Link>
<span> or </span>
<Link
to={`/${window.location.pathname.split('/').pop()}`}
query={queryParamsUpload}>
<span className="btn btn-default btn-sm">
{getLangText('Upload and hash your work')}
</span>
</Link>
</div>
);
} else {
if (multipleFiles) {
dialogElement = [
this.getDragDialog(fileClassToUpload.plural),
<span
className="btn btn-default"
onClick={onClick}>
{getLangText('choose %s to upload', fileClassToUpload.plural)}
</span> </span>
]; </Link>
} else {
const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular)
: getLangText('choose a %s to upload', fileClassToUpload.singular);
dialogElement = [ <span> {getLangText('or')} </span>
this.getDragDialog(fileClassToUpload.singular),
<span <Link
className="btn btn-default" to={`/${window.location.pathname.split('/').pop()}`}
onClick={onClick}> query={queryParamsUpload}>
{dialog} <span className="btn btn-default btn-sm">
{getLangText('Upload and hash your work')}
</span> </span>
]; </Link>
}
}
return (
<div className="file-drag-and-drop-dialog">
<div className="hidden-print">
{dialogElement}
</div>
{/* Hide the uploader and just show that there's been on files uploaded yet when printing */}
<p className="text-align-center visible-print">
{getLangText('No files uploaded')}
</p>
</div> </div>
); );
} else {
if (multipleFiles) {
dialogElement = [
this.getDragDialog(fileClassToUpload.plural),
(<span
key='mutlipleFilesBtn'
className="btn btn-default"
onClick={onClick}>
{getLangText('choose %s to upload', fileClassToUpload.plural)}
</span>)
];
} else {
const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular)
: getLangText('choose a %s to upload', fileClassToUpload.singular);
dialogElement = [
this.getDragDialog(fileClassToUpload.singular),
(<span
key='singleFileBtn'
className="btn btn-default"
onClick={onClick}>
{dialog}
</span>)
];
}
} }
return (
<div className="file-drag-and-drop-dialog">
<div className="hidden-print">
{dialogElement}
</div>
{/* Hide the uploader and just show that there's been on files uploaded yet when printing */}
<p className="text-align-center visible-print">
{getLangText('No files uploaded')}
</p>
</div>
);
} }
}); });

View File

@ -0,0 +1,86 @@
'use strict';
import React from 'react';
import classNames from 'classnames';
import { ErrorClasses } from '../../../constants/error_constants';
import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropErrorDialog = React.createClass({
propTypes: {
errorClass: React.PropTypes.shape({
name: React.PropTypes.string,
prettifiedText: React.PropTypes.string
}).isRequired,
files: React.PropTypes.array.isRequired,
handleRetryFiles: React.PropTypes.func.isRequired
},
getRetryButton(text, openIntercom) {
return (
<button
type="button"
className='btn btn-default'
onClick={() => {
if (openIntercom) {
window.Intercom('showNewMessage', getLangText("I'm having trouble uploading my file."));
}
this.retryAllFiles()
}}>
{getLangText(text)}
</button>
);
},
getContactUsDetail() {
return (
<div className='file-drag-and-drop-error'>
<h4>{getLangText('Let us help you')}</h4>
<p>{getLangText('Still having problems? Send us a message.')}</p>
{this.getRetryButton('Contact us', true)}
</div>
);
},
getErrorDetail(multipleFiles) {
const { errorClass: { prettifiedText }, files } = this.props;
return (
<div className='file-drag-and-drop-error'>
<div className={classNames('file-drag-and-drop-error-detail', { 'file-drag-and-drop-error-detail-multiple-files': multipleFiles })}>
<h4>{getLangText(multipleFiles ? 'Some files did not upload correctly'
: 'Error uploading the file!')}
</h4>
<p>{prettifiedText}</p>
{this.getRetryButton('Retry')}
</div>
<span className={classNames('file-drag-and-drop-error-icon-container', { 'file-drag-and-drop-error-icon-container-multiple-files': multipleFiles })}>
<span className='ascribe-icon icon-ascribe-thin-cross'></span>
</span>
<div className='file-drag-and-drop-error-file-names'>
<ul>
{files.map((file) => (<li key={file.id} className='file-name'>{file.originalName}</li>))}
</ul>
</div>
</div>
);
},
retryAllFiles() {
const { files, handleRetryFiles } = this.props;
handleRetryFiles(files.map(file => file.id));
},
render() {
const { errorClass: { name: errorName }, files } = this.props;
const multipleFiles = files.length > 1;
const contactUs = errorName === ErrorClasses.upload.contactUs.name;
return contactUs ? this.getContactUsDetail() : this.getErrorDetail(multipleFiles);
}
});
export default FileDragAndDropErrorDialog;

View File

@ -5,6 +5,7 @@ import React from 'react';
import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image'; import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image';
import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other'; import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other';
import { FileStatus } from '../react_s3_fine_uploader_utils';
import { getLangText } from '../../../utils/lang_utils'; import { getLangText } from '../../../utils/lang_utils';
import { truncateTextAtCharIndex } from '../../../utils/general_utils'; import { truncateTextAtCharIndex } from '../../../utils/general_utils';
import { extractFileExtensionFromString } from '../../../utils/file_utils'; import { extractFileExtensionFromString } from '../../../utils/file_utils';
@ -23,27 +24,30 @@ const FileDragAndDropPreview = React.createClass({
s3Url: string, s3Url: string,
s3UrlSafe: string s3UrlSafe: string
}).isRequired, }).isRequired,
areAssetsDownloadable: bool,
areAssetsEditable: bool,
handleDeleteFile: func, handleDeleteFile: func,
handleCancelFile: func, handleCancelFile: func,
handlePauseFile: func, handlePauseFile: func,
handleResumeFile: func, handleResumeFile: func,
areAssetsDownloadable: bool,
areAssetsEditable: bool,
numberOfDisplayedFiles: number numberOfDisplayedFiles: number
}, },
toggleUploadProcess() { toggleUploadProcess() {
if(this.props.file.status === 'uploading') { const { file, handlePauseFile, handleResumeFile } = this.props;
this.props.handlePauseFile(this.props.file.id);
} else if(this.props.file.status === 'paused') { if (file.status === FileStatus.UPLOADING) {
this.props.handleResumeFile(this.props.file.id); handlePauseFile(file.id);
} else if (file.status === FileStatus.PAUSED) {
handleResumeFile(file.id);
} }
}, },
handleDeleteFile() { handleDeleteFile() {
const { handleDeleteFile, const { file,
handleCancelFile, handleDeleteFile,
file } = this.props; handleCancelFile } = this.props;
// `handleDeleteFile` is optional, so if its not submitted, don't run it // `handleDeleteFile` is optional, so if its not submitted, don't run it
// //
// For delete though, we only want to trigger it, when we're sure that // For delete though, we only want to trigger it, when we're sure that
@ -51,16 +55,16 @@ const FileDragAndDropPreview = React.createClass({
// deleted using an HTTP DELETE request. // deleted using an HTTP DELETE request.
if (handleDeleteFile && if (handleDeleteFile &&
file.progress === 100 && file.progress === 100 &&
(file.status === 'upload successful' || file.status === 'online') && (file.status === FileStatus.UPLOAD_SUCCESSFUL || file.status === FileStatus.ONLINE) &&
file.s3UrlSafe) { file.s3UrlSafe) {
handleDeleteFile(file.id); handleDeleteFile(file.id);
} else if(handleCancelFile) { } else if (handleCancelFile) {
handleCancelFile(file.id); handleCancelFile(file.id);
} }
}, },
handleDownloadFile() { handleDownloadFile() {
if(this.props.file.s3Url) { if (this.props.file.s3Url) {
// This simply opens a new browser tab with the url provided // This simply opens a new browser tab with the url provided
open(this.props.file.s3Url); open(this.props.file.s3Url);
} }
@ -69,7 +73,7 @@ const FileDragAndDropPreview = React.createClass({
getFileName() { getFileName() {
const { numberOfDisplayedFiles, file } = this.props; const { numberOfDisplayedFiles, file } = this.props;
if(numberOfDisplayedFiles === 1) { if (numberOfDisplayedFiles === 1) {
return ( return (
<span className="file-name"> <span className="file-name">
{truncateTextAtCharIndex(file.name, 30, '(...).' + extractFileExtensionFromString(file.name))} {truncateTextAtCharIndex(file.name, 30, '(...).' + extractFileExtensionFromString(file.name))}
@ -81,7 +85,7 @@ const FileDragAndDropPreview = React.createClass({
}, },
getRemoveButton() { getRemoveButton() {
if(this.props.areAssetsEditable) { if (this.props.areAssetsEditable) {
return ( return (
<div className="delete-file"> <div className="delete-file">
<span <span
@ -107,7 +111,7 @@ const FileDragAndDropPreview = React.createClass({
// Decide whether an image or a placeholder picture should be displayed // Decide whether an image or a placeholder picture should be displayed
// If a file has its `thumbnailUrl` defined, then we display it also as an image // If a file has its `thumbnailUrl` defined, then we display it also as an image
if(file.type.split('/')[0] === 'image' || file.thumbnailUrl) { if (file.type.split('/')[0] === 'image' || file.thumbnailUrl) {
previewElement = ( previewElement = (
<FileDragAndDropPreviewImage <FileDragAndDropPreviewImage
onClick={this.handleDeleteFile} onClick={this.handleDeleteFile}
@ -123,7 +127,7 @@ const FileDragAndDropPreview = React.createClass({
<FileDragAndDropPreviewOther <FileDragAndDropPreviewOther
onClick={this.handleDeleteFile} onClick={this.handleDeleteFile}
progress={file.progress} progress={file.progress}
type={file.type.split('/')[1]} type={extractFileExtensionFromString(file.name)}
toggleUploadProcess={this.toggleUploadProcess} toggleUploadProcess={this.toggleUploadProcess}
areAssetsDownloadable={areAssetsDownloadable} areAssetsDownloadable={areAssetsDownloadable}
downloadUrl={file.s3UrlSafe} downloadUrl={file.s3UrlSafe}

View File

@ -56,9 +56,9 @@ const FileDragAndDropPreviewOther = React.createClass({
target="_blank" target="_blank"
className="glyphicon glyphicon-download action-file" className="glyphicon glyphicon-download action-file"
aria-hidden="true" aria-hidden="true"
title={getLangText('Download file')}/> title={getLangText('Download file')} />
); );
} else if(progress >= 0 && progress < 100) { } else if (progress >= 0 && progress < 100) {
actionSymbol = ( actionSymbol = (
<div className="spinner-file"> <div className="spinner-file">
<AscribeSpinner color='dark-blue' size='md' /> <AscribeSpinner color='dark-blue' size='md' />
@ -66,22 +66,19 @@ const FileDragAndDropPreviewOther = React.createClass({
); );
} else { } else {
actionSymbol = ( actionSymbol = (
<span className='ascribe-icon icon-ascribe-ok action-file'/> <span className='ascribe-icon icon-ascribe-ok action-file' />
); );
} }
return ( return (
<div <div className="file-drag-and-drop-preview">
className="file-drag-and-drop-preview">
<ProgressBar <ProgressBar
now={Math.ceil(progress)} now={Math.ceil(progress)}
style={style} style={style}
className="ascribe-progress-bar ascribe-progress-bar-xs"/> className="ascribe-progress-bar ascribe-progress-bar-xs" />
<div className="file-drag-and-drop-preview-table-wrapper"> <div className="file-drag-and-drop-preview-other">
<div className="file-drag-and-drop-preview-other"> {actionSymbol}
{actionSymbol} <p style={style}>{'.' + (type ? type : 'file')}</p>
<p style={style}>{'.' + type}</p>
</div>
</div> </div>
</div> </div>
); );

View File

@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils'; import { displayValidProgressFilesFilter, FileStatus } from '../react_s3_fine_uploader_utils';
import { getLangText } from '../../../utils/lang_utils'; import { getLangText } from '../../../utils/lang_utils';
import { truncateTextAtCharIndex } from '../../../utils/general_utils'; import { truncateTextAtCharIndex } from '../../../utils/general_utils';
@ -58,11 +58,11 @@ export default function UploadButton({ className = 'btn btn-default btn-sm', sho
}, },
getUploadingFiles(filesToUpload = this.props.filesToUpload) { getUploadingFiles(filesToUpload = this.props.filesToUpload) {
return filesToUpload.filter((file) => file.status === 'uploading'); return filesToUpload.filter((file) => file.status === FileStatus.UPLOADING);
}, },
getUploadedFile() { getUploadedFile() {
return this.props.filesToUpload.filter((file) => file.status === 'upload successful')[0]; return this.props.filesToUpload.filter((file) => file.status === FileStatus.UPLOAD_SUCCESSFUL)[0];
}, },
clearSelection() { clearSelection() {

View File

@ -8,12 +8,16 @@ import S3Fetcher from '../../fetchers/s3_fetcher';
import FileDragAndDrop from './ascribe_file_drag_and_drop/file_drag_and_drop'; import FileDragAndDrop from './ascribe_file_drag_and_drop/file_drag_and_drop';
import ErrorQueueStore from '../../stores/error_queue_store';
import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions'; import GlobalNotificationActions from '../../actions/global_notification_actions';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { ErrorClasses, testErrorAgainstAll } from '../../constants/error_constants';
import { RETRY_ATTEMPT_TO_SHOW_CONTACT_US } from '../../constants/uploader_constants';
import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils'; import { displayValidFilesFilter, FileStatus, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils';
import { getCookie } from '../../utils/fetch_api_utils'; import { getCookie } from '../../utils/fetch_api_utils';
import { computeHashOfFile, extractFileExtensionFromString } from '../../utils/file_utils'; import { computeHashOfFile, extractFileExtensionFromString } from '../../utils/file_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
@ -33,80 +37,18 @@ const { shape,
const ReactS3FineUploader = React.createClass({ const ReactS3FineUploader = React.createClass({
propTypes: { propTypes: {
keyRoutine: shape({
url: string,
fileClass: string,
pieceId: number
}),
createBlobRoutine: shape({
url: string,
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({
acl: string
}),
request: shape({
endpoint: string,
accessKey: string,
params: shape({
csrfmiddlewaretoken: string
})
}),
signature: shape({
endpoint: string
}).isRequired,
uploadSuccess: shape({
method: string,
endpoint: string,
params: shape({
isBrowserPreviewCapable: any, // maybe fix this later
bitcoin_ID_noPrefix: string
})
}),
cors: shape({
expected: bool
}),
chunking: shape({
enabled: bool
}),
resume: shape({
enabled: bool
}),
deleteFile: shape({
enabled: bool,
method: string,
endpoint: string,
customHeaders: object
}).isRequired,
session: shape({
customHeaders: object,
endpoint: string,
params: object,
refreshOnRequests: bool
}),
validation: shape({
itemLimit: number,
sizeLimit: number,
allowedExtensions: arrayOf(string)
}),
messages: shape({
unsupportedBrowser: string
}),
formatFileName: func,
multiple: bool,
retry: shape({
enableAuto: bool
}),
setIsUploadReady: func,
isReadyForFormSubmission: func,
areAssetsDownloadable: bool, areAssetsDownloadable: bool,
areAssetsEditable: bool, areAssetsEditable: bool,
defaultErrorMessage: string, errorNotificationMessage: string,
handleChangedFile: func, // for when a file is dropped or selected, TODO: rename to onChangedFile
submitFile: func, // for when a file has been successfully uploaded, TODO: rename to onSubmitFile
onValidationFailed: func,
setWarning: func, // for when the parent component wants to be notified of uploader warnings (ie. upload failed)
showErrorPrompt: bool,
// Handle form validation
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
isReadyForFormSubmission: func,
// We encountered some cases where people had difficulties to upload their // We encountered some cases where people had difficulties to upload their
// works to ascribe due to a slow internet connection. // works to ascribe due to a slow internet connection.
@ -135,13 +77,94 @@ const ReactS3FineUploader = React.createClass({
fileInputElement: oneOfType([ fileInputElement: oneOfType([
func, func,
element element
]) ]),
// S3 helpers
createBlobRoutine: shape({
url: string,
pieceId: number
}),
keyRoutine: shape({
url: string,
fileClass: string,
pieceId: number
}),
// FineUploader options
debug: bool,
autoUpload: bool,
chunking: shape({
enabled: bool
}),
cors: shape({
expected: bool
}),
deleteFile: shape({
enabled: bool,
method: string,
endpoint: string,
customHeaders: object
}).isRequired,
formatFileName: func,
messages: shape({
unsupportedBrowser: string
}),
multiple: bool,
objectProperties: shape({
acl: string
}),
request: shape({
endpoint: string,
accessKey: string,
params: shape({
csrfmiddlewaretoken: string
})
}),
resume: shape({
enabled: bool
}),
retry: shape({
enableAuto: bool
}),
session: shape({
customHeaders: object,
endpoint: string,
params: object,
refreshOnRequests: bool
}),
signature: shape({
endpoint: string
}).isRequired,
uploadSuccess: shape({
method: string,
endpoint: string,
params: shape({
isBrowserPreviewCapable: any, // maybe fix this later
bitcoin_ID_noPrefix: string
})
}),
validation: shape({
itemLimit: number,
sizeLimit: number,
allowedExtensions: arrayOf(string)
})
}, },
getDefaultProps() { getDefaultProps() {
return { return {
errorNotificationMessage: getLangText('Oops, we had a problem uploading your file. Please contact us if this happens repeatedly.'),
showErrorPrompt: false,
fileClassToUpload: {
singular: getLangText('file'),
plural: getLangText('files')
},
fileInputElement: FileDragAndDrop,
// FineUploader options
autoUpload: true, autoUpload: true,
debug: false, debug: false,
multiple: false,
objectProperties: { objectProperties: {
acl: 'public-read', acl: 'public-read',
bucket: 'ascribe0' bucket: 'ascribe0'
@ -178,27 +201,25 @@ const ReactS3FineUploader = React.createClass({
messages: { messages: {
unsupportedBrowser: '<h3>' + getLangText('Upload is not functional in IE7 as IE7 has no support for CORS!') + '</h3>' unsupportedBrowser: '<h3>' + getLangText('Upload is not functional in IE7 as IE7 has no support for CORS!') + '</h3>'
}, },
formatFileName: function(name){// fix maybe formatFileName: function(name) { // fix maybe
if (name !== undefined && name.length > 26) { if (name !== undefined && name.length > 26) {
name = name.slice(0, 15) + '...' + name.slice(-15); name = name.slice(0, 15) + '...' + name.slice(-15);
} }
return name; return name;
}, }
multiple: false,
defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.'),
fileClassToUpload: {
singular: getLangText('file'),
plural: getLangText('files')
},
fileInputElement: FileDragAndDrop
}; };
}, },
getInitialState() { getInitialState() {
return { return {
filesToUpload: [], filesToUpload: [],
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()), uploader: this.createNewFineUploader(),
csrfToken: getCookie(AppConstants.csrftoken), csrfToken: getCookie(AppConstants.csrftoken),
errorState: {
manualRetryAttempt: 0,
errorClass: null
},
uploadInProgress: false,
// -1: aborted // -1: aborted
// -2: uninitialized // -2: uninitialized
@ -216,7 +237,7 @@ const ReactS3FineUploader = React.createClass({
let potentiallyNewCSRFToken = getCookie(AppConstants.csrftoken); let potentiallyNewCSRFToken = getCookie(AppConstants.csrftoken);
if(this.state.csrfToken !== potentiallyNewCSRFToken) { if(this.state.csrfToken !== potentiallyNewCSRFToken) {
this.setState({ this.setState({
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()), uploader: this.createNewFineUploader(),
csrfToken: potentiallyNewCSRFToken csrfToken: potentiallyNewCSRFToken
}); });
} }
@ -229,8 +250,12 @@ const ReactS3FineUploader = React.createClass({
this.state.uploader.cancelAll(); this.state.uploader.cancelAll();
}, },
createNewFineUploader() {
return new fineUploader.s3.FineUploaderBasic(this.propsToConfig());
},
propsToConfig() { propsToConfig() {
let objectProperties = this.props.objectProperties; const objectProperties = Object.assign({}, this.props.objectProperties);
objectProperties.key = this.requestKey; objectProperties.key = this.requestKey;
return { return {
@ -251,6 +276,7 @@ const ReactS3FineUploader = React.createClass({
multiple: this.props.multiple, multiple: this.props.multiple,
retry: this.props.retry, retry: this.props.retry,
callbacks: { callbacks: {
onAllComplete: this.onAllComplete,
onComplete: this.onComplete, onComplete: this.onComplete,
onCancel: this.onCancel, onCancel: this.onCancel,
onProgress: this.onProgress, onProgress: this.onProgress,
@ -274,6 +300,9 @@ const ReactS3FineUploader = React.createClass({
// proclaim that upload is not ready // proclaim that upload is not ready
this.props.setIsUploadReady(false); this.props.setIsUploadReady(false);
// reset any warnings propagated to parent
this.setWarning(false);
// reset internal data structures of component // reset internal data structures of component
this.setState(this.getInitialState()); this.setState(this.getInitialState());
}, },
@ -319,7 +348,7 @@ const ReactS3FineUploader = React.createClass({
// if createBlobRoutine is not defined, // if createBlobRoutine is not defined,
// we're progressing right away without posting to S3 // we're progressing right away without posting to S3
// so that this can be done manually by the form // so that this can be done manually by the form
if(!createBlobRoutine) { if (!createBlobRoutine) {
// still we warn the user of this component // still we warn the user of this component
console.warn('createBlobRoutine was not defined for ReactS3FineUploader. Continuing without creating the blob on the server.'); console.warn('createBlobRoutine was not defined for ReactS3FineUploader. Continuing without creating the blob on the server.');
resolve(); resolve();
@ -377,6 +406,19 @@ const ReactS3FineUploader = React.createClass({
this.clearFileSelection(); this.clearFileSelection();
}, },
checkFormSubmissionReady() {
const { isReadyForFormSubmission, setIsUploadReady } = this.props;
// since the form validation props isReadyForFormSubmission and setIsUploadReady
// are optional, we'll only trigger them when they're actually defined
if (typeof isReadyForFormSubmission === 'function' && typeof setIsUploadReady === 'function') {
// set uploadReady to true if the uploader's ready for submission
setIsUploadReady(isReadyForFormSubmission(this.state.filesToUpload));
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
}
},
clearFileSelection() { clearFileSelection() {
const { fileInput } = this.refs; const { fileInput } = this.refs;
if (fileInput && typeof fileInput.clearSelection === 'function') { if (fileInput && typeof fileInput.clearSelection === 'function') {
@ -394,6 +436,30 @@ const ReactS3FineUploader = React.createClass({
} }
}, },
getUploadErrorClass({ type = 'upload', reason, xhr }) {
const { manualRetryAttempt } = this.state.errorState;
let matchedErrorClass;
if ('onLine' in window.navigator && !window.navigator.onLine) {
// If the user's offline, this is definitely the most important error to show.
// TODO: use a better mechanism for checking network state, ie. offline.js
matchedErrorClass = ErrorClasses.upload.offline;
} else if (manualRetryAttempt === RETRY_ATTEMPT_TO_SHOW_CONTACT_US) {
// Use the contact us error class if they've retried a number of times
// and are still unsuccessful
matchedErrorClass = ErrorClasses.upload.contactUs;
} else {
matchedErrorClass = testErrorAgainstAll({ type, reason, xhr });
if (!matchedErrorClass) {
// If none found, show the next error message in the queue for upload errors
matchedErrorClass = ErrorQueueStore.getNextError('upload');
}
}
return matchedErrorClass;
},
getXhrErrorComment(xhr) { getXhrErrorComment(xhr) {
if (xhr) { if (xhr) {
return { return {
@ -406,13 +472,20 @@ const ReactS3FineUploader = React.createClass({
}, },
isDropzoneInactive() { isDropzoneInactive() {
const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1); const { areAssetsEditable, enableLocalHashing, multiple, showErrorPrompt, uploadMethod } = this.props;
const { errorState, filesToUpload } = this.state;
if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) { const filesToDisplay = filesToUpload.filter((file) => {
return true; return file.status !== FileStatus.DELETED &&
} else { file.status !== FileStatus.CANCELED &&
return false; file.status !== FileStatus.UPLOAD_FAILED &&
} file.size !== -1;
});
return (enableLocalHashing && !uploadMethod) ||
!areAssetsEditable ||
(showErrorPrompt && errorState.errorClass) ||
(!multiple && filesToDisplay.length > 0);
}, },
isFileValid(file) { isFileValid(file) {
@ -457,7 +530,7 @@ const ReactS3FineUploader = React.createClass({
return Q.Promise((resolve) => { return Q.Promise((resolve) => {
let changeSet = {}; let changeSet = {};
if(status === 'deleted' || status === 'canceled') { if (status === FileStatus.DELETED || status === FileStatus.CANCELED || status === FileStatus.UPLOAD_FAILED) {
changeSet.progress = { $set: 0 }; changeSet.progress = { $set: 0 };
} }
@ -484,8 +557,13 @@ const ReactS3FineUploader = React.createClass({
} }
}, },
/* FineUploader specific callback function handlers */ setWarning(hasWarning) {
if (typeof this.props.setWarning === 'function') {
this.props.setWarning(hasWarning);
}
},
/* FineUploader specific callback function handlers */
onUploadChunk(id, name, chunkData) { onUploadChunk(id, name, chunkData) {
let chunks = this.state.chunks; let chunks = this.state.chunks;
@ -514,7 +592,14 @@ const ReactS3FineUploader = React.createClass({
this.setState({ startedChunks }); this.setState({ startedChunks });
} }
},
onAllComplete(succeed, failed) {
if (this.state.uploadInProgress) {
this.setState({
uploadInProgress: false
});
}
}, },
onComplete(id, name, res, xhr) { onComplete(id, name, res, xhr) {
@ -526,12 +611,12 @@ const ReactS3FineUploader = React.createClass({
xhr: this.getXhrErrorComment(xhr) xhr: this.getXhrErrorComment(xhr)
}); });
// onError will catch any errors, so we can ignore them here // onError will catch any errors, so we can ignore them here
} else if (!res.error || res.success) { } else if (!res.error && res.success) {
let files = this.state.filesToUpload; let files = this.state.filesToUpload;
// Set the state of the completed file to 'upload successful' in order to // Set the state of the completed file to 'upload successful' in order to
// remove it from the GUI // remove it from the GUI
files[id].status = 'upload successful'; files[id].status = FileStatus.UPLOAD_SUCCESSFUL;
files[id].key = this.state.uploader.getKey(id); files[id].key = this.state.uploader.getKey(id);
let filesToUpload = React.addons.update(this.state.filesToUpload, { $set: files }); let filesToUpload = React.addons.update(this.state.filesToUpload, { $set: files });
@ -540,29 +625,14 @@ const ReactS3FineUploader = React.createClass({
// Only after the blob has been created server-side, we can make the form submittable. // Only after the blob has been created server-side, we can make the form submittable.
this.createBlob(files[id]) this.createBlob(files[id])
.then(() => { .then(() => {
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile if (typeof this.props.submitFile === 'function') {
// are optional, we'll only trigger them when they're actually defined
if(this.props.submitFile) {
this.props.submitFile(files[id]); this.props.submitFile(files[id]);
} else { } else {
console.warn('You didn\'t define submitFile as a prop in react-s3-fine-uploader'); console.warn('You didn\'t define submitFile as a prop in react-s3-fine-uploader');
} }
// for explanation, check comment of if statement above this.checkFormSubmissionReady();
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { });
// also, lets check if after the completion of this upload,
// the form is ready for submission or not
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
}
})
.catch(this.onErrorPromiseProxy);
} }
}, },
@ -578,22 +648,51 @@ const ReactS3FineUploader = React.createClass({
}, },
onError(id, name, errorReason, xhr) { onError(id, name, errorReason, xhr) {
const { errorNotificationMessage, showErrorPrompt } = this.props;
const { chunks, filesToUpload } = this.state;
console.logGlobal(errorReason, { console.logGlobal(errorReason, {
files: this.state.filesToUpload, files: filesToUpload,
chunks: this.state.chunks, chunks: chunks,
xhr: this.getXhrErrorComment(xhr) xhr: this.getXhrErrorComment(xhr)
}); });
this.props.setIsUploadReady(true); let notificationMessage;
this.cancelUploads();
let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000); if (showErrorPrompt) {
GlobalNotificationActions.appendGlobalNotification(notification); this.setStatusOfFile(id, FileStatus.UPLOAD_FAILED);
// If we've already found an error on this upload, just ignore other errors
// that pop up. They'll likely pop up again when the user retries.
if (!this.state.errorState.errorClass) {
notificationMessage = errorNotificationMessage;
const errorState = React.addons.update(this.state.errorState, {
errorClass: {
$set: this.getUploadErrorClass({
reason: errorReason,
xhr
})
}
});
this.setState({ errorState });
this.setWarning(true);
}
} else {
notificationMessage = errorReason || errorNotificationMessage;
this.cancelUploads();
}
if (notificationMessage) {
const notification = new GlobalNotificationModel(notificationMessage, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
}
}, },
onCancel(id) { onCancel(id) {
// when a upload is canceled, we need to update this components file array // when a upload is canceled, we need to update this components file array
this.setStatusOfFile(id, 'canceled') this.setStatusOfFile(id, FileStatus.CANCELED)
.then(() => { .then(() => {
if(typeof this.props.handleChangedFile === 'function') { if(typeof this.props.handleChangedFile === 'function') {
this.props.handleChangedFile(this.state.filesToUpload[id]); this.props.handleChangedFile(this.state.filesToUpload[id]);
@ -603,17 +702,18 @@ const ReactS3FineUploader = React.createClass({
let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000); let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile this.checkFormSubmissionReady();
// are optional, we'll only trigger them when they're actually defined
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { // FineUploader's onAllComplete event doesn't fire if all files are cancelled
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) { // so we need to double check if this is the last file getting cancelled.
// if so, set uploadstatus to true //
this.props.setIsUploadReady(true); // Because we're calling FineUploader.getInProgress() in a cancel callback,
} else { // the current file getting cancelled is still considered to be in progress
this.props.setIsUploadReady(false); // so there will be one file left in progress when we're cancelling the last file.
} if (this.state.uploader.getInProgress() === 1) {
} else { this.setState({
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader'); uploadInProgress: false
});
} }
return true; return true;
@ -633,7 +733,7 @@ const ReactS3FineUploader = React.createClass({
// fetch blobs for images // fetch blobs for images
response = response.map((file) => { response = response.map((file) => {
file.url = file.s3UrlSafe; file.url = file.s3UrlSafe;
file.status = 'online'; file.status = FileStatus.ONLINE;
file.progress = 100; file.progress = 100;
return file; return file;
}); });
@ -661,7 +761,7 @@ const ReactS3FineUploader = React.createClass({
onDeleteComplete(id, xhr, isError) { onDeleteComplete(id, xhr, isError) {
if(isError) { if(isError) {
this.setStatusOfFile(id, 'online'); this.setStatusOfFile(id, FileStatus.ONLINE);
let notification = new GlobalNotificationModel(getLangText('There was an error deleting your file.'), 'danger', 10000); let notification = new GlobalNotificationModel(getLangText('There was an error deleting your file.'), 'danger', 10000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
@ -670,29 +770,16 @@ const ReactS3FineUploader = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
} }
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile this.checkFormSubmissionReady();
// are optional, we'll only trigger them when they're actually defined
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
// also, lets check if after the completion of this upload,
// the form is ready for submission or not
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
}
}, },
handleDeleteFile(fileId) { handleDeleteFile(fileId) {
// We set the files state to 'deleted' immediately, so that the user is not confused with // We set the files state to 'deleted' immediately, so that the user is not confused with
// the unresponsiveness of the UI // the unresponsiveness of the UI
// //
// If there is an error during the deletion, we will just change the status back to 'online' // If there is an error during the deletion, we will just change the status back to FileStatus.ONLINE
// and display an error message // and display an error message
this.setStatusOfFile(fileId, 'deleted') this.setStatusOfFile(fileId, FileStatus.DELETED)
.then(() => { .then(() => {
if(typeof this.props.handleChangedFile === 'function') { if(typeof this.props.handleChangedFile === 'function') {
this.props.handleChangedFile(this.state.filesToUpload[fileId]); this.props.handleChangedFile(this.state.filesToUpload[fileId]);
@ -708,7 +795,7 @@ const ReactS3FineUploader = React.createClass({
// To check which files are already uploaded from previous sessions we check their status. // To check which files are already uploaded from previous sessions we check their status.
// If they are, it is "online" // If they are, it is "online"
if(this.state.filesToUpload[fileId].status !== 'online') { if(this.state.filesToUpload[fileId].status !== FileStatus.ONLINE) {
// delete file from server // delete file from server
this.state.uploader.deleteFile(fileId); this.state.uploader.deleteFile(fileId);
// this is being continued in onDeleteFile, as // this is being continued in onDeleteFile, as
@ -736,7 +823,7 @@ const ReactS3FineUploader = React.createClass({
handlePauseFile(fileId) { handlePauseFile(fileId) {
if(this.state.uploader.pauseUpload(fileId)) { if(this.state.uploader.pauseUpload(fileId)) {
this.setStatusOfFile(fileId, 'paused'); this.setStatusOfFile(fileId, FileStatus.PAUSED);
} else { } else {
throw new Error(getLangText('File upload could not be paused.')); throw new Error(getLangText('File upload could not be paused.'));
} }
@ -744,12 +831,35 @@ const ReactS3FineUploader = React.createClass({
handleResumeFile(fileId) { handleResumeFile(fileId) {
if(this.state.uploader.continueUpload(fileId)) { if(this.state.uploader.continueUpload(fileId)) {
this.setStatusOfFile(fileId, 'uploading'); this.setStatusOfFile(fileId, FileStatus.UPLOADING);
} else { } else {
throw new Error(getLangText('File upload could not be resumed.')); throw new Error(getLangText('File upload could not be resumed.'));
} }
}, },
handleRetryFiles(fileIds) {
let filesToUpload = this.state.filesToUpload;
if (fileIds.constructor !== Array) {
fileIds = [ fileIds ];
}
fileIds.forEach((fileId) => {
this.state.uploader.retry(fileId);
filesToUpload = React.addons.update(filesToUpload, { [fileId]: { status: { $set: FileStatus.UPLOADING } } });
});
this.setState({
// Reset the error class along with the retry
errorState: {
manualRetryAttempt: this.state.errorState.manualRetryAttempt + 1
},
filesToUpload
});
this.setWarning(false);
},
handleUploadFile(files) { handleUploadFile(files) {
// While files are being uploaded, the form cannot be ready // While files are being uploaded, the form cannot be ready
// for submission // for submission
@ -870,6 +980,9 @@ const ReactS3FineUploader = React.createClass({
if(files.length > 0) { if(files.length > 0) {
this.state.uploader.addFiles(files); this.state.uploader.addFiles(files);
this.synchronizeFileLists(files); this.synchronizeFileLists(files);
this.setState({
uploadInProgress: true
});
} }
} }
}, },
@ -910,12 +1023,12 @@ const ReactS3FineUploader = React.createClass({
// //
// If the user deletes one of those files, then fineuploader will still keep it in his // If the user deletes one of those files, then fineuploader will still keep it in his
// files array but with key, progress undefined and size === -1 but // files array but with key, progress undefined and size === -1 but
// status === 'upload successful'. // status === FileStatus.UPLOAD_SUCCESSFUL.
// This poses a problem as we depend on the amount of files that have // This poses a problem as we depend on the amount of files that have
// status === 'upload successful', therefore once the file is synced, // status === FileStatus.UPLOAD_SUCCESSFUL, therefore once the file is synced,
// we need to tag its status as 'deleted' (which basically happens here) // we need to tag its status as FileStatus.DELETED (which basically happens here)
if(oldAndNewFiles[i].size === -1 && (!oldAndNewFiles[i].progress || oldAndNewFiles[i].progress === 0)) { if(oldAndNewFiles[i].size === -1 && (!oldAndNewFiles[i].progress || oldAndNewFiles[i].progress === 0)) {
oldAndNewFiles[i].status = 'deleted'; oldAndNewFiles[i].status = FileStatus.DELETED;
} }
if(oldAndNewFiles[i].originalName === oldFiles[j].name) { if(oldAndNewFiles[i].originalName === oldFiles[j].name) {
@ -944,14 +1057,19 @@ const ReactS3FineUploader = React.createClass({
}, },
render() { render() {
const { errorState: { errorClass }, filesToUpload, uploadInProgress } = this.state;
const { const {
multiple, areAssetsDownloadable,
areAssetsDownloadable, areAssetsEditable,
areAssetsEditable, enableLocalHashing,
enableLocalHashing, fileClassToUpload,
fileClassToUpload, fileInputElement: FileInputElement,
fileInputElement: FileInputElement, multiple,
uploadMethod } = this.props; showErrorPrompt,
uploadMethod } = this.props;
// Only show the error state once all files are finished
const showError = !uploadInProgress && showErrorPrompt && errorClass != null;
const props = { const props = {
multiple, multiple,
@ -960,12 +1078,16 @@ const ReactS3FineUploader = React.createClass({
enableLocalHashing, enableLocalHashing,
uploadMethod, uploadMethod,
fileClassToUpload, fileClassToUpload,
filesToUpload,
uploadInProgress,
errorClass,
showError,
onDrop: this.handleUploadFile, onDrop: this.handleUploadFile,
filesToUpload: this.state.filesToUpload,
handleDeleteFile: this.handleDeleteFile, handleDeleteFile: this.handleDeleteFile,
handleCancelFile: this.handleCancelFile, handleCancelFile: this.handleCancelFile,
handlePauseFile: this.handlePauseFile, handlePauseFile: this.handlePauseFile,
handleResumeFile: this.handleResumeFile, handleResumeFile: this.handleResumeFile,
handleRetryFiles: this.handleRetryFiles,
handleCancelHashing: this.handleCancelHashing, handleCancelHashing: this.handleCancelHashing,
dropzoneInactive: this.isDropzoneInactive(), dropzoneInactive: this.isDropzoneInactive(),
hashingProgress: this.state.hashingProgress, hashingProgress: this.state.hashingProgress,

View File

@ -1,7 +1,15 @@
'use strict'; 'use strict';
import fineUploader from 'fineUploader';
import MimeTypes from '../../constants/mime_types'; import MimeTypes from '../../constants/mime_types';
// Re-export qq.status from FineUploader with an additional online
// state that we use to keep track of files from S3.
export const FileStatus = Object.assign({}, fineUploader.status, {
ONLINE: 'online'
});
export const formSubmissionValidation = { export const formSubmissionValidation = {
/** /**
* Returns a boolean if there has been at least one file uploaded * Returns a boolean if there has been at least one file uploaded
@ -10,8 +18,13 @@ export const formSubmissionValidation = {
* @return {boolean} * @return {boolean}
*/ */
atLeastOneUploadedFile(files) { atLeastOneUploadedFile(files) {
files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled'); files = files.filter((file) => {
if (files.length > 0 && files[0].status === 'upload successful') { return file.status !== FileStatus.DELETED &&
file.status !== FileStatus.CANCELED &&
file.status != FileStatus.UPLOADED_FAILED
});
if (files.length && files[0].status === FileStatus.UPLOAD_SUCCESSFUL) {
return true; return true;
} else { } else {
return false; return false;
@ -25,32 +38,32 @@ export const formSubmissionValidation = {
* @return {boolean} [description] * @return {boolean} [description]
*/ */
fileOptional(files) { fileOptional(files) {
let uploadingFiles = files.filter((file) => file.status === 'submitting'); const uploadingFiles = files.filter((file) => file.status === FileStatus.SUBMITTING);
if (uploadingFiles.length === 0) { return uploadFiles.length === 0;
return true;
} else {
return false;
}
} }
}; };
/** /**
* Filter function for filtering all deleted and canceled files * Filter function for filtering all deleted, canceled, and failed files
* @param {object} file A file from filesToUpload that has status as a prop. * @param {object} file A file from filesToUpload that has status as a prop.
* @return {boolean} * @return {boolean}
*/ */
export function displayValidFilesFilter(file) { export function displayValidFilesFilter(file) {
return file.status !== 'deleted' && file.status !== 'canceled'; return file.status !== FileStatus.DELETED &&
file.status !== FileStatus.CANCELED &&
file.status !== FileStatus.UPLOAD_FAILED;
} }
/** /**
* Filter function for filtering all files except for deleted and canceled files * Filter function for filtering all files except for deleted, canceled, and failed files
* @param {object} file A file from filesToUpload that has status as a prop. * @param {object} file A file from filesToUpload that has status as a prop.
* @return {boolean} * @return {boolean}
*/ */
export function displayRemovedFilesFilter(file) { export function displayRemovedFilesFilter(file) {
return file.status === 'deleted' || file.status === 'canceled'; return file.status === FileStatus.DELETED ||
file.status === FileStatus.CANCELED ||
file.status === FileStatus.UPLOAD_FAILED;
} }
@ -60,7 +73,10 @@ export function displayRemovedFilesFilter(file) {
* @return {boolean} * @return {boolean}
*/ */
export function displayValidProgressFilesFilter(file) { export function displayValidProgressFilesFilter(file) {
return file.status !== 'deleted' && file.status !== 'canceled' && file.status !== 'online'; return file.status !== FileStatus.DELETED &&
file.status !== FileStatus.CANCELED &&
file.status !== FileStatus.UPLOAD_FAILED &&
file.status !== FileStatus.ONLINE;
} }
@ -77,7 +93,7 @@ export function displayValidProgressFilesFilter(file) {
export function transformAllowedExtensionsToInputAcceptProp(allowedExtensions) { export function transformAllowedExtensionsToInputAcceptProp(allowedExtensions) {
// Get the mime type of the extension if it's defined or add a dot in front of the extension // Get the mime type of the extension if it's defined or add a dot in front of the extension
// This is important for Safari as it doesn't understand just the extension. // This is important for Safari as it doesn't understand just the extension.
let prefixedAllowedExtensions = allowedExtensions.map((ext) => { const prefixedAllowedExtensions = allowedExtensions.map((ext) => {
return MimeTypes[ext] || ('.' + ext); return MimeTypes[ext] || ('.' + ext);
}); });

View File

@ -18,6 +18,11 @@ import { setDocumentTitle } from '../utils/dom_utils';
let CoaVerifyContainer = React.createClass({ let CoaVerifyContainer = React.createClass({
propTypes: { propTypes: {
// Provided from AscribeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
@ -27,7 +32,7 @@ let CoaVerifyContainer = React.createClass({
return ( return (
<div className="ascribe-login-wrapper"> <div className="ascribe-login-wrapper">
<br/> <br />
<div className="ascribe-login-text ascribe-login-header"> <div className="ascribe-login-text ascribe-login-header">
{getLangText('Verify your Certificate of Authenticity')} {getLangText('Verify your Certificate of Authenticity')}
</div> </div>
@ -37,7 +42,7 @@ let CoaVerifyContainer = React.createClass({
signature={signature}/> signature={signature}/>
<br /> <br />
<br /> <br />
{getLangText('ascribe is using the following public key for verification')}: {getLangText('ascribe is using the following public key for verification')}:
<br /> <br />
<pre> <pre>
-----BEGIN PUBLIC KEY----- -----BEGIN PUBLIC KEY-----
@ -60,9 +65,8 @@ let CoaVerifyForm = React.createClass({
}, },
handleSuccess(response){ handleSuccess(response){
let notification = null;
if (response.verdict) { if (response.verdict) {
notification = new GlobalNotificationModel(getLangText('Certificate of Authenticity successfully verified'), 'success'); const notification = new GlobalNotificationModel(getLangText('Certificate of Authenticity successfully verified'), 'success');
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
} }
}, },
@ -71,46 +75,44 @@ let CoaVerifyForm = React.createClass({
const { message, signature } = this.props; const { message, signature } = this.props;
return ( return (
<div> <Form
<Form url={ApiUrls.coa_verify}
url={ApiUrls.coa_verify} handleSuccess={this.handleSuccess}
handleSuccess={this.handleSuccess} buttons={
buttons={ <button
<button type="submit"
type="submit" className="btn btn-default btn-wide">
className="btn btn-default btn-wide"> {getLangText('Verify your Certificate of Authenticity')}
{getLangText('Verify your Certificate of Authenticity')} </button>
</button>} }
spinner={ spinner={
<span className="btn btn-default btn-wide btn-spinner"> <span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" /> <AscribeSpinner color="dark-blue" size="md" />
</span> </span>
}> }>
<Property <Property
name='message' name='message'
label={getLangText('Message')}> label={getLangText('Message')}>
<input <input
type="text" type="text"
placeholder={getLangText('Copy paste the message on the bottom of your Certificate of Authenticity')} placeholder={getLangText('Copy paste the message on the bottom of your Certificate of Authenticity')}
autoComplete="on" autoComplete="on"
defaultValue={message} defaultValue={message}
name="username" required />
required/> </Property>
</Property> <Property
<Property name='signature'
name='signature' label="Signature"
label="Signature" editable={true}
editable={true} overrideForm={true}>
overrideForm={true}> <InputTextAreaToggable
<InputTextAreaToggable rows={3}
rows={3} placeholder={getLangText('Copy paste the signature on the bottom of your Certificate of Authenticity')}
placeholder={getLangText('Copy paste the signature on the bottom of your Certificate of Authenticity')} defaultValue={signature}
defaultValue={signature} required />
required/> </Property>
</Property> <hr />
<hr /> </Form>
</Form>
</div>
); );
} }
}); });

View File

@ -1,21 +1,42 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import { History } from 'react-router';
import { getLangText } from '../utils/lang_utils'; import { getLangText } from '../utils/lang_utils';
let ErrorNotFoundPage = React.createClass({ let ErrorNotFoundPage = React.createClass({
propTypes: { propTypes: {
message: React.PropTypes.string message: React.PropTypes.string,
// Provided from AscribeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object
}, },
mixins: [History],
getDefaultProps() { getDefaultProps() {
return { return {
message: getLangText("Oops, the page you are looking for doesn't exist.") message: getLangText("Oops, the page you are looking for doesn't exist.")
}; };
}, },
componentDidMount() {
// The previous page, if any, is the second item in the locationQueue
const { locationQueue: [ , previousPage ] } = this.history;
if (previousPage) {
console.logGlobal('Page not found', {
previousPath: previousPage.pathname
});
}
},
render() { render() {
return ( return (
<div className="row"> <div className="row">

View File

@ -5,8 +5,12 @@ import React from 'react';
import { getLangText } from '../utils/lang_utils'; import { getLangText } from '../utils/lang_utils';
let Footer = React.createClass({ let Footer = React.createClass({
propTypes: {
activeRoute: React.PropTypes.object.isRequired
},
render() { render() {
return ( return !this.props.activeRoute.hideFooter ? (
<div className="ascribe-footer hidden-print"> <div className="ascribe-footer hidden-print">
<p className="ascribe-sub-sub-statement"> <p className="ascribe-sub-sub-statement">
<br /> <br />
@ -24,7 +28,7 @@ let Footer = React.createClass({
<a href="https://www.linkedin.com/company/4816284?trk=vsrp_companies_res_name&trkInfo=VSRPsearchId%3A122827941425632318075%2CVSRPtargetId%3A4816284%2CVSRPcmpt%3Aprimary" className="social social-linked-in" target="_blank"></a> <a href="https://www.linkedin.com/company/4816284?trk=vsrp_companies_res_name&trkInfo=VSRPsearchId%3A122827941425632318075%2CVSRPtargetId%3A4816284%2CVSRPcmpt%3Aprimary" className="social social-linked-in" target="_blank"></a>
</p> </p>
</div> </div>
); ) : null;
} }
}); });

View File

@ -14,41 +14,28 @@ import NavItem from 'react-bootstrap/lib/NavItem';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import AclProxy from './acl_proxy';
import EventActions from '../actions/event_actions'; import EventActions from '../actions/event_actions';
import PieceListStore from '../stores/piece_list_store'; import PieceListStore from '../stores/piece_list_store';
import UserActions from '../actions/user_actions'; import AclProxy from './acl_proxy';
import UserStore from '../stores/user_store';
import WhitelabelActions from '../actions/whitelabel_actions';
import WhitelabelStore from '../stores/whitelabel_store';
import HeaderNotifications from './header_notification'; import HeaderNotifications from './header_notification';
import HeaderNotificationDebug from './header_notification_debug'; import HeaderNotificationDebug from './header_notification_debug';
import NavRoutesLinks from './nav_routes_links'; import NavRoutesLinks from './nav_routes_links';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils'; import { getLangText } from '../utils/lang_utils';
import { constructHead } from '../utils/dom_utils'; import { constructHead } from '../utils/dom_utils';
let Header = React.createClass({ let Header = React.createClass({
propTypes: { propTypes: {
routes: React.PropTypes.arrayOf(React.PropTypes.object) currentUser: React.PropTypes.object.isRequired,
routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
whitelabel: React.PropTypes.object.isRequired
}, },
getInitialState() { getInitialState() {
return mergeOptions( return PieceListStore.getState();
PieceListStore.getState(),
WhitelabelStore.getState(),
UserStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
@ -56,35 +43,14 @@ let Header = React.createClass({
// conflicts with routes that may need to wait to load the piece list // conflicts with routes that may need to wait to load the piece list
PieceListStore.listen(this.onChange); PieceListStore.listen(this.onChange);
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser.defer();
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel.defer();
// react-bootstrap 0.25.1 has a bug in which it doesn't // react-bootstrap 0.25.1 has a bug in which it doesn't
// close the mobile expanded navigation after a click by itself. // close the mobile expanded navigation after a click by itself.
// To get rid of this, we set the state of the component ourselves. // To get rid of this, we set the state of the component ourselves.
history.listen(this.onRouteChange); history.listen(this.onRouteChange);
if (this.state.currentUser && this.state.currentUser.email) {
EventActions.profileDidLoad.defer(this.state.currentUser);
}
},
componentWillUpdate(nextProps, nextState) {
const { currentUser: { email: curEmail } = {} } = this.state;
const { currentUser: { email: nextEmail } = {} } = nextState;
if (nextEmail && curEmail !== nextEmail) {
EventActions.profileDidLoad.defer(nextState.currentUser);
}
}, },
componentWillUnmount() { componentWillUnmount() {
PieceListStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
//history.unlisten(this.onRouteChange); //history.unlisten(this.onRouteChange);
}, },
@ -93,7 +59,7 @@ let Header = React.createClass({
}, },
getLogo() { getLogo() {
let { whitelabel } = this.state; const { whitelabel } = this.props;
if (whitelabel.head) { if (whitelabel.head) {
constructHead(whitelabel.head); constructHead(whitelabel.head);
@ -117,7 +83,7 @@ let Header = React.createClass({
getPoweredBy() { getPoweredBy() {
return ( return (
<AclProxy <AclProxy
aclObject={this.state.whitelabel} aclObject={this.props.whitelabel}
aclName="acl_view_powered_by"> aclName="acl_view_powered_by">
<li> <li>
<a className="pull-right ascribe-powered-by" href="https://www.ascribe.io/" target="_blank"> <a className="pull-right ascribe-powered-by" href="https://www.ascribe.io/" target="_blank">
@ -164,7 +130,9 @@ let Header = React.createClass({
}, },
render() { render() {
const { currentUser, unfilteredPieceListCount } = this.state; const { currentUser, routes } = this.props;
const { unfilteredPieceListCount } = this.state;
let account; let account;
let signup; let signup;
let navRoutesLinks; let navRoutesLinks;
@ -173,13 +141,13 @@ let Header = React.createClass({
account = ( account = (
<DropdownButton <DropdownButton
ref='dropdownbutton' ref='dropdownbutton'
id="nav-route-user-dropdown"
eventKey="1" eventKey="1"
title={currentUser.username}> title={currentUser.username}>
<LinkContainer <LinkContainer
to="/settings" to="/settings"
onClick={this.onMenuItemClick}> onClick={this.onMenuItemClick}>
<MenuItem <MenuItem eventKey="2">
eventKey="2">
{getLangText('Account Settings')} {getLangText('Account Settings')}
</MenuItem> </MenuItem>
</LinkContainer> </LinkContainer>
@ -189,17 +157,14 @@ let Header = React.createClass({
<LinkContainer <LinkContainer
to="/contract_settings" to="/contract_settings"
onClick={this.onMenuItemClick}> onClick={this.onMenuItemClick}>
<MenuItem <MenuItem eventKey="2">
eventKey="2">
{getLangText('Contract Settings')} {getLangText('Contract Settings')}
</MenuItem> </MenuItem>
</LinkContainer> </LinkContainer>
</AclProxy> </AclProxy>
<MenuItem divider /> <MenuItem divider />
<LinkContainer <LinkContainer to="/logout">
to="/logout"> <MenuItem eventKey="3">
<MenuItem
eventKey="3">
{getLangText('Log out')} {getLangText('Log out')}
</MenuItem> </MenuItem>
</LinkContainer> </LinkContainer>
@ -216,21 +181,19 @@ let Header = React.createClass({
navbar navbar
right right
hasPieces={!!unfilteredPieceListCount} hasPieces={!!unfilteredPieceListCount}
routes={this.props.routes} routes={routes}
userAcl={currentUser.acl} /> userAcl={currentUser.acl} />
); );
} else { } else {
account = ( account = (
<LinkContainer <LinkContainer to="/login">
to="/login">
<NavItem> <NavItem>
{getLangText('LOGIN')} {getLangText('LOGIN')}
</NavItem> </NavItem>
</LinkContainer> </LinkContainer>
); );
signup = ( signup = (
<LinkContainer <LinkContainer to="/signup">
to="/signup">
<NavItem> <NavItem>
{getLangText('SIGNUP')} {getLangText('SIGNUP')}
</NavItem> </NavItem>
@ -246,13 +209,12 @@ let Header = React.createClass({
toggleNavKey={0} toggleNavKey={0}
fixedTop={true} fixedTop={true}
className="hidden-print"> className="hidden-print">
<CollapsibleNav <CollapsibleNav eventKey={0}>
eventKey={0}>
<Nav navbar left> <Nav navbar left>
{this.getPoweredBy()} {this.getPoweredBy()}
</Nav> </Nav>
<Nav navbar right> <Nav navbar right>
<HeaderNotificationDebug show={false}/> <HeaderNotificationDebug show={false} />
{account} {account}
{signup} {signup}
</Nav> </Nav>

View File

@ -11,16 +11,12 @@ import Nav from 'react-bootstrap/lib/Nav';
import NotificationActions from '../actions/notification_actions'; import NotificationActions from '../actions/notification_actions';
import NotificationStore from '../stores/notification_store'; import NotificationStore from '../stores/notification_store';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils'; import { getLangText } from '../utils/lang_utils';
let HeaderNotifications = React.createClass({ let HeaderNotifications = React.createClass({
getInitialState() { getInitialState() {
return mergeOptions( return NotificationStore.getState();
NotificationStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
@ -62,7 +58,7 @@ let HeaderNotifications = React.createClass({
this.refs.dropdownbutton.setDropdownState(false); this.refs.dropdownbutton.setDropdownState(false);
}, },
getPieceNotifications(){ getPieceNotifications() {
if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) { if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) {
return ( return (
<div> <div>
@ -87,7 +83,7 @@ let HeaderNotifications = React.createClass({
return null; return null;
}, },
getEditionNotifications(){ getEditionNotifications() {
if (this.state.editionListNotifications && this.state.editionListNotifications.length > 0) { if (this.state.editionListNotifications && this.state.editionListNotifications.length > 0) {
return ( return (
<div> <div>
@ -114,7 +110,7 @@ let HeaderNotifications = React.createClass({
render() { render() {
if ((this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) || if ((this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) ||
(this.state.editionListNotifications && this.state.editionListNotifications.length > 0)){ (this.state.editionListNotifications && this.state.editionListNotifications.length > 0)) {
let numNotifications = 0; let numNotifications = 0;
if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) { if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) {
numNotifications += this.state.pieceListNotifications.length; numNotifications += this.state.pieceListNotifications.length;
@ -125,7 +121,8 @@ let HeaderNotifications = React.createClass({
return ( return (
<Nav navbar right> <Nav navbar right>
<DropdownButton <DropdownButton
ref='dropdownbutton' ref='dropdownButton'
id="header-notification-dropdown"
eventKey="1" eventKey="1"
title={ title={
<span> <span>

View File

@ -11,19 +11,12 @@ import { setDocumentTitle } from '../utils/dom_utils';
let LoginContainer = React.createClass({ let LoginContainer = React.createClass({
propTypes: { propTypes: {
message: React.PropTypes.string, // Provided from AscribeApp
redirectOnLoggedIn: React.PropTypes.bool, currentUser: React.PropTypes.object,
redirectOnLoginSuccess: React.PropTypes.bool, whitelabel: React.PropTypes.object,
onLogin: React.PropTypes.func,
location: React.PropTypes.object
},
getDefaultProps() { // Provided from router
return { location: React.PropTypes.object
message: getLangText('Enter') + ' ascribe',
redirectOnLoggedIn: true,
redirectOnLoginSuccess: true
};
}, },
render() { render() {
@ -31,12 +24,7 @@ let LoginContainer = React.createClass({
return ( return (
<div className="ascribe-login-wrapper"> <div className="ascribe-login-wrapper">
<LoginForm <LoginForm location={this.props.location} />
redirectOnLoggedIn={this.props.redirectOnLoggedIn}
redirectOnLoginSuccess={this.props.redirectOnLoginSuccess}
message={this.props.message}
onLogin={this.props.onLogin}
location={this.props.location}/>
<div className="ascribe-login-text"> <div className="ascribe-login-text">
{getLangText('Not an ascribe user')}&#63; <Link to="/signup">{getLangText('Sign up')}...</Link><br/> {getLangText('Not an ascribe user')}&#63; <Link to="/signup">{getLangText('Sign up')}...</Link><br/>
{getLangText('Forgot my password')}&#63; <Link to="/password_reset">{getLangText('Rescue me')}...</Link> {getLangText('Forgot my password')}&#63; <Link to="/password_reset">{getLangText('Rescue me')}...</Link>

View File

@ -6,23 +6,25 @@ import { History } from 'react-router';
import AscribeSpinner from './ascribe_spinner'; import AscribeSpinner from './ascribe_spinner';
import UserActions from '../actions/user_actions'; import UserActions from '../actions/user_actions';
import { alt, altWhitelabel, altUser, altThirdParty } from '../alt';
import { getLangText } from '../utils/lang_utils'; import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils'; import { setDocumentTitle } from '../utils/dom_utils';
let LogoutContainer = React.createClass({ let LogoutContainer = React.createClass({
propTypes: {
// Provided from AscribeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object
},
mixins: [History], mixins: [History],
componentDidMount() { componentDidMount() {
UserActions.logoutCurrentUser(); UserActions.logoutCurrentUser();
alt.flush();
altWhitelabel.flush();
altUser.flush();
altThirdParty.flush();
// kill intercom (with fire)
window.Intercom('shutdown');
}, },
render() { render() {

View File

@ -30,6 +30,7 @@ let NavRoutesLinksLink = React.createClass({
return ( return (
<DropdownButton <DropdownButton
disabled={disabled} disabled={disabled}
id={`nav-route-${headerTitle.toLowerCase()}-dropdown`}
title={headerTitle}> title={headerTitle}>
{children} {children}
</DropdownButton> </DropdownButton>

View File

@ -16,52 +16,44 @@ import { setDocumentTitle } from '../utils/dom_utils';
let PasswordResetContainer = React.createClass({ let PasswordResetContainer = React.createClass({
propTypes: { propTypes: {
// Provided from AscribeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
getInitialState() { getInitialState() {
return {isRequested: false}; return { isRequested: false };
}, },
handleRequestSuccess(email) { handleRequestSuccess(email) {
this.setState({isRequested: email}); this.setState({ isRequested: !!email });
}, },
render() { render() {
let { location } = this.props; const { email: emailQuery, token: tokenQuery } = this.props.location.query;
const { isRequested } = this.state;
if (location.query.email && location.query.token) { if (emailQuery && tokenQuery) {
return ( return (
<div> <PasswordResetForm
<PasswordResetForm email={emailQuery}
email={location.query.email} token={tokenQuery} />
token={location.query.token}/> );
} else if (!isRequested) {
return (
<PasswordRequestResetForm handleRequestSuccess={this.handleRequestSuccess} />
);
} else {
return (
<div className="ascribe-login-text ascribe-login-header">
{getLangText('If your email address exists in our database, you will receive a password recovery link in a few minutes.')}
</div> </div>
); );
} }
else { }
if (this.state.isRequested === false) {
return (
<div>
<PasswordRequestResetForm
handleRequestSuccess={this.handleRequestSuccess}/>
</div>
);
}
else if (this.state.isRequested) {
return (
<div>
<div className="ascribe-login-text ascribe-login-header">
{getLangText('If your email address exists in our database, you will receive a password recovery link in a few minutes.')}
</div>
</div>
);
}
else {
return <span />;
}
}
}
}); });
let PasswordRequestResetForm = React.createClass({ let PasswordRequestResetForm = React.createClass({
@ -70,9 +62,10 @@ let PasswordRequestResetForm = React.createClass({
}, },
handleSuccess() { handleSuccess() {
let notificationText = getLangText('If your email address exists in our database, you will receive a password recovery link in a few minutes.'); const notificationText = getLangText('If your email address exists in our database, you will receive a password recovery link in a few minutes.');
let notification = new GlobalNotificationModel(notificationText, 'success', 50000); const notification = new GlobalNotificationModel(notificationText, 'success', 50000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
this.props.handleRequestSuccess(this.refs.form.refs.email.state.value); this.props.handleRequestSuccess(this.refs.form.refs.email.state.value);
}, },
@ -90,12 +83,13 @@ let PasswordRequestResetForm = React.createClass({
type="submit" type="submit"
className="btn btn-default btn-wide"> className="btn btn-default btn-wide">
{getLangText('Reset your password')} {getLangText('Reset your password')}
</button>} </button>
}
spinner={ spinner={
<span className="btn btn-default btn-wide btn-spinner"> <span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" /> <AscribeSpinner color="dark-blue" size="md" />
</span> </span>
}> }>
<div className="ascribe-form-header"> <div className="ascribe-form-header">
<h3>{getLangText('Reset your password')}</h3> <h3>{getLangText('Reset your password')}</h3>
</div> </div>
@ -149,12 +143,13 @@ let PasswordResetForm = React.createClass({
type="submit" type="submit"
className="btn btn-default btn-wide"> className="btn btn-default btn-wide">
{getLangText('Reset your password')} {getLangText('Reset your password')}
</button>} </button>
}
spinner={ spinner={
<span className="btn btn-default btn-wide btn-spinner"> <span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" /> <AscribeSpinner color="dark-blue" size="md" />
</span> </span>
}> }>
<div className="ascribe-form-header"> <div className="ascribe-form-header">
<h3>{getLangText('Reset the password for')} {this.props.email}</h3> <h3>{getLangText('Reset the password for')} {this.props.email}</h3>
</div> </div>

View File

@ -36,13 +36,22 @@ let PieceList = React.createClass({
accordionListItemType: React.PropTypes.func, accordionListItemType: React.PropTypes.func,
bulkModalButtonListType: React.PropTypes.func, bulkModalButtonListType: React.PropTypes.func,
canLoadPieceList: React.PropTypes.bool, canLoadPieceList: React.PropTypes.bool,
redirectTo: React.PropTypes.string, redirectTo: React.PropTypes.shape({
pathname: React.PropTypes.string,
query: React.PropTypes.object
}),
shouldRedirect: React.PropTypes.func, shouldRedirect: React.PropTypes.func,
customSubmitButton: React.PropTypes.element, customSubmitButton: React.PropTypes.element,
customThumbnailPlaceholder: React.PropTypes.func, customThumbnailPlaceholder: React.PropTypes.func,
filterParams: React.PropTypes.array, filterParams: React.PropTypes.array,
orderParams: React.PropTypes.array, orderParams: React.PropTypes.array,
orderBy: React.PropTypes.string, orderBy: React.PropTypes.string,
// Provided from AscribeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object.isRequired,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
@ -62,8 +71,11 @@ let PieceList = React.createClass({
] ]
}], }],
orderParams: ['artist_name', 'title'], orderParams: ['artist_name', 'title'],
redirectTo: '/register_piece', redirectTo: {
shouldRedirect: () => true pathname: '/register_piece',
query: null
},
shouldRedirect: (pieceCount) => !pieceCount
}; };
}, },
@ -87,7 +99,7 @@ let PieceList = React.createClass({
PieceListStore.listen(this.onChange); PieceListStore.listen(this.onChange);
EditionListStore.listen(this.onChange); EditionListStore.listen(this.onChange);
let page = this.props.location.query.page || 1; const page = this.props.location.query.page || 1;
if (this.props.canLoadPieceList && (this.state.pieceList.length === 0 || this.state.page !== page)) { if (this.props.canLoadPieceList && (this.state.pieceList.length === 0 || this.state.page !== page)) {
this.loadPieceList({ page }); this.loadPieceList({ page });
} }
@ -120,10 +132,16 @@ let PieceList = React.createClass({
const { location: { query }, redirectTo, shouldRedirect } = this.props; const { location: { query }, redirectTo, shouldRedirect } = this.props;
const { unfilteredPieceListCount } = this.state; const { unfilteredPieceListCount } = this.state;
if (redirectTo && unfilteredPieceListCount === 0 && if (redirectTo && redirectTo.pathname &&
(typeof shouldRedirect === 'function' && shouldRedirect(unfilteredPieceListCount))) { (typeof shouldRedirect === 'function' && shouldRedirect(unfilteredPieceListCount))) {
// FIXME: hack to redirect out of the dispatch cycle // FIXME: hack to redirect out of the dispatch cycle
window.setTimeout(() => this.history.push({ query, pathname: redirectTo }), 0); window.setTimeout(() => this.history.push({
// Occasionally, the back end also sets query parameters for Onion.
// We need to consider this by merging all passed query parameters, as we'll
// otherwise end up in a 404 screen
query: Object.assign({}, query, redirectTo.query),
pathname: redirectTo.pathname
}), 0);
} }
}, },
@ -163,8 +181,8 @@ let PieceList = React.createClass({
}, },
getPagination() { getPagination() {
let currentPage = parseInt(this.props.location.query.page, 10) || 1; const currentPage = parseInt(this.props.location.query.page, 10) || 1;
let totalPages = Math.ceil(this.state.pieceListCount / this.state.pageSize); const totalPages = Math.ceil(this.state.pieceListCount / this.state.pageSize);
if (this.state.pieceListCount > 20) { if (this.state.pieceListCount > 20) {
return ( return (
@ -191,8 +209,7 @@ let PieceList = React.createClass({
}); });
// first we need to apply the filter on the piece list // first we need to apply the filter on the piece list
this this.loadPieceList({ page: 1, filterBy })
.loadPieceList({ page: 1, filterBy })
.then(() => { .then(() => {
// but also, we need to filter all the open edition lists // but also, we need to filter all the open edition lists
this.state.pieceList this.state.pieceList
@ -227,23 +244,22 @@ let PieceList = React.createClass({
}, },
fetchSelectedPieceEditionList() { fetchSelectedPieceEditionList() {
let filteredPieceIdList = Object.keys(this.state.editionList) const filteredPieceIdList = Object.keys(this.state.editionList)
.filter((pieceId) => { .filter((pieceId) => {
return this.state.editionList[pieceId] return this.state.editionList[pieceId]
.filter((edition) => edition.selected).length > 0; .filter((edition) => edition.selected)
}); .length;
});
return filteredPieceIdList; return filteredPieceIdList;
}, },
fetchSelectedEditionList() { fetchSelectedEditionList() {
let selectedEditionList = []; const selectedEditionList = Object.keys(this.state.editionList)
.reduce((selectedList, pieceId) => {
Object const selectedEditionsForPiece = this.state.editionList[pieceId]
.keys(this.state.editionList) .filter((edition) => edition.selected);
.forEach((pieceId) => { return selectedList.concat(selectedEditionsForPiece);
let filteredEditionsForPiece = this.state.editionList[pieceId].filter((edition) => edition.selected); }, []);
selectedEditionList = selectedEditionList.concat(filteredEditionsForPiece);
});
return selectedEditionList; return selectedEditionList;
}, },
@ -255,7 +271,7 @@ let PieceList = React.createClass({
this.fetchSelectedPieceEditionList() this.fetchSelectedPieceEditionList()
.forEach((pieceId) => { .forEach((pieceId) => {
EditionListActions.refreshEditionList({pieceId}); EditionListActions.refreshEditionList({ pieceId });
}); });
EditionListActions.clearAllEditionSelections(); EditionListActions.clearAllEditionSelections();
}, },
@ -264,10 +280,12 @@ let PieceList = React.createClass({
const { const {
accordionListItemType: AccordionListItemType, accordionListItemType: AccordionListItemType,
bulkModalButtonListType: BulkModalButtonListType, bulkModalButtonListType: BulkModalButtonListType,
currentUser,
customSubmitButton, customSubmitButton,
customThumbnailPlaceholder, customThumbnailPlaceholder,
filterParams, filterParams,
orderParams } = this.props; orderParams,
whitelabel } = this.props;
const loadingElement = <AscribeSpinner color='dark-blue' size='lg'/>; const loadingElement = <AscribeSpinner color='dark-blue' size='lg'/>;
@ -275,6 +293,7 @@ let PieceList = React.createClass({
const availableAcls = getAvailableAcls(selectedEditions, (aclName) => aclName !== 'acl_view'); const availableAcls = getAvailableAcls(selectedEditions, (aclName) => aclName !== 'acl_view');
setDocumentTitle(getLangText('Collection')); setDocumentTitle(getLangText('Collection'));
return ( return (
<div> <div>
<PieceListToolbar <PieceListToolbar
@ -295,17 +314,19 @@ let PieceList = React.createClass({
className="ascribe-piece-list-bulk-modal"> className="ascribe-piece-list-bulk-modal">
<BulkModalButtonListType <BulkModalButtonListType
availableAcls={availableAcls} availableAcls={availableAcls}
pieceOrEditions={selectedEditions} currentUser={currentUser}
handleSuccess={this.handleAclSuccess} handleSuccess={this.handleAclSuccess}
pieceOrEditions={selectedEditions}
whitelabel={whitelabel}
className="text-center ascribe-button-list collapse-group"> className="text-center ascribe-button-list collapse-group">
<DeleteButton <DeleteButton
handleSuccess={this.handleAclSuccess} handleSuccess={this.handleAclSuccess}
editions={selectedEditions}/> editions={selectedEditions} />
</BulkModalButtonListType> </BulkModalButtonListType>
</PieceListBulkModal> </PieceListBulkModal>
<PieceListFilterDisplay <PieceListFilterDisplay
filterBy={this.state.filterBy} filterBy={this.state.filterBy}
filterParams={filterParams}/> filterParams={filterParams} />
<AccordionList <AccordionList
className="ascribe-accordion-list" className="ascribe-accordion-list"
changeOrder={this.accordionChangeOrder} changeOrder={this.accordionChangeOrder}
@ -318,13 +339,15 @@ let PieceList = React.createClass({
page={this.state.page} page={this.state.page}
pageSize={this.state.pageSize} pageSize={this.state.pageSize}
loadingElement={loadingElement}> loadingElement={loadingElement}>
{this.state.pieceList.map((piece, i) => { {this.state.pieceList.map((piece) => {
return ( return (
<AccordionListItemType <AccordionListItemType
key={piece.id}
className="col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2 ascribe-accordion-list-item" className="col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2 ascribe-accordion-list-item"
content={piece} content={piece}
currentUser={currentUser}
thumbnailPlaceholder={customThumbnailPlaceholder} thumbnailPlaceholder={customThumbnailPlaceholder}
key={i}> whitelabel={whitelabel}>
<AccordionListItemTableEditions <AccordionListItemTableEditions
className="ascribe-accordion-list-item-table col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2" className="ascribe-accordion-list-item-table col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2"
parentId={piece.id} /> parentId={piece.id} />

View File

@ -6,27 +6,20 @@ import { History } from 'react-router';
import Col from 'react-bootstrap/lib/Col'; import Col from 'react-bootstrap/lib/Col';
import Row from 'react-bootstrap/lib/Row'; import Row from 'react-bootstrap/lib/Row';
import WhitelabelActions from '../actions/whitelabel_actions';
import WhitelabelStore from '../stores/whitelabel_store';
import PieceListStore from '../stores/piece_list_store'; import PieceListStore from '../stores/piece_list_store';
import PieceListActions from '../actions/piece_list_actions'; import PieceListActions from '../actions/piece_list_actions';
import UserStore from '../stores/user_store';
import GlobalNotificationModel from '../models/global_notification_model'; import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions'; import GlobalNotificationActions from '../actions/global_notification_actions';
import Property from './ascribe_forms/property'; import Property from './ascribe_forms/property';
import RegisterPieceForm from './ascribe_forms/form_register_piece'; import RegisterPieceForm from './ascribe_forms/form_register_piece';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils'; import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils'; import { setDocumentTitle } from '../utils/dom_utils';
let RegisterPiece = React.createClass( { let RegisterPiece = React.createClass( {
propTypes: { propTypes: {
headerMessage: React.PropTypes.string, headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string, submitMessage: React.PropTypes.string,
@ -35,30 +28,27 @@ let RegisterPiece = React.createClass( {
React.PropTypes.element, React.PropTypes.element,
React.PropTypes.string React.PropTypes.string
]), ]),
// Provided from AscribeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object.isRequired,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
mixins: [History], mixins: [History],
getInitialState(){ getInitialState(){
return mergeOptions( return PieceListStore.getState();
UserStore.getState(),
WhitelabelStore.getState(),
PieceListStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
PieceListStore.listen(this.onChange); PieceListStore.listen(this.onChange);
UserStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
}, },
componentWillUnmount() { componentWillUnmount() {
PieceListStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
@ -79,7 +69,9 @@ let RegisterPiece = React.createClass( {
}, },
getSpecifyEditions() { getSpecifyEditions() {
if (this.state.whitelabel && this.state.whitelabel.acl_create_editions || Object.keys(this.state.whitelabel).length === 0) { const { whitelabel } = this.props;
if (whitelabel.acl_create_editions || Object.keys(whitelabel).length) {
return ( return (
<Property <Property
name="num_editions" name="num_editions"
@ -105,8 +97,7 @@ let RegisterPiece = React.createClass( {
<RegisterPieceForm <RegisterPieceForm
{...this.props} {...this.props}
isFineUploaderActive={true} isFineUploaderActive={true}
handleSuccess={this.handleSuccess} handleSuccess={this.handleSuccess}>
location={this.props.location}>
{this.props.children} {this.props.children}
{this.getSpecifyEditions()} {this.getSpecifyEditions()}
</RegisterPieceForm> </RegisterPieceForm>

View File

@ -11,6 +11,11 @@ import { setDocumentTitle } from '../utils/dom_utils';
let SignupContainer = React.createClass({ let SignupContainer = React.createClass({
propTypes: { propTypes: {
// Provided from AscribeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
@ -21,7 +26,7 @@ let SignupContainer = React.createClass({
}; };
}, },
handleSuccess(message){ handleSuccess(message) {
this.setState({ this.setState({
submitted: true, submitted: true,
message: message message: message
@ -29,14 +34,17 @@ let SignupContainer = React.createClass({
}, },
render() { render() {
const { location } = this.props;
const { message, submitted } = this.state;
setDocumentTitle(getLangText('Sign up')); setDocumentTitle(getLangText('Sign up'));
if (this.state.submitted){ if (submitted) {
return ( return (
<div className="ascribe-login-wrapper"> <div className="ascribe-login-wrapper">
<br/> <br/>
<div className="ascribe-login-text ascribe-login-header"> <div className="ascribe-login-text ascribe-login-header">
{this.state.message} {message}
</div> </div>
</div> </div>
); );
@ -45,7 +53,7 @@ let SignupContainer = React.createClass({
<div className="ascribe-login-wrapper"> <div className="ascribe-login-wrapper">
<SignupForm <SignupForm
handleSuccess={this.handleSuccess} handleSuccess={this.handleSuccess}
location={this.props.location}/> location={location}/>
<div className="ascribe-login-text"> <div className="ascribe-login-text">
{getLangText('Already an ascribe user')}&#63; <Link to="/login">{getLangText('Log in')}...</Link><br/> {getLangText('Already an ascribe user')}&#63; <Link to="/login">{getLangText('Log in')}...</Link><br/>
</div> </div>

View File

@ -32,9 +32,8 @@ const { object } = React.PropTypes;
const PRRegisterPieceForm = React.createClass({ const PRRegisterPieceForm = React.createClass({
propTypes: { propTypes: {
location: object, currentUser: object.isRequired,
history: object, location: object
currentUser: object
}, },
mixins: [History], mixins: [History],
@ -397,8 +396,7 @@ const PRRegisterPieceForm = React.createClass({
className="ascribe-form-bordered"> className="ascribe-form-bordered">
<Property <Property
name="terms" name="terms"
className="ascribe-property-collapsible-toggle" className="ascribe-property-collapsible-toggle">
style={{paddingBottom: 0}}>
<span> <span>
{getLangText('By submitting this form, you agree to the') + ' '} {getLangText('By submitting this form, you agree to the') + ' '}
<a <a

View File

@ -12,7 +12,7 @@ const PRHero = React.createClass({
propTypes: { propTypes: {
currentUser: React.PropTypes.shape({ currentUser: React.PropTypes.shape({
email: React.PropTypes.object email: React.PropTypes.object
}) }).isRequired
}, },
render() { render() {

View File

@ -3,45 +3,41 @@
import React from 'react'; import React from 'react';
import { History } from 'react-router'; import { History } from 'react-router';
import PrizeActions from '../../simple_prize/actions/prize_actions';
import PrizeStore from '../../simple_prize/stores/prize_store';
import Button from 'react-bootstrap/lib/Button'; import Button from 'react-bootstrap/lib/Button';
import ButtonGroup from 'react-bootstrap/lib/ButtonGroup'; import ButtonGroup from 'react-bootstrap/lib/ButtonGroup';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import UserStore from '../../../../../stores/user_store'; import PrizeActions from '../../simple_prize/actions/prize_actions';
import UserActions from '../../../../../actions/user_actions'; import PrizeStore from '../../simple_prize/stores/prize_store';
import { mergeOptions, omitFromObject } from '../../../../../utils/general_utils'; import { omitFromObject } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
const PRLanding = React.createClass({ const PRLanding = React.createClass({
propTypes: { propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
mixins: [History], mixins: [History],
getInitialState() { getInitialState() {
return mergeOptions( return PrizeStore.getState();
PrizeStore.getState(),
UserStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
const { location } = this.props; const { location } = this.props;
UserStore.listen(this.onChange);
PrizeStore.listen(this.onChange); PrizeStore.listen(this.onChange);
UserActions.fetchCurrentUser();
PrizeActions.fetchPrize(); PrizeActions.fetchPrize();
if (location && location.query && location.query.redirect) { if (location.query.redirect) {
window.setTimeout(() => this.history.replace({ window.setTimeout(() => this.history.replace({
pathname: `/${location.query.redirect}`, pathname: `/${location.query.redirect}`,
query: omitFromObject(location.query, ['redirect']) query: omitFromObject(location.query, ['redirect'])
@ -50,7 +46,6 @@ const PRLanding = React.createClass({
}, },
componentWillUnmount() { componentWillUnmount() {
UserStore.unlisten(this.onChange);
PrizeStore.unlisten(this.onChange); PrizeStore.unlisten(this.onChange);
}, },
@ -59,7 +54,7 @@ const PRLanding = React.createClass({
}, },
getButtons() { getButtons() {
if (this.state.prize && this.state.prize.active){ if (this.state.prize && this.state.prize.active) {
return ( return (
<ButtonGroup className="enter" bsSize="large" vertical> <ButtonGroup className="enter" bsSize="large" vertical>
<LinkContainer to="/signup"> <LinkContainer to="/signup">
@ -78,39 +73,37 @@ const PRLanding = React.createClass({
</LinkContainer> </LinkContainer>
</ButtonGroup> </ButtonGroup>
); );
} } else {
return ( return (
<ButtonGroup className="enter" bsSize="large" vertical> <ButtonGroup className="enter" bsSize="large" vertical>
<a className="btn btn-default" href="https://www.ascribe.io/app/signup"> <a className="btn btn-default" href="https://www.ascribe.io/app/signup">
{getLangText('Sign up to ascribe')} {getLangText('Sign up to ascribe')}
</a> </a>
<p> <p>
{getLangText('or, already an ascribe user?')} {getLangText('or, already an ascribe user?')}
</p> </p>
<LinkContainer to="/login"> <LinkContainer to="/login">
<Button> <Button>
{getLangText('Log in')} {getLangText('Log in')}
</Button> </Button>
</LinkContainer> </LinkContainer>
</ButtonGroup> </ButtonGroup>
); );
}
}, },
getTitle() { getTitle() {
if (this.state.prize && this.state.prize.active){ const { prize } = this.state;
return (
<p>
{getLangText('This is the submission page for Portfolio Review 2016.')}
</p>
);
}
return ( return (
<p> <p>
{getLangText('Submissions for Portfolio Review 2016 are now closed.')} {getLangText(prize && prize.active ? 'This is the submission page for Portfolio Review 2016.'
: 'Submissions for Portfolio Review 2016 are now closed.')}
</p> </p>
); );
}, },
render() { render() {
return ( return (
<div className="container"> <div className="container">

View File

@ -6,9 +6,6 @@ import { Link, History } from 'react-router';
import Col from 'react-bootstrap/lib/Col'; import Col from 'react-bootstrap/lib/Col';
import Row from 'react-bootstrap/lib/Row'; import Row from 'react-bootstrap/lib/Row';
import UserStore from '../../../../../stores/user_store';
import UserActions from '../../../../../actions/user_actions';
import PRRegisterPieceForm from './pr_forms/pr_register_piece_form'; import PRRegisterPieceForm from './pr_forms/pr_register_piece_form';
import { getLangText } from '../../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
@ -20,43 +17,31 @@ const { object } = React.PropTypes;
const PRRegisterPiece = React.createClass({ const PRRegisterPiece = React.createClass({
propTypes: { propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object,
// Provided from router
location: object location: object
}, },
mixins: [History], mixins: [History],
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentDidUpdate() { componentDidUpdate() {
const { currentUser } = this.state; const { currentUser } = this.props;
if(currentUser && currentUser.email) { if (currentUser.email) {
const submittedPieceId = getCookie(currentUser.email); const submittedPieceId = getCookie(currentUser.email);
if(submittedPieceId) { if (submittedPieceId) {
this.history.push(`/pieces/${submittedPieceId}`); this.history.push(`/pieces/${submittedPieceId}`);
} }
} }
}, },
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() { render() {
const { currentUser } = this.state; const { currentUser, location } = this.props;
const { location } = this.props;
setDocumentTitle(getLangText('Submit to Portfolio Review')); setDocumentTitle(getLangText('Submit to Portfolio Review'));
return ( return (
<Row> <Row>
<Col xs={6}> <Col xs={6}>
@ -77,7 +62,7 @@ const PRRegisterPiece = React.createClass({
<Col xs={6}> <Col xs={6}>
<PRRegisterPieceForm <PRRegisterPieceForm
location={location} location={location}
currentUser={currentUser}/> currentUser={currentUser} />
</Col> </Col>
</Row> </Row>
); );

View File

@ -1,90 +1,73 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import GlobalNotification from '../../../global_notification'; import classNames from 'classnames';
import Hero from './components/pr_hero';
import Header from '../../../header';
import EventActions from '../../../../actions/event_actions'; import EventActions from '../../../../actions/event_actions';
import UserStore from '../../../../stores/user_store'; import UserStore from '../../../../stores/user_store';
import UserActions from '../../../../actions/user_actions'; import UserActions from '../../../../actions/user_actions';
import Hero from './components/pr_hero';
import AppBase from '../../../app_base';
import AppRouteWrapper from '../../../app_route_wrapper';
import Footer from '../../../footer';
import Header from '../../../header';
import { getSubdomain } from '../../../../utils/general_utils'; import { getSubdomain } from '../../../../utils/general_utils';
import { getCookie } from '../../../../utils/fetch_api_utils'; import { getCookie } from '../../../../utils/fetch_api_utils';
let PRApp = React.createClass({ let PRApp = React.createClass({
propTypes: { propTypes: {
children: React.PropTypes.oneOfType([ activeRoute: React.PropTypes.object.isRequired,
React.PropTypes.arrayOf(React.PropTypes.element), children: React.PropTypes.element.isRequired,
React.PropTypes.element history: React.PropTypes.object.isRequired,
]), routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
history: React.PropTypes.object,
routes: React.PropTypes.arrayOf(React.PropTypes.object)
},
getInitialState() { // Provided from AppBase
return UserStore.getState(); currentUser: React.PropTypes.object,
}, whitelabel: React.PropTypes.object
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
if (this.state.currentUser && this.state.currentUser.email) {
EventActions.profileDidLoad.defer(this.state.currentUser);
}
},
componentWillUpdate(nextProps, nextState) {
const { currentUser: { email: curEmail } = {} } = this.state;
const { currentUser: { email: nextEmail } = {} } = nextState;
if (nextEmail && curEmail !== nextEmail) {
EventActions.profileDidLoad.defer(nextState.currentUser);
}
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
}, },
render() { render() {
const { history, children, routes } = this.props; const { activeRoute, children, currentUser, history, routes, whitelabel } = this.props;
const { currentUser } = this.state; const subdomain = getSubdomain();
const path = activeRoute && activeRoute.path;
let style = {}; let style = {};
let subdomain = getSubdomain();
let header; let header;
if (currentUser && currentUser.email && history.isActive(`/pieces/${getCookie(currentUser.email)}`)) { if (currentUser && currentUser.email && history.isActive(`/pieces/${getCookie(currentUser.email)}`)) {
header = <Hero currentUser={currentUser} />; header = (<Hero currentUser={currentUser} />);
style = { paddingTop: '0 !important' }; style = { paddingTop: '0 !important' };
} else if(currentUser && (currentUser.is_admin || currentUser.is_jury || currentUser.is_judge)) { } else if (currentUser && (currentUser.is_admin || currentUser.is_jury || currentUser.is_judge)) {
header = <Header routes={routes} />; header = (
<Header
currentUser={currentUser}
routes={routes}
whitelabel={whitelabel}
/>
);
} else { } else {
style = { paddingTop: '0 !important' }; style = { paddingTop: '0 !important' };
} }
return ( return (
<div> <div
style={style}
className={classNames('ascribe-app', 'ascribe-prize-app', `route--${(path ? path.split('/')[0] : 'landing')}`)}>
{header} {header}
<div <AppRouteWrapper
style={style} currentUser={currentUser}
className={'container ascribe-prize-app client--' + subdomain}> whitelabel={whitelabel}>
{/* Routes are injected here */}
{children} {children}
<GlobalNotification /> </AppRouteWrapper>
<div id="modal" className="container"></div> <Footer activeRoute={activeRoute} />
</div>
</div> </div>
); );
} }
}); });
export default PRApp; export default AppBase(PRApp);

View File

@ -31,74 +31,116 @@ import { AuthPrizeRoleRedirect } from './portfolioreview/components/pr_routes/pr
const ROUTES = { const ROUTES = {
sluice: ( sluice: (
<Route path='/' component={SPApp}> <Route path='/' component={SPApp}>
<IndexRoute component={SPLanding} /> <IndexRoute
component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SPLanding)}
hideFooter />
<Route <Route
path='login' path='login'
component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SPLoginContainer)} /> component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SPLoginContainer)}
hideFooter />
<Route <Route
path='logout' path='logout'
component={ProxyHandler(AuthRedirect({to: '/', when: 'loggedOut'}))(LogoutContainer)}/> component={ProxyHandler(AuthRedirect({to: '/', when: 'loggedOut'}))(LogoutContainer)}
hideFooter />
<Route <Route
path='signup' path='signup'
component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SPSignupContainer)} /> component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SPSignupContainer)}
hideFooter />
<Route <Route
path='password_reset' path='password_reset'
component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(PasswordResetContainer)} /> component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(PasswordResetContainer)}
hideFooter />
<Route <Route
path='settings' path='settings'
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPSettingsContainer)}/> component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPSettingsContainer)}
hideFooter />
<Route <Route
path='register_piece' path='register_piece'
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPRegisterPiece)} component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPRegisterPiece)}
headerTitle='+ NEW WORK'/> headerTitle='+ NEW WORK'
hideFooter />
<Route <Route
path='collection' path='collection'
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPPieceList)} component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPPieceList)}
headerTitle='COLLECTION'/> headerTitle='COLLECTION'
<Route path='pieces/:pieceId' component={SluicePieceContainer} /> hideFooter />
<Route path='editions/:editionId' component={EditionContainer} /> <Route
<Route path='verify' component={CoaVerifyContainer} /> path='pieces/:pieceId'
<Route path='*' component={ErrorNotFoundPage} /> component={SluicePieceContainer}
hideFooter />
<Route
path='editions/:editionId'
component={EditionContainer}
hideFooter />
<Route
path='coa_verify'
component={CoaVerifyContainer}
hideFooter />
<Route
path='*'
component={ErrorNotFoundPage}
hideFooter />
</Route> </Route>
), ),
portfolioreview: ( portfolioreview: (
<Route path='/' component={PRApp}> <Route path='/' component={PRApp}>
<IndexRoute component={ProxyHandler(AuthPrizeRoleRedirect({ to: '/collection', when: ['is_admin', 'is_judge', 'is_jury'] }))(PRLanding)} /> <IndexRoute
component={ProxyHandler(AuthPrizeRoleRedirect({ to: '/collection', when: ['is_admin', 'is_judge', 'is_jury'] }))(PRLanding)}
hideFooter />
<Route <Route
path='register_piece' path='register_piece'
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(PRRegisterPiece)}/> component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(PRRegisterPiece)}
hideFooter />
<Route <Route
path='collection' path='collection'
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPPieceList)} component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPPieceList)}
headerTitle='SUBMISSIONS'/> headerTitle='SUBMISSIONS'
hideFooter />
<Route <Route
path='login' path='login'
component={ProxyHandler( component={ProxyHandler(
AuthPrizeRoleRedirect({ to: '/collection', when: ['is_admin', 'is_judge', 'is_jury'] }), AuthPrizeRoleRedirect({ to: '/collection', when: ['is_admin', 'is_judge', 'is_jury'] }),
AuthRedirect({to: '/register_piece', when: 'loggedIn'}) AuthRedirect({to: '/register_piece', when: 'loggedIn'})
)(SPLoginContainer)} /> )(SPLoginContainer)}
hideFooter />
<Route <Route
path='logout' path='logout'
component={ProxyHandler(AuthRedirect({to: '/', when: 'loggedOut'}))(LogoutContainer)} /> component={ProxyHandler(AuthRedirect({to: '/', when: 'loggedOut'}))(LogoutContainer)}
hideFooter />
<Route <Route
path='signup' path='signup'
component={ProxyHandler( component={ProxyHandler(
AuthPrizeRoleRedirect({ to: '/collection', when: ['is_admin', 'is_judge', 'is_jury'] }), AuthPrizeRoleRedirect({ to: '/collection', when: ['is_admin', 'is_judge', 'is_jury'] }),
AuthRedirect({to: '/register_piece', when: 'loggedIn'}) AuthRedirect({to: '/register_piece', when: 'loggedIn'})
)(SPSignupContainer)} /> )(SPSignupContainer)}
hideFooter />
<Route <Route
path='password_reset' path='password_reset'
component={ProxyHandler( component={ProxyHandler(
AuthPrizeRoleRedirect({ to: '/collection', when: ['is_admin', 'is_judge', 'is_jury'] }), AuthPrizeRoleRedirect({ to: '/collection', when: ['is_admin', 'is_judge', 'is_jury'] }),
AuthRedirect({to: '/register_piece', when: 'loggedIn'}) AuthRedirect({to: '/register_piece', when: 'loggedIn'})
)(PasswordResetContainer)} /> )(PasswordResetContainer)}
hideFooter />
<Route <Route
path='settings' path='settings'
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPSettingsContainer)}/> component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPSettingsContainer)}
<Route path='pieces/:pieceId' component={SPPieceContainer} /> hideFooter />
<Route path='editions/:editionId' component={EditionContainer} /> <Route
<Route path='verify' component={CoaVerifyContainer} /> path='pieces/:pieceId'
<Route path='*' component={ErrorNotFoundPage} /> component={SPPieceContainer}
hideFooter />
<Route
path='editions/:editionId'
component={EditionContainer}
hideFooter />
<Route
path='coa_verify'
component={CoaVerifyContainer}
hideFooter />
<Route
path='*'
component={ErrorNotFoundPage}
hideFooter />
</Route> </Route>
) )
}; };

View File

@ -10,8 +10,6 @@ import PieceListStore from '../../../../../../stores/piece_list_store';
import PrizeRatingActions from '../../actions/prize_rating_actions'; import PrizeRatingActions from '../../actions/prize_rating_actions';
import UserStore from '../../../../../../stores/user_store';
import InputCheckbox from '../../../../../ascribe_forms/input_checkbox'; import InputCheckbox from '../../../../../ascribe_forms/input_checkbox';
import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece'; import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece';
@ -23,34 +21,30 @@ import AclProxy from '../../../../../acl_proxy';
import SubmitToPrizeButton from './../ascribe_buttons/submit_to_prize_button'; import SubmitToPrizeButton from './../ascribe_buttons/submit_to_prize_button';
import { getLangText } from '../../../../../../utils/lang_utils'; import { getLangText } from '../../../../../../utils/lang_utils';
import { mergeOptions } from '../../../../../../utils/general_utils';
let AccordionListItemPrize = React.createClass({ let AccordionListItemPrize = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, content: React.PropTypes.object.isRequired,
content: React.PropTypes.object, currentUser: React.PropTypes.object.isRequired,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element React.PropTypes.element
]) ]),
className: React.PropTypes.string
}, },
getInitialState() { getInitialState() {
return mergeOptions( return PieceListStore.getState();
PieceListStore.getState(),
UserStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
PieceListStore.listen(this.onChange); PieceListStore.listen(this.onChange);
UserStore.listen(this.onChange);
}, },
componentWillUnmount() { componentWillUnmount() {
PieceListStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
@ -62,29 +56,30 @@ let AccordionListItemPrize = React.createClass({
PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy });
let notification = new GlobalNotificationModel(response.notification, 'success', 10000); const notification = new GlobalNotificationModel(response.notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
getPrizeButtons() { getPrizeButtons() {
if (this.state.currentUser && this.state.currentUser.is_jury){ const { currentUser, content: { id, ratings } } = this.props;
if ((this.props.content.ratings) &&
(this.props.content.ratings.rating || this.props.content.ratings.average)){ if (currentUser && (currentUser.is_jury || currentUser.is_judge)) {
if (ratings && (ratings.rating || ratings.average)) {
// jury and rating available // jury and rating available
let rating = null, let rating = null;
caption = null; let caption = null;
if (this.props.content.ratings.rating){
rating = parseInt(this.props.content.ratings.rating, 10); if (ratings.rating) {
rating = parseInt(ratings.rating, 10);
caption = getLangText('Your rating'); caption = getLangText('Your rating');
} } else if (ratings.average) {
else if (this.props.content.ratings.average){ rating = ratings.average;
rating = this.props.content.ratings.average; caption = getLangText('Average of ' + ratings.num_ratings + ' rating(s)');
caption = getLangText('Average of ' + this.props.content.ratings.num_ratings + ' rating(s)');
} }
return ( return (
<div id="list-rating" className="pull-right"> <div id="list-rating" className="pull-right">
<Link to={`/pieces/${this.props.content.id}`}> <Link to={`/pieces/${id}`}>
<StarRating <StarRating
ref='rating' ref='rating'
name="prize-rating" name="prize-rating"
@ -94,47 +89,46 @@ let AccordionListItemPrize = React.createClass({
rating={rating} rating={rating}
ratingAmount={5} /> ratingAmount={5} />
</Link> </Link>
</div>); </div>
} );
else { } else {
if (this.state.currentUser.is_judge){ if (currentUser.is_judge) {
return ( return (
<div className="react-rating-caption pull-right"> <div className="react-rating-caption pull-right">
{getLangText('Not rated')} {getLangText('Not rated')}
</div> </div>
); );
} else {
// jury and no rating yet
return (
<div className="react-rating-caption pull-right">
<Link to={`/pieces/${id}`}>
{getLangText('Submit your rating')}
</Link>
</div>
);
} }
// jury and no rating yet
return (
<div className="react-rating-caption pull-right">
<Link to={`/pieces/${this.props.content.id}`}>
{getLangText('Submit your rating')}
</Link>
</div>
);
} }
} else {
return this.getPrizeButtonsParticipant();
} }
return this.getPrizeButtonsParticipant();
}, },
getPrizeButtonsParticipant() { getPrizeButtonsParticipant() {
return ( return (
<div> <AclProxy
<AclProxy aclObject={this.props.content.acl}
aclObject={this.props.content.acl} aclName="acl_wallet_submit">
aclName="acl_wallet_submit"> <SubmitToPrizeButton
<SubmitToPrizeButton className="pull-right"
className="pull-right" piece={this.props.content}
piece={this.props.content} handleSuccess={this.handleSubmitPrizeSuccess} />
handleSuccess={this.handleSubmitPrizeSuccess}/> </AclProxy>
</AclProxy>
</div>
); );
}, },
handleShortlistSuccess(message){ handleShortlistSuccess(message) {
let notification = new GlobalNotificationModel(message, 'success', 2000); const notification = new GlobalNotificationModel(message, 'success', 2000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
@ -144,56 +138,52 @@ let AccordionListItemPrize = React.createClass({
PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy });
}, },
onSelectChange(){ onSelectChange() {
PrizeRatingActions.toggleShortlist(this.props.content.id) PrizeRatingActions.toggleShortlist(this.props.content.id)
.then( .then((res) => {
(res) => { this.refreshPieceData();
this.refreshPieceData(); this.handleShortlistSuccess(res.notification);
return res; });
})
.then(
(res) => {
this.handleShortlistSuccess(res.notification);
}
);
}, },
getPrizeBadge(){ getPrizeBadge() {
if (this.state.currentUser && this.state.currentUser.is_judge) { const { currentUser } = this.props;
if (currentUser && currentUser.is_judge) {
return ( return (
<span className="pull-right ascribe-checkbox-wrapper ascribe-checkbox-badge"> <span className="pull-right ascribe-checkbox-wrapper ascribe-checkbox-badge">
<InputCheckbox <InputCheckbox
defaultChecked={this.props.content.selected} defaultChecked={this.props.content.selected}
onChange={this.onSelectChange}/> onChange={this.onSelectChange} />
</span> </span>
); );
} else {
return null;
} }
return null;
}, },
render() { render() {
const { children, className, content } = this.props; const { children, className, content, currentUser } = 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 // Only show the artist name if you are the participant or if you are a judge and the piece is shortlisted
let artistName = ((currentUser.is_jury && !currentUser.is_judge) || (currentUser.is_judge && !content.selected )) ? const artistName = ((currentUser.is_jury && !currentUser.is_judge) || (currentUser.is_judge && !content.selected )) ?
<span className="glyphicon glyphicon-eye-close" aria-hidden="true"/> : content.artist_name; <span className="glyphicon glyphicon-eye-close" aria-hidden="true"/> : content.artist_name;
return ( return (
<div> <AccordionListItemPiece
<AccordionListItemPiece className={className}
className={className} piece={content}
piece={content} artistName={artistName}
artistName={artistName} subsubheading={
subsubheading={ <div>
<div> <span>{Moment(content.date_created, 'YYYY-MM-DD').year()}</span>
<span>{Moment(content.date_created, 'YYYY-MM-DD').year()}</span> </div>
</div>} }
buttons={this.getPrizeButtons()} buttons={this.getPrizeButtons()}
badge={this.getPrizeBadge()}> badge={this.getPrizeBadge()}>
{children} {children}
</AccordionListItemPiece> </AccordionListItemPiece>
</div>
); );
} }
}); });

View File

@ -14,13 +14,10 @@ import PrizeStore from '../../stores/prize_store';
import PrizeRatingActions from '../../actions/prize_rating_actions'; import PrizeRatingActions from '../../actions/prize_rating_actions';
import PrizeRatingStore from '../../stores/prize_rating_store'; 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 PieceListStore from '../../../../../../stores/piece_list_store';
import PieceListActions from '../../../../../../actions/piece_list_actions'; import PieceListActions from '../../../../../../actions/piece_list_actions';
import PieceActions from '../../../../../../actions/piece_actions';
import UserStore from '../../../../../../stores/user_store'; import PieceStore from '../../../../../../stores/piece_store';
import UserActions from '../../../../../../actions/user_actions';
import Piece from '../../../../../../components/ascribe_detail/piece'; import Piece from '../../../../../../components/ascribe_detail/piece';
import Note from '../../../../../../components/ascribe_detail/note'; import Note from '../../../../../../components/ascribe_detail/note';
@ -53,24 +50,26 @@ import { setDocumentTitle } from '../../../../../../utils/dom_utils';
*/ */
let PrizePieceContainer = React.createClass({ let PrizePieceContainer = React.createClass({
propTypes: { propTypes: {
params: React.PropTypes.object, selectedPrizeActionButton: React.PropTypes.func,
selectedPrizeActionButton: React.PropTypes.func
// Provided from PrizeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object,
params: React.PropTypes.object
}, },
mixins: [ReactError], mixins: [ReactError],
getInitialState() { getInitialState() {
return mergeOptions( return PieceStore.getInitialState();
PieceStore.getInitialState(),
UserStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
PieceStore.listen(this.onChange); PieceStore.listen(this.onChange);
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
this.loadPiece(); this.loadPiece();
}, },
@ -94,7 +93,6 @@ let PrizePieceContainer = React.createClass({
componentWillUnmount() { componentWillUnmount() {
PieceStore.unlisten(this.onChange); PieceStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
@ -102,7 +100,8 @@ let PrizePieceContainer = React.createClass({
}, },
getActions() { getActions() {
const { currentUser, piece } = this.state; const { currentUser } = this.props;
const { piece } = this.state;
if (piece.notifications && piece.notifications.length > 0) { if (piece.notifications && piece.notifications.length > 0) {
return ( return (
@ -119,8 +118,8 @@ let PrizePieceContainer = React.createClass({
}, },
render() { render() {
const { selectedPrizeActionButton } = this.props; const { currentUser, selectedPrizeActionButton } = this.props;
const { currentUser, piece } = this.state; const { piece } = this.state;
if (piece.id) { if (piece.id) {
/* /*

View File

@ -1,57 +1,47 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import { History } from 'react-router';
import PrizeActions from '../actions/prize_actions';
import PrizeStore from '../stores/prize_store';
import Button from 'react-bootstrap/lib/Button'; import Button from 'react-bootstrap/lib/Button';
import ButtonGroup from 'react-bootstrap/lib/ButtonGroup'; import ButtonGroup from 'react-bootstrap/lib/ButtonGroup';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import UserStore from '../../../../../stores/user_store'; import PrizeActions from '../actions/prize_actions';
import UserActions from '../../../../../actions/user_actions'; import PrizeStore from '../stores/prize_store';
import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
let Landing = React.createClass({
mixins: [History], let Landing = React.createClass({
propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object
},
getInitialState() { getInitialState() {
return mergeOptions( return PrizeStore.getState();
PrizeStore.getState(),
UserStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
PrizeStore.listen(this.onChange); PrizeStore.listen(this.onChange);
PrizeActions.fetchPrize(); PrizeActions.fetchPrize();
}, },
componentWillUnmount() { componentWillUnmount() {
UserStore.unlisten(this.onChange);
PrizeStore.unlisten(this.onChange); PrizeStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
this.setState(state); this.setState(state);
// 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.replace('/collection'), 0);
}
}, },
getButtons() { getButtons() {
if (this.state.prize && this.state.prize.active){ if (this.state.prize && this.state.prize.active) {
return ( return (
<ButtonGroup className="enter" bsSize="large" vertical> <ButtonGroup className="enter" bsSize="large" vertical>
<LinkContainer to="/signup"> <LinkContainer to="/signup">
@ -70,39 +60,37 @@ let Landing = React.createClass({
</LinkContainer> </LinkContainer>
</ButtonGroup> </ButtonGroup>
); );
} } else {
return ( return (
<ButtonGroup className="enter" bsSize="large" vertical> <ButtonGroup className="enter" bsSize="large" vertical>
<a className="btn btn-default" href="https://www.ascribe.io/app/signup"> <a className="btn btn-default" href="https://www.ascribe.io/app/signup">
{getLangText('Sign up to ascribe')} {getLangText('Sign up to ascribe')}
</a> </a>
<p> <p>
{getLangText('or, already an ascribe user?')} {getLangText('or, already an ascribe user?')}
</p> </p>
<LinkContainer to="/login"> <LinkContainer to="/login">
<Button> <Button>
{getLangText('Log in')} {getLangText('Log in')}
</Button> </Button>
</LinkContainer> </LinkContainer>
</ButtonGroup> </ButtonGroup>
); );
}
}, },
getTitle() { getTitle() {
if (this.state.prize && this.state.prize.active){ const { prize } = this.state;
return (
<p>
{getLangText('This is the submission page for Sluice_screens ↄc Prize 2015.')}
</p>
);
}
return ( return (
<p> <p>
{getLangText('Submissions for Sluice_screens ↄc Prize 2015 are now closed.')} {getLangText(prize && prize.active ? 'This is the submission page for Sluice_screens ↄc Prize 2015.'
: 'Submissions for Sluice_screens ↄc Prize 2015 are now closed.')}
</p> </p>
); );
}, },
render() { render() {
return ( return (
<div className="container"> <div className="container">

View File

@ -11,6 +11,11 @@ import { setDocumentTitle } from '../../../../../utils/dom_utils';
let LoginContainer = React.createClass({ let LoginContainer = React.createClass({
propTypes: { propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
@ -21,12 +26,11 @@ let LoginContainer = React.createClass({
<div className="ascribe-login-wrapper"> <div className="ascribe-login-wrapper">
<LoginForm <LoginForm
headerMessage={getLangText('Log in with ascribe')} headerMessage={getLangText('Log in with ascribe')}
location={this.props.location}/> location={this.props.location} />
<div <div className="ascribe-login-text">
className="ascribe-login-text">
{getLangText('I\'m not a user') + ' '} {getLangText('I\'m not a user') + ' '}
<Link to="/signup">{getLangText('Sign up...')}</Link> <Link to="/signup">{getLangText('Sign up...')}</Link>
<br/> <br />
{getLangText('I forgot my password') + ' '} {getLangText('I forgot my password') + ' '}
<Link to="/password_reset">{getLangText('Rescue me...')}</Link> <Link to="/password_reset">{getLangText('Rescue me...')}</Link>

View File

@ -1,19 +1,15 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PieceList from '../../../../piece_list';
import UserActions from '../../../../../actions/user_actions'; import Button from 'react-bootstrap/lib/Button';
import UserStore from '../../../../../stores/user_store'; import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import PrizeActions from '../actions/prize_actions'; import PrizeActions from '../actions/prize_actions';
import PrizeStore from '../stores/prize_store'; import PrizeStore from '../stores/prize_store';
import Button from 'react-bootstrap/lib/Button';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import AccordionListItemPrize from './ascribe_accordion_list/accordion_list_item_prize'; import AccordionListItemPrize from './ascribe_accordion_list/accordion_list_item_prize';
import PieceList from '../../../../piece_list';
import { mergeOptions } from '../../../../../utils/general_utils'; import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
@ -21,25 +17,24 @@ import { setDocumentTitle } from '../../../../../utils/dom_utils';
let PrizePieceList = React.createClass({ let PrizePieceList = React.createClass({
propTypes: { propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
getInitialState() { getInitialState() {
return mergeOptions( return PrizeStore.getState();
PrizeStore.getState(),
UserStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
PrizeStore.listen(this.onChange); PrizeStore.listen(this.onChange);
PrizeActions.fetchPrize(); PrizeActions.fetchPrize();
}, },
componentWillUnmount() { componentWillUnmount() {
UserStore.unlisten(this.onChange);
PrizeStore.unlisten(this.onChange); PrizeStore.unlisten(this.onChange);
}, },
@ -48,7 +43,8 @@ let PrizePieceList = React.createClass({
}, },
getButtonSubmit() { getButtonSubmit() {
const { currentUser, prize } = this.state; const { currentUser } = this.props;
const { prize } = this.state;
if (prize && prize.active && !currentUser.is_jury && !currentUser.is_admin && !currentUser.is_judge) { if (prize && prize.active && !currentUser.is_jury && !currentUser.is_admin && !currentUser.is_judge) {
return ( return (
<LinkContainer to="/register_piece"> <LinkContainer to="/register_piece">
@ -57,31 +53,40 @@ let PrizePieceList = React.createClass({
</Button> </Button>
</LinkContainer> </LinkContainer>
); );
} else {
return null;
} }
return null; },
shouldRedirect(pieceCount) {
const { currentUser } = this.props;
return !currentUser.is_admin && !currentUser.is_jury && !currentUser.is_judge && !pieceCount;
}, },
render() { render() {
const { currentUser, location } = this.props;
setDocumentTitle(getLangText('Collection')); setDocumentTitle(getLangText('Collection'));
let orderParams = ['artist_name', 'title']; let orderParams = ['artist_name', 'title'];
if (this.state.currentUser.is_jury) { if (currentUser.is_jury) {
orderParams = ['rating', 'title']; orderParams = ['rating', 'title'];
} }
if (this.state.currentUser.is_judge) { if (currentUser.is_judge) {
orderParams = ['rating', 'title', 'selected']; orderParams = ['rating', 'title', 'selected'];
} }
return ( return (
<div> <PieceList
<PieceList ref="list"
ref="list" {...this.props}
accordionListItemType={AccordionListItemPrize} accordionListItemType={AccordionListItemPrize}
orderParams={orderParams} customSubmitButton={this.getButtonSubmit()}
orderBy={this.state.currentUser.is_jury ? 'rating' : null} filterParams={[]}
filterParams={[]} orderParams={orderParams}
customSubmitButton={this.getButtonSubmit()} orderBy={currentUser.is_jury ? 'rating' : null}
location={this.props.location}/> shouldRedirect={this.shouldRedirect} />
</div>
); );
} }
}); });

View File

@ -16,6 +16,11 @@ import { setDocumentTitle } from '../../../../../utils/dom_utils';
let PrizeRegisterPiece = React.createClass({ let PrizeRegisterPiece = React.createClass({
propTypes: { propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
@ -37,63 +42,59 @@ let PrizeRegisterPiece = React.createClass({
}, },
render() { render() {
const { location } = this.props; const { prize } = this.state;
setDocumentTitle(getLangText('Submit to the prize')); setDocumentTitle(getLangText('Submit to the prize'));
if(this.state.prize && this.state.prize.active){ if (prize && prize.active) {
return ( return (
<div> <RegisterPiece
<RegisterPiece {...this.props}
enableLocalHashing={false} enableLocalHashing={false}
headerMessage={''} headerMessage={''}
submitMessage={getLangText('Submit')} submitMessage={getLangText('Submit')}>
location={location}> <Property
<Property name='artist_statement'
name='artist_statement' label={getLangText('Artist statement')}
label={getLangText('Artist statement')} editable={true}
editable={true} overrideForm={true}>
overrideForm={true}> <InputTextAreaToggable
<InputTextAreaToggable rows={1}
rows={1} placeholder={getLangText('Enter your statement')}
placeholder={getLangText('Enter your statement')} required />
required /> </Property>
</Property> <Property
<Property name='work_description'
name='work_description' label={getLangText('Work description')}
label={getLangText('Work description')} editable={true}
editable={true} overrideForm={true}>
overrideForm={true}> <InputTextAreaToggable
<InputTextAreaToggable rows={1}
rows={1} placeholder={getLangText('Enter the description for your work')}
placeholder={getLangText('Enter the description for your work')} required />
required /> </Property>
</Property> <Property
<Property name="terms"
name="terms" className="ascribe-property-collapsible-toggle">
className="ascribe-property-collapsible-toggle" <InputCheckbox>
style={{paddingBottom: 0}}> <span>
<InputCheckbox> {' ' + getLangText('I agree to the Terms of Service the art price') + ' '}
<span> (<a href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/sluice/terms.pdf" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}>
{' ' + getLangText('I agree to the Terms of Service the art price') + ' '} {getLangText('read')}
(<a href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/sluice/terms.pdf" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}> </a>)
{getLangText('read')} </span>
</a>) </InputCheckbox>
</span> </Property>
</InputCheckbox> </RegisterPiece>
</Property>
</RegisterPiece>
</div>
); );
} } else {
else {
return ( return (
<div className='row'> <div className='row'>
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
{getLangText('The prize is no longer active')} {getLangText('The prize is no longer active')}
</div> </div>
</div> </div>
); );
} }
} }
}); });

View File

@ -2,8 +2,6 @@
import React from 'react'; import React from 'react';
import UserStore from '../../../../../stores/user_store';
import UserActions from '../../../../../actions/user_actions';
import PrizeActions from '../actions/prize_actions'; import PrizeActions from '../actions/prize_actions';
import PrizeStore from '../stores/prize_store'; import PrizeStore from '../stores/prize_store';
import PrizeJuryActions from '../actions/prize_jury_actions'; import PrizeJuryActions from '../actions/prize_jury_actions';
@ -28,40 +26,27 @@ import { setDocumentTitle } from '../../../../../utils/dom_utils';
let Settings = React.createClass({ let Settings = React.createClass({
getInitialState() { propTypes: {
return UserStore.getState(); // Provided from PrizeApp
}, currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object,
componentDidMount() { // Provided from router
UserStore.listen(this.onChange); location: React.PropTypes.object
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
}, },
render() { render() {
setDocumentTitle(getLangText('Account settings')); setDocumentTitle(getLangText('Account settings'));
let prizeSettings = null;
if (this.state.currentUser.is_admin){
prizeSettings = <PrizeSettings />;
}
return ( return (
<SettingsContainer> <SettingsContainer {...this.props}>
{prizeSettings} {this.props.currentUser.is_admin ? <PrizeSettings /> : null}
</SettingsContainer> </SettingsContainer>
); );
} }
}); });
let PrizeSettings = React.createClass({ let PrizeSettings = React.createClass({
getInitialState() { getInitialState() {
return PrizeStore.getState(); return PrizeStore.getState();
}, },

View File

@ -8,6 +8,11 @@ import { setDocumentTitle } from '../../../../../utils/dom_utils';
let SignupContainer = React.createClass({ let SignupContainer = React.createClass({
propTypes: { propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
@ -18,7 +23,7 @@ let SignupContainer = React.createClass({
}; };
}, },
handleSuccess(message){ handleSuccess(message) {
this.setState({ this.setState({
submitted: true, submitted: true,
message: message message: message
@ -26,13 +31,15 @@ let SignupContainer = React.createClass({
}, },
render() { render() {
const { location } = this.props;
const { message, submitted } = this.state;
setDocumentTitle(getLangText('Sign up')); setDocumentTitle(getLangText('Sign up'));
if (this.state.submitted){ if (submitted) {
return ( return (
<div className="ascribe-login-wrapper"> <div className="ascribe-login-wrapper">
<div className="ascribe-login-text ascribe-login-header"> <div className="ascribe-login-text ascribe-login-header">
{this.state.message} {message}
</div> </div>
</div> </div>
); );
@ -43,7 +50,7 @@ let SignupContainer = React.createClass({
headerMessage={getLangText('Create account for submission')} headerMessage={getLangText('Create account for submission')}
submitMessage={getLangText('Sign up')} submitMessage={getLangText('Sign up')}
handleSuccess={this.handleSuccess} handleSuccess={this.handleSuccess}
location={this.props.location}/> location={location} />
</div> </div>
); );
} }

View File

@ -1,50 +1,61 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import Hero from './components/prize_hero'; import Hero from './components/prize_hero';
import Header from '../../../header';
import AppBase from '../../../app_base';
import AppRouteWrapper from '../../../app_route_wrapper';
import Footer from '../../../footer'; import Footer from '../../../footer';
import GlobalNotification from '../../../global_notification'; import Header from '../../../header';
import { getSubdomain } from '../../../../utils/general_utils'; import { getSubdomain } from '../../../../utils/general_utils';
let PrizeApp = React.createClass({ let PrizeApp = React.createClass({
propTypes: { propTypes: {
children: React.PropTypes.oneOfType([ activeRoute: React.PropTypes.object.isRequired,
React.PropTypes.arrayOf(React.PropTypes.element), children: React.PropTypes.element.isRequired,
React.PropTypes.element history: React.PropTypes.object.isRequired,
]), routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
history: React.PropTypes.object,
routes: React.PropTypes.arrayOf(React.PropTypes.object) // Provided from AppBase
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object
}, },
render() { render() {
const { history, routes } = this.props; const { activeRoute, children, currentUser, history, routes, whitelabel } = this.props;
const subdomain = getSubdomain();
const path = activeRoute && activeRoute.path;
let header = null; let header = null;
let subdomain = getSubdomain();
// The second element of routes is always the active component object, where we can
// extract the path.
let path = routes[1] ? routes[1].path : null;
// if the path of the current activeRoute is not defined, then this is the IndexRoute // if the path of the current activeRoute is not defined, then this is the IndexRoute
if (!path || history.isActive('/login') || history.isActive('/signup')) { if (!path || history.isActive('/login') || history.isActive('/signup')) {
header = <Hero />; header = (<Hero />);
} else { } else {
header = <Header routes={routes}/>; header = (
<Header
currentUser={currentUser}
routes={routes}
whitelabel={whitelabel} />
);
} }
return ( return (
<div className={'container ascribe-prize-app client--' + subdomain}> <div className={classNames('ascribe-app', 'ascribe-prize-app', `route--${(path ? path.split('/')[0] : 'landing')}`)}>
{header} {header}
{this.props.children} <AppRouteWrapper
<GlobalNotification /> currentUser={currentUser}
<div id="modal" className="container"></div> whitelabel={whitelabel}>
<Footer /> {/* Routes are injected here */}
{children}
</AppRouteWrapper>
<Footer activeRoute={activeRoute} />
</div> </div>
); );
} }
}); });
export default PrizeApp; export default AppBase(PrizeApp);

View File

@ -1,4 +1,4 @@
'use strict' 'use strict';
import React from 'react'; import React from 'react';
import Moment from 'moment'; import Moment from 'moment';
@ -16,8 +16,9 @@ import { getLangText } from '../../../../../../utils/lang_utils';
const SluiceSelectedPrizeActionButton = React.createClass({ const SluiceSelectedPrizeActionButton = React.createClass({
propTypes: { propTypes: {
piece: React.PropTypes.object, currentUser: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object, piece: React.PropTypes.object.isRequired,
startLoanDate: React.PropTypes.object, startLoanDate: React.PropTypes.object,
endLoanDate: React.PropTypes.object, endLoanDate: React.PropTypes.object,
className: React.PropTypes.string, className: React.PropTypes.string,
@ -38,7 +39,7 @@ const SluiceSelectedPrizeActionButton = React.createClass({
// Can't use default props since those are only created once // Can't use default props since those are only created once
const startLoanDate = this.props.startLoanDate || new Moment(); const startLoanDate = this.props.startLoanDate || new Moment();
const endLoanDate = this.props.endLoanDate || (new Moment()).add(6, 'months'); const endLoanDate = this.props.endLoanDate || new Moment().add(6, 'months');
return ( return (
<ModalWrapper <ModalWrapper

View File

@ -8,6 +8,12 @@ import PrizePieceContainer from '../../../simple_prize/components/ascribe_detail
const SluicePieceContainer = React.createClass({ const SluicePieceContainer = React.createClass({
propTypes: { propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object,
params: React.PropTypes.object params: React.PropTypes.object
}, },

View File

@ -1,4 +1,4 @@
'use strict' 'use strict';
import React from 'react'; import React from 'react';

View File

@ -5,44 +5,38 @@ import React from 'react';
import Button from 'react-bootstrap/lib/Button'; import Button from 'react-bootstrap/lib/Button';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import WhitelabelActions from '../../../../../actions/whitelabel_actions';
import WhitelabelStore from '../../../../../stores/whitelabel_store';
import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils'; import { setDocumentTitle } from '../../../../../utils/dom_utils';
let Vivi23Landing = React.createClass({ let Vivi23Landing = React.createClass({
getInitialState() { propTypes: {
return WhitelabelStore.getState(); customThumbnailPlaceholder: React.PropTypes.func,
// Provided from WalletApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object.isRequired,
// Provided from router
location: React.PropTypes.object
}, },
componentWillMount() { componentWillMount() {
setDocumentTitle('23VIVI Marketplace'); setDocumentTitle('23VIVI Marketplace');
}, },
componentDidMount() {
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
},
componentWillUnmount() {
WhitelabelStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() { render() {
return ( return (
<div className="container ascribe-form-wrapper vivi23-landing"> <div className="ascribe-form-wrapper vivi23-landing">
<div className="row"> <div className="row">
<div className="col-xs-12"> <div className="col-xs-12">
<div className="row vivi23-landing--header"> <div className="row vivi23-landing--header">
<img className="vivi23-landing--header-logo" src={this.state.whitelabel.logo} /> <img
className="vivi23-landing--header-logo"
src={this.props.whitelabel.logo}
height="75" />
<div> <div>
{getLangText('Artwork from the 23VIVI Marketplace is powered by') + ' '} {getLangText('23VIVI Marketplace is powered by') + ' '}
<span className="icon-ascribe-logo" /> <span className="icon-ascribe-logo" />
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
'use strict' 'use strict';
import React from 'react'; import React from 'react';
@ -8,17 +8,21 @@ import MarketPieceList from '../market/market_piece_list';
let Vivi23PieceList = React.createClass({ let Vivi23PieceList = React.createClass({
propTypes: { propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
render() { render() {
return ( return (
<MarketPieceList <MarketPieceList
customThumbnailPlaceholder={Vivi23AccordionListItemThumbnailPlaceholder} {...this.props}
location={this.props.location} /> customThumbnailPlaceholder={Vivi23AccordionListItemThumbnailPlaceholder} />
); );
} }
}); });
export default Vivi23PieceList; export default Vivi23PieceList;

View File

@ -0,0 +1,64 @@
'use strict';
import React from 'react';
import Button from 'react-bootstrap/lib/Button';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils';
let ArtcityLanding = React.createClass({
propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object.isRequired
},
componentWillMount() {
setDocumentTitle('Artcity Marketplace');
},
render() {
return (
<div className="container ascribe-form-wrapper artcity-landing">
<div className="row">
<div className="col-xs-12">
<div className="row artcity-landing--header">
<img className="artcity-landing--header-logo" src={this.props.whitelabel.logo} />
<div>
{getLangText('Artcity Marketplace is powered by') + ' '}
<span className="icon-ascribe-logo" />
</div>
</div>
<div className="row artcity-landing--content">
<div className="col-xs-6">
<p>
{getLangText('Existing ascribe user?')}
</p>
<LinkContainer to="/login">
<Button>
{getLangText('Log in')}
</Button>
</LinkContainer>
</div>
<div className="col-xs-6">
<p>
{getLangText('Do you need an account?')}
</p>
<LinkContainer to="/signup">
<Button>
{getLangText('Sign up')}
</Button>
</LinkContainer>
</div>
</div>
</div>
</div>
</div>
);
}
});
export default ArtcityLanding;

View File

@ -15,54 +15,54 @@ let WalletActionPanel = React.createClass({
propTypes: { propTypes: {
piece: React.PropTypes.object.isRequired, piece: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired,
loadPiece: React.PropTypes.func.isRequired,
handleDeleteSuccess: React.PropTypes.func.isRequired, handleDeleteSuccess: React.PropTypes.func.isRequired,
loadPiece: React.PropTypes.func.isRequired,
submitButtonType: React.PropTypes.func.isRequired submitButtonType: React.PropTypes.func.isRequired
}, },
render(){ render() {
if (this.props.piece && const { currentUser, handleDeleteSuccess, loadPiece, piece, submitButtonType } = this.props;
this.props.piece.notifications &&
this.props.piece.notifications.length > 0) { if (piece && piece.notifications && piece.notifications.length) {
return ( return (
<ListRequestActions <ListRequestActions
pieceOrEditions={this.props.piece} pieceOrEditions={piece}
currentUser={this.props.currentUser} currentUser={currentUser}
handleSuccess={this.props.loadPiece} handleSuccess={loadPiece}
notifications={this.props.piece.notifications}/>); notifications={piece.notifications}/>);
} } else {
else {
//We need to disable the normal acl_loan because we're inserting a custom acl_loan button //We need to disable the normal acl_loan because we're inserting a custom acl_loan button
let availableAcls; let availableAcls;
if (this.props.piece && this.props.piece.acl && typeof this.props.piece.acl.acl_loan !== 'undefined') { if (piece && piece.acl && typeof piece.acl.acl_loan !== 'undefined') {
// make a copy to not have side effects // make a copy to not have side effects
availableAcls = mergeOptions({}, this.props.piece.acl); availableAcls = mergeOptions({}, piece.acl);
availableAcls.acl_loan = false; availableAcls.acl_loan = false;
} }
let SubmitButtonType = this.props.submitButtonType; let SubmitButtonType = submitButtonType;
return ( return (
<AclButtonList <AclButtonList
className="text-center ascribe-button-list"
availableAcls={availableAcls} availableAcls={availableAcls}
pieceOrEditions={this.props.piece} className="text-center ascribe-button-list"
handleSuccess={this.props.loadPiece}> currentUser={currentUser}
pieceOrEditions={piece}
handleSuccess={loadPiece}>
<AclProxy <AclProxy
aclObject={this.props.currentUser.acl} aclObject={currentUser.acl}
aclName="acl_wallet_submit"> aclName="acl_wallet_submit">
<AclProxy <AclProxy
aclObject={availableAcls} aclObject={availableAcls}
aclName="acl_wallet_submit"> aclName="acl_wallet_submit">
<SubmitButtonType <SubmitButtonType
className="btn-sm" className="btn-sm"
piece={this.props.piece}/> piece={piece}/>
</AclProxy> </AclProxy>
</AclProxy> </AclProxy>
<DeleteButton <DeleteButton
handleSuccess={this.props.handleDeleteSuccess} handleSuccess={handleDeleteSuccess}
piece={this.props.piece}/> piece={piece}/>
</AclButtonList> </AclButtonList>
); );
} }

View File

@ -3,18 +3,18 @@
import React from 'react'; import React from 'react';
import Moment from 'moment'; import Moment from 'moment';
import Piece from '../../../../../components/ascribe_detail/piece';
import WalletActionPanel from './wallet_action_panel'; import WalletActionPanel from './wallet_action_panel';
import CollapsibleParagraph from '../../../../../components/ascribe_collapsible/collapsible_paragraph'; import CollapsibleParagraph from '../../../../../components/ascribe_collapsible/collapsible_paragraph';
import DetailProperty from '../../../../ascribe_detail/detail_property';
import HistoryIterator from '../../../../ascribe_detail/history_iterator'; import HistoryIterator from '../../../../ascribe_detail/history_iterator';
import Note from '../../../../ascribe_detail/note'; import Note from '../../../../ascribe_detail/note';
import Piece from '../../../../../components/ascribe_detail/piece';
import DetailProperty from '../../../../ascribe_detail/detail_property'; import AscribeSpinner from '../../../../ascribe_spinner';
import ApiUrls from '../../../../../constants/api_urls'; import ApiUrls from '../../../../../constants/api_urls';
import AscribeSpinner from '../../../../ascribe_spinner';
import { getLangText } from '../../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
@ -23,9 +23,10 @@ let WalletPieceContainer = React.createClass({
propTypes: { propTypes: {
piece: React.PropTypes.object.isRequired, piece: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired,
loadPiece: React.PropTypes.func.isRequired,
handleDeleteSuccess: React.PropTypes.func.isRequired, handleDeleteSuccess: React.PropTypes.func.isRequired,
loadPiece: React.PropTypes.func.isRequired,
submitButtonType: React.PropTypes.func.isRequired, submitButtonType: React.PropTypes.func.isRequired,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.object, React.PropTypes.object,
React.PropTypes.array React.PropTypes.array

View File

@ -13,6 +13,11 @@ import { mergeOptions } from '../../../../../utils/general_utils';
let CCRegisterPiece = React.createClass({ let CCRegisterPiece = React.createClass({
propTypes: { propTypes: {
// Provided from AscribeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
@ -85,6 +90,7 @@ let CCRegisterPiece = React.createClass({
setDocumentTitle(getLangText('Register a new piece')); setDocumentTitle(getLangText('Register a new piece'));
return ( return (
<RegisterPiece <RegisterPiece
{...this.props}
enableLocalHashing={false} enableLocalHashing={false}
headerMessage={getLangText('Register under a Creative Commons license')} headerMessage={getLangText('Register under a Creative Commons license')}
submitMessage={getLangText('Submit')} submitMessage={getLangText('Submit')}

View File

@ -3,27 +3,26 @@
import React from 'react'; import React from 'react';
import Moment from 'moment'; import Moment from 'moment';
import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece'; import GlobalNotificationModel from '../../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
import PieceListActions from '../../../../../../actions/piece_list_actions'; import PieceListActions from '../../../../../../actions/piece_list_actions';
import PieceListStore from '../../../../../../stores/piece_list_store'; import PieceListStore from '../../../../../../stores/piece_list_store';
import UserStore from '../../../../../../stores/user_store';
import GlobalNotificationModel from '../../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
import CylandSubmitButton from '../cyland_buttons/cyland_submit_button'; import CylandSubmitButton from '../cyland_buttons/cyland_submit_button';
import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece';
import AclProxy from '../../../../../acl_proxy'; import AclProxy from '../../../../../acl_proxy';
import { getLangText } from '../../../../../../utils/lang_utils'; import { getLangText } from '../../../../../../utils/lang_utils';
import { mergeOptions } from '../../../../../../utils/general_utils';
let CylandAccordionListItem = React.createClass({ let CylandAccordionListItem = React.createClass({
propTypes: { propTypes: {
content: React.PropTypes.object.isRequired,
className: React.PropTypes.string, className: React.PropTypes.string,
content: React.PropTypes.object,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element React.PropTypes.element
@ -31,20 +30,15 @@ let CylandAccordionListItem = React.createClass({
}, },
getInitialState() { getInitialState() {
return mergeOptions( return PieceListStore.getState();
PieceListStore.getState(),
UserStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
PieceListStore.listen(this.onChange); PieceListStore.listen(this.onChange);
UserStore.listen(this.onChange);
}, },
componentWillUnmount() { componentWillUnmount() {
PieceListStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
@ -56,37 +50,39 @@ let CylandAccordionListItem = React.createClass({
PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy });
let notification = new GlobalNotificationModel(response.notification, 'success', 10000); const notification = new GlobalNotificationModel(response.notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
getSubmitButtons() { getSubmitButtons() {
const { content } = this.props;
return ( return (
<div> <div>
<AclProxy <AclProxy
aclObject={this.props.content.acl} aclObject={content.acl}
aclName="acl_wallet_submit"> aclName="acl_wallet_submit">
<CylandSubmitButton <CylandSubmitButton
className="pull-right" className="pull-right"
piece={this.props.content} piece={content}
handleSuccess={this.handleSubmitSuccess}/> handleSuccess={this.handleSubmitSuccess}/>
</AclProxy> </AclProxy>
<AclProxy <AclProxy
aclObject={this.props.content.acl} aclObject={content.acl}
aclName="acl_wallet_submitted"> aclName="acl_wallet_submitted">
<button <button
disabled disabled
className="btn btn-default btn-xs pull-right"> className="btn btn-default btn-xs pull-right">
{getLangText('Submitted to Cyland') + ' '} {getLangText('Submitted to Cyland') + ' '}
<span className='ascribe-icon icon-ascribe-ok'/> <span className='ascribe-icon icon-ascribe-ok'/>
</button> </button>
</AclProxy> </AclProxy>
<AclProxy <AclProxy
aclObject={this.props.content.acl} aclObject={content.acl}
aclName="acl_wallet_accepted"> aclName="acl_wallet_accepted">
<button <button
disabled disabled
className="btn btn-default btn-xs pull-right"> className="btn btn-default btn-xs pull-right">
{getLangText('Loaned to Cyland') + ' '} {getLangText('Loaned to Cyland') + ' '}
<span className='ascribe-icon icon-ascribe-ok'/> <span className='ascribe-icon icon-ascribe-ok'/>
</button> </button>
@ -96,16 +92,19 @@ let CylandAccordionListItem = React.createClass({
}, },
render() { render() {
const { children, className, content } = this.props;
return ( return (
<AccordionListItemPiece <AccordionListItemPiece
className={this.props.className} className={className}
piece={this.props.content} piece={content}
subsubheading={ subsubheading={
<div className="pull-left"> <div className="pull-left">
<span>{Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}</span> <span>{Moment(content.date_created, 'YYYY-MM-DD').year()}</span>
</div>} </div>
}
buttons={this.getSubmitButtons()}> buttons={this.getSubmitButtons()}>
{this.props.children} {children}
</AccordionListItemPiece> </AccordionListItemPiece>
); );
} }

View File

@ -7,39 +7,19 @@ import Button from 'react-bootstrap/lib/Button';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import WhitelabelActions from '../../../../../../actions/whitelabel_actions';
import WhitelabelStore from '../../../../../../stores/whitelabel_store';
import { getLangText } from '../../../../../../utils/lang_utils'; import { getLangText } from '../../../../../../utils/lang_utils';
let CylandSubmitButton = React.createClass({ let CylandSubmitButton = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string,
handleSuccess: React.PropTypes.func,
piece: React.PropTypes.object.isRequired, piece: React.PropTypes.object.isRequired,
username: React.PropTypes.string
},
getInitialState() { className: React.PropTypes.string,
return WhitelabelStore.getState(); handleSuccess: React.PropTypes.func
},
componentDidMount() {
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
},
componentWillUnmount() {
WhitelabelStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
}, },
render() { render() {
const { piece, className } = this.props; const { className, piece } = this.props;
return ( return (
<LinkContainer <LinkContainer

View File

@ -3,28 +3,26 @@
import React from 'react'; import React from 'react';
import { History } from 'react-router'; import { History } from 'react-router';
import EditionListActions from '../../../../../../actions/edition_list_actions';
import GlobalNotificationModel from '../../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
import PieceActions from '../../../../../../actions/piece_actions'; import PieceActions from '../../../../../../actions/piece_actions';
import PieceStore from '../../../../../../stores/piece_store'; import PieceStore from '../../../../../../stores/piece_store';
import UserStore from '../../../../../../stores/user_store';
import PieceListStore from '../../../../../../stores/piece_list_store'; import PieceListStore from '../../../../../../stores/piece_list_store';
import PieceListActions from '../../../../../../actions/piece_list_actions'; import PieceListActions from '../../../../../../actions/piece_list_actions';
import EditionListActions from '../../../../../../actions/edition_list_actions'; import CylandAdditionalDataForm from '../cyland_forms/cyland_additional_data_form';
import CylandSubmitButton from '../cyland_buttons/cyland_submit_button'; import CylandSubmitButton from '../cyland_buttons/cyland_submit_button';
import CollapsibleParagraph from '../../../../../../components/ascribe_collapsible/collapsible_paragraph';
import CylandAdditionalDataForm from '../cyland_forms/cyland_additional_data_form';
import WalletPieceContainer from '../../ascribe_detail/wallet_piece_container'; import WalletPieceContainer from '../../ascribe_detail/wallet_piece_container';
import AscribeSpinner from '../../../../../ascribe_spinner'; import CollapsibleParagraph from '../../../../../../components/ascribe_collapsible/collapsible_paragraph';
import GlobalNotificationModel from '../../../../../../models/global_notification_model'; import AscribeSpinner from '../../../../../ascribe_spinner';
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
import { getLangText } from '../../../../../../utils/lang_utils'; import { getLangText } from '../../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../../utils/dom_utils'; import { setDocumentTitle } from '../../../../../../utils/dom_utils';
@ -33,6 +31,12 @@ import { mergeOptions } from '../../../../../../utils/general_utils';
let CylandPieceContainer = React.createClass({ let CylandPieceContainer = React.createClass({
propTypes: { propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object,
params: React.PropTypes.object params: React.PropTypes.object
}, },
@ -41,14 +45,12 @@ let CylandPieceContainer = React.createClass({
getInitialState() { getInitialState() {
return mergeOptions( return mergeOptions(
PieceStore.getInitialState(), PieceStore.getInitialState(),
UserStore.getState(),
PieceListStore.getState() PieceListStore.getState()
); );
}, },
componentDidMount() { componentDidMount() {
PieceStore.listen(this.onChange); PieceStore.listen(this.onChange);
UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange); PieceListStore.listen(this.onChange);
this.loadPiece(); this.loadPiece();
@ -64,7 +66,6 @@ let CylandPieceContainer = React.createClass({
componentWillUnmount() { componentWillUnmount() {
PieceStore.unlisten(this.onChange); PieceStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
PieceListStore.listen(this.onChange); PieceListStore.listen(this.onChange);
}, },
@ -96,12 +97,15 @@ let CylandPieceContainer = React.createClass({
const { piece } = this.state; const { piece } = this.state;
if (piece.id) { if (piece.id) {
const { currentUser } = this.props;
setDocumentTitle(`${piece.artist_name}, ${piece.title}`); setDocumentTitle(`${piece.artist_name}, ${piece.title}`);
return ( return (
<WalletPieceContainer <WalletPieceContainer
piece={piece} {...this.props}
currentUser={this.state.currentUser} piece={this.state.piece}
currentUser={currentUser}
loadPiece={this.loadPiece} loadPiece={this.loadPiece}
handleDeleteSuccess={this.handleDeleteSuccess} handleDeleteSuccess={this.handleDeleteSuccess}
submitButtonType={CylandSubmitButton}> submitButtonType={CylandSubmitButton}>

View File

@ -1,20 +0,0 @@
'use strict';
import React from 'react';
import AppConstants from '../../../../constants/application_constants';
let Hero = React.createClass({
render() {
return (
<div className="hero">
<img
className="logo" src={AppConstants.whitelabel.logo}
alt="Cyland Video Archive"
height="200px"/>
</div>
);
}
});
export default Hero;

View File

@ -1,67 +1,36 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import { History } from 'react-router';
import WhitelabelActions from '../../../../../actions/whitelabel_actions';
import WhitelabelStore from '../../../../../stores/whitelabel_store';
import Button from 'react-bootstrap/lib/Button'; import Button from 'react-bootstrap/lib/Button';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import UserStore from '../../../../../stores/user_store';
import UserActions from '../../../../../actions/user_actions';
import AscribeSpinner from '../../../../ascribe_spinner'; import AscribeSpinner from '../../../../ascribe_spinner';
import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils'; import { setDocumentTitle } from '../../../../../utils/dom_utils';
let CylandLanding = React.createClass({ let CylandLanding = React.createClass({
propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object.isRequired,
mixins: [History], // Provided from router
location: React.PropTypes.object
getInitialState() {
return mergeOptions(
UserStore.getState(),
WhitelabelStore.getState()
);
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
// 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.replace('/collection'), 0);
}
}, },
render() { render() {
setDocumentTitle('CYLAND MediaArtLab'); setDocumentTitle('CYLAND MediaArtLab');
return ( return (
<div className="container ascribe-form-wrapper cyland-landing"> <div className="ascribe-form-wrapper cyland-landing">
<div className="row"> <div className="row">
<div className="col-xs-12"> <div className="col-xs-12">
<div className="row" style={{border: '1px solid #CCC', padding: '2em'}}> <div className="row" style={{border: '1px solid #CCC', padding: '2em'}}>
<img src={this.state.whitelabel.logo} width="400px"/> <img src={this.props.whitelabel.logo} height="115" />
<div style={{marginTop: '1em'}}> <div style={{marginTop: '1em'}}>
{getLangText('Submissions to Cyland Archive are powered by') + ' '} {getLangText('Submissions to Cyland Archive are powered by') + ' '}
<span> <span>

View File

@ -3,54 +3,51 @@
import React from 'react'; import React from 'react';
import PieceList from '../../../../piece_list'; import PieceList from '../../../../piece_list';
import UserActions from '../../../../../actions/user_actions';
import UserStore from '../../../../../stores/user_store';
import CylandAccordionListItem from './cyland_accordion_list/cyland_accordion_list_item'; import CylandAccordionListItem from './cyland_accordion_list/cyland_accordion_list_item';
import { getLangText } from '../../../../../utils/lang_utils'; import { getLangText } from '../../../../../utils/lang_utils';
import { setDocumentTitle } from '../../../../../utils/dom_utils'; import { setDocumentTitle } from '../../../../../utils/dom_utils';
let CylandPieceList = React.createClass({ let CylandPieceList = React.createClass({
propTypes: { propTypes: {
// Provided from PrizeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object.isRequired,
// Provided from router
location: React.PropTypes.object location: React.PropTypes.object
}, },
getInitialState() { shouldRedirect(pieceCount) {
return UserStore.getState(); const { currentUser: { email: userEmail },
}, whitelabel: {
user: whitelabelAdminEmail
} } = this.props;
componentDidMount() { return userEmail !== whitelabelAdminEmail && !pieceCount;
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
}, },
render() { render() {
setDocumentTitle(getLangText('Collection')); setDocumentTitle(getLangText('Collection'));
return ( return (
<div> <PieceList
<PieceList {...this.props}
redirectTo="/register_piece?slide_num=0" accordionListItemType={CylandAccordionListItem}
accordionListItemType={CylandAccordionListItem} filterParams={[{
filterParams={[{ label: getLangText('Show works I have'),
label: getLangText('Show works I have'), items: [{
items: [{ key: 'acl_loaned',
key: 'acl_loaned', label: getLangText('loaned to Cyland')
label: getLangText('loaned to Cyland') }]
}] }]}
}]} redirectTo={{
location={this.props.location}/> pathname: '/register_piece',
</div> query: {
'slide_num': 0
}
}}
shouldRedirect={this.shouldRedirect} />
); );
} }
}); });

Some files were not shown because too many files have changed in this diff Show More