Merge with master

This commit is contained in:
Brett Sun 2016-02-05 17:06:16 +01:00
commit a4292f09c3
245 changed files with 7524 additions and 4749 deletions

3
.env-template Normal file
View File

@ -0,0 +1,3 @@
SAUCE_USERNAME=ascribe
SAUCE_ACCESS_KEY=
SAUCE_DEFAULT_URL=

View File

@ -2,7 +2,7 @@
"parser": "babel-eslint", "parser": "babel-eslint",
"env": { "env": {
"browser": true, "browser": true,
"es6": true "es6": true,
}, },
"rules": { "rules": {
"new-cap": [2, {newIsCap: true, capIsNew: false}], "new-cap": [2, {newIsCap: true, capIsNew: false}],

11
.gitignore vendored
View File

@ -16,9 +16,14 @@ webapp-dependencies.txt
pids pids
logs logs
results results
build/*
gemini-coverage/*
gemini-report/*
test/gemini/screenshots/*
node_modules/* node_modules/*
build
.DS_Store .DS_Store
.env

View File

@ -1,18 +1,19 @@
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
cd onion cd onion
@ -37,43 +38,62 @@ Additionally, to work on the white labeling functionality, you need to edit your
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
Branch names Make sure to check out the [style guide](https://github.com/ascribe/javascript).
=====================
Since we moved to Github, we cannot create branch names automatically with JIRA anymore.
To not lose context, but still be able to switch branches quickly using a ticket's number, we're recommending the following rules when naming our branches in onion.
``` Linting
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 ticket's title. We use [ESLint](https://github.com/eslint/eslint) with our own [custom ruleset](.eslintrc).
This allows JIRA to still track branches and pull-requests while allowing us to keep our peace of mind.
Example
-------------
**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`
SCSS Code Conventions 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. 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: 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) * [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
============
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:
```
// For issues logged in Github:
AG-<Github-issue-id>-brief-and-sane-description-of-the-ticket
// For issues logged in JIRA:
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.
Example
-------
**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`
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.
@ -83,7 +103,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`.
@ -134,9 +171,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
============ ============
@ -149,7 +193,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

@ -1,51 +0,0 @@
'use strict';
import { alt } from '../alt';
import CoaFetcher from '../fetchers/coa_fetcher';
import Q from 'q';
class CoaActions {
constructor() {
this.generateActions(
'updateCoa',
'flushCoa'
);
}
fetchOrCreate(id, bitcoinId) {
return Q.Promise((resolve, reject) => {
CoaFetcher.fetchOne(id)
.then((res) => {
if (res.coa) {
this.actions.updateCoa(res.coa);
resolve(res.coa);
}
else {
this.actions.create(bitcoinId);
}
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateCoa(null);
reject(err);
});
});
}
create(bitcoinId) {
return Q.Promise((resolve, reject) => {
CoaFetcher.create(bitcoinId)
.then((res) => {
this.actions.updateCoa(res.coa);
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateCoa(null);
reject(err);
});
});
}
}
export default alt.createActions(CoaActions);

View File

@ -83,9 +83,9 @@ class ContractAgreementListActions {
contractAgreementList in the store is already set to null; contractAgreementList in the store is already set to null;
*/ */
} }
}).then((publicContracAgreement) => { }).then((publicContractAgreement) => {
if (publicContracAgreement) { if (publicContractAgreement) {
this.actions.updateContractAgreementList([publicContracAgreement]); this.actions.updateContractAgreementList([publicContractAgreement]);
} }
}).catch(console.logGlobal); }).catch(console.logGlobal);
} }
@ -93,7 +93,10 @@ class ContractAgreementListActions {
createContractAgreement(issuer, contract){ createContractAgreement(issuer, contract){
return Q.Promise((resolve, reject) => { return Q.Promise((resolve, reject) => {
OwnershipFetcher OwnershipFetcher
.createContractAgreement(issuer, contract).then(resolve) .createContractAgreement(issuer, contract)
.then((res) => {
resolve(res && res.contractagreement)
})
.catch((err) => { .catch((err) => {
console.logGlobal(err); console.logGlobal(err);
reject(err); reject(err);

View File

@ -28,12 +28,10 @@ class ContractListActions {
} }
changeContract(contract){ changeContract(contract) {
return Q.Promise((resolve, reject) => { return Q.Promise((resolve, reject) => {
OwnershipFetcher.changeContract(contract) OwnershipFetcher.changeContract(contract)
.then((res) => { .then(resolve)
resolve(res);
})
.catch((err)=> { .catch((err)=> {
console.logGlobal(err); console.logGlobal(err);
reject(err); reject(err);
@ -41,13 +39,11 @@ class ContractListActions {
}); });
} }
removeContract(contractId){ removeContract(contractId) {
return Q.Promise( (resolve, reject) => { return Q.Promise((resolve, reject) => {
OwnershipFetcher.deleteContract(contractId) OwnershipFetcher.deleteContract(contractId)
.then((res) => { .then(resolve)
resolve(res); .catch((err) => {
})
.catch( (err) => {
console.logGlobal(err); console.logGlobal(err);
reject(err); reject(err);
}); });

View File

@ -1,27 +1,19 @@
'use strict'; 'use strict';
import { alt } from '../alt'; import { alt } from '../alt';
import EditionFetcher from '../fetchers/edition_fetcher';
class EditionActions { class EditionActions {
constructor() { constructor() {
this.generateActions( this.generateActions(
'updateEdition', 'fetchEdition',
'editionFailed' 'successFetchCoa',
'successFetchEdition',
'errorCoa',
'errorEdition',
'flushEdition'
); );
} }
fetchOne(editionId) {
EditionFetcher.fetchOne(editionId)
.then((res) => {
this.actions.updateEdition(res.edition);
})
.catch((err) => {
console.logGlobal(err);
this.actions.editionFailed(err.json);
});
}
} }
export default alt.createActions(EditionActions); export default alt.createActions(EditionActions);

View File

@ -17,23 +17,31 @@ class EditionListActions {
); );
} }
fetchEditionList(pieceId, page, pageSize, orderBy, orderAsc, filterBy) { fetchEditionList({ pieceId, page, pageSize, orderBy, orderAsc, filterBy, maxEdition }) {
if((!orderBy && typeof orderAsc === 'undefined') || !orderAsc) { if ((!orderBy && typeof orderAsc === 'undefined') || !orderAsc) {
orderBy = 'edition_number'; orderBy = 'edition_number';
orderAsc = true; orderAsc = true;
} }
// Taken from: http://stackoverflow.com/a/519157/1263876 // Taken from: http://stackoverflow.com/a/519157/1263876
if((typeof page === 'undefined' || !page) && (typeof pageSize === 'undefined' || !pageSize)) { if ((typeof page === 'undefined' || !page) && (typeof pageSize === 'undefined' || !pageSize)) {
page = 1; page = 1;
pageSize = 10; pageSize = 10;
} }
let itemsToFetch = pageSize;
// If we only want to fetch up to a specified edition, fetch all pages up to it
// as one page and adjust afterwards
if (typeof maxEdition === 'number') {
itemsToFetch = Math.ceil(maxEdition / pageSize) * pageSize;
page = 1;
}
return Q.Promise((resolve, reject) => { return Q.Promise((resolve, reject) => {
EditionListFetcher EditionListFetcher
.fetch(pieceId, page, pageSize, orderBy, orderAsc, filterBy) .fetch({ pieceId, page, orderBy, orderAsc, filterBy, pageSize: itemsToFetch })
.then((res) => { .then((res) => {
if(res && !res.editions) { if (res && !res.editions) {
throw new Error('Piece has no editions to fetch.'); throw new Error('Piece has no editions to fetch.');
} }
@ -44,8 +52,9 @@ class EditionListActions {
orderBy, orderBy,
orderAsc, orderAsc,
filterBy, filterBy,
'editionListOfPiece': res.editions, maxEdition,
'count': res.count count: res.count,
editionListOfPiece: res.editions
}); });
resolve(res); resolve(res);
}) })
@ -54,7 +63,6 @@ class EditionListActions {
reject(err); reject(err);
}); });
}); });
} }
} }

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

@ -0,0 +1,14 @@
'use strict';
import { altThirdParty } from '../alt';
class FacebookActions {
constructor() {
this.generateActions(
'sdkReady'
);
}
}
export default altThirdParty.createActions(FacebookActions);

View File

@ -9,10 +9,13 @@ class NotificationActions {
constructor() { constructor() {
this.generateActions( this.generateActions(
'updatePieceListNotifications', 'updatePieceListNotifications',
'flushPieceListNotifications',
'updateEditionListNotifications', 'updateEditionListNotifications',
'flushEditionListNotifications',
'updateEditionNotifications', 'updateEditionNotifications',
'updatePieceNotifications', 'updatePieceNotifications',
'updateContractAgreementListNotifications' 'updateContractAgreementListNotifications',
'flushContractAgreementListNotifications'
); );
} }

View File

@ -1,28 +1,19 @@
'use strict'; 'use strict';
import { alt } from '../alt'; import { alt } from '../alt';
import PieceFetcher from '../fetchers/piece_fetcher';
class PieceActions { class PieceActions {
constructor() { constructor() {
this.generateActions( this.generateActions(
'fetchPiece',
'successFetchPiece',
'errorPiece',
'flushPiece',
'updatePiece', 'updatePiece',
'updateProperty', 'updateProperty'
'pieceFailed'
); );
} }
fetchOne(pieceId) {
PieceFetcher.fetchOne(pieceId)
.then((res) => {
this.actions.updatePiece(res.piece);
})
.catch((err) => {
console.logGlobal(err);
this.actions.pieceFailed(err.json);
});
}
} }
export default alt.createActions(PieceActions); export default alt.createActions(PieceActions);

View File

@ -15,7 +15,7 @@ class PieceListActions {
); );
} }
fetchPieceList(page, pageSize, search, orderBy, orderAsc, filterBy) { fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }) {
// To prevent flickering on a pagination request, // To prevent flickering on a pagination request,
// we overwrite the piecelist with an empty list before // we overwrite the piecelist with an empty list before
// pieceListCount === -1 defines the loading state // pieceListCount === -1 defines the loading state
@ -34,7 +34,7 @@ class PieceListActions {
// afterwards, we can load the list // afterwards, we can load the list
return Q.Promise((resolve, reject) => { return Q.Promise((resolve, reject) => {
PieceListFetcher PieceListFetcher
.fetch(page, pageSize, search, orderBy, orderAsc, filterBy) .fetch({ page, pageSize, search, orderBy, orderAsc, filterBy })
.then((res) => { .then((res) => {
this.actions.updatePieceList({ this.actions.updatePieceList({
page, page,

View File

@ -1,34 +0,0 @@
'use strict';
import { alt } from '../alt';
import Q from 'q';
import PrizeListFetcher from '../fetchers/prize_list_fetcher';
class PrizeListActions {
constructor() {
this.generateActions(
'updatePrizeList'
);
}
fetchPrizeList() {
return Q.Promise((resolve, reject) => {
PrizeListFetcher
.fetch()
.then((res) => {
this.actions.updatePrizeList({
prizeList: res.prizes,
prizeListCount: res.count
});
resolve(res);
})
.catch((err) => {
console.logGlobal(err);
reject(err);
});
});
}
}
export default alt.createActions(PrizeListActions);

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();

View File

@ -1,14 +1,13 @@
'use strict'; 'use strict';
require('babel/polyfill'); import 'babel/polyfill';
import 'classlist-polyfill';
import React from 'react'; import React from 'react';
import { Router, Redirect } from 'react-router'; import { Router, Redirect } from 'react-router';
import history from './history'; import history from './history';
/* eslint-disable */
import fetch from 'isomorphic-fetch'; import fetch from 'isomorphic-fetch';
/* eslint-enable */
import ApiUrls from './constants/api_urls'; import ApiUrls from './constants/api_urls';
@ -17,44 +16,27 @@ import getRoutes from './routes';
import requests from './utils/requests'; import requests from './utils/requests';
import { updateApiUrls } from './constants/api_urls'; import { updateApiUrls } from './constants/api_urls';
import { getSubdomainSettings } from './utils/constants_utils'; import { getDefaultSubdomainSettings, getSubdomainSettings } from './utils/constants_utils';
import { initLogging } from './utils/error_utils'; import { initLogging } from './utils/error_utils';
import { getSubdomain } from './utils/general_utils'; import { getSubdomain } from './utils/general_utils';
import EventActions from './actions/event_actions'; import EventActions from './actions/event_actions';
/* eslint-disable */
// You can comment out the modules you don't need // You can comment out the modules you don't need
// import DebugHandler from './third_party/debug'; // import DebugHandler from './third_party/debug_handler';
import GoogleAnalyticsHandler from './third_party/ga'; import FacebookHandler from './third_party/facebook_handler';
import RavenHandler from './third_party/raven'; import GoogleAnalyticsHandler from './third_party/ga_handler';
import IntercomHandler from './third_party/intercom'; import IntercomHandler from './third_party/intercom_handler';
import NotificationsHandler from './third_party/notifications'; import NotificationsHandler from './third_party/notifications_handler';
import FacebookHandler from './third_party/facebook'; import RavenHandler from './third_party/raven_handler';
/* eslint-enable */
initLogging();
let headers = { const AppGateway = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
requests.defaults({
urlMap: ApiUrls,
http: {
headers: headers,
credentials: 'include'
}
});
class AppGateway {
start() { start() {
let settings;
let subdomain = getSubdomain();
try { try {
settings = getSubdomainSettings(subdomain); const subdomain = getSubdomain();
const settings = getSubdomainSettings(subdomain);
AppConstants.whitelabel = settings; AppConstants.whitelabel = settings;
updateApiUrls(settings.type, subdomain); updateApiUrls(settings.type, subdomain);
this.load(settings); this.load(settings);
@ -62,28 +44,25 @@ class AppGateway {
// if there are no matching subdomains, we're routing // if there are no matching subdomains, we're routing
// to the default frontend // to the default frontend
console.logGlobal(err); console.logGlobal(err);
this.load(); this.load(getDefaultSubdomainSettings());
} }
} },
load(settings) { load(settings) {
let type = 'default'; const { subdomain, type } = settings;
let subdomain = 'www';
let redirectRoute = (<Redirect from="/" to="/collection" />); let redirectRoute = (<Redirect from="/" to="/collection" />);
if (settings) { if (subdomain) {
type = settings.type; // Some whitelabels have landing pages so we should not automatically redirect from / to /collection.
subdomain = settings.subdomain; // Only www and cc do not have a landing page.
} if (subdomain !== 'cc') {
redirectRoute = null;
}
// www and cc do not have a landing page // Adds a client specific class to the body for whitelabel styling
if(subdomain && subdomain !== 'cc') { window.document.body.classList.add('client--' + subdomain);
redirectRoute = null;
} }
// Adds a client specific class to the body for whitelabel styling
window.document.body.classList.add('client--' + subdomain);
// Send the applicationWillBoot event to the third-party stores // Send the applicationWillBoot event to the third-party stores
EventActions.applicationWillBoot(settings); EventActions.applicationWillBoot(settings);
@ -101,8 +80,21 @@ class AppGateway {
// Send the applicationDidBoot event to the third-party stores // Send the applicationDidBoot event to the third-party stores
EventActions.applicationDidBoot(settings); EventActions.applicationDidBoot(settings);
} }
} };
let ag = new AppGateway(); // Initialize pre-start components
ag.start(); initLogging();
requests.defaults({
urlMap: ApiUrls,
http: {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'include'
}
});
// And bootstrap app
AppGateway.start();

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

@ -19,9 +19,10 @@ import { getLangText } from '../../utils/lang_utils';
let AccordionListItemEditionWidget = React.createClass({ let AccordionListItemEditionWidget = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string,
piece: React.PropTypes.object.isRequired, piece: React.PropTypes.object.isRequired,
toggleCreateEditionsDialog: React.PropTypes.func.isRequired, toggleCreateEditionsDialog: React.PropTypes.func.isRequired,
className: React.PropTypes.string,
onPollingSuccess: React.PropTypes.func onPollingSuccess: React.PropTypes.func
}, },
@ -50,14 +51,15 @@ let AccordionListItemEditionWidget = React.createClass({
* Calls the store to either show or hide the editionListTable * Calls the store to either show or hide the editionListTable
*/ */
toggleTable() { toggleTable() {
let pieceId = this.props.piece.id; const { piece: { id: pieceId } } = this.props;
let isEditionListOpen = this.state.isEditionListOpenForPieceId[pieceId] ? this.state.isEditionListOpenForPieceId[pieceId].show : false; const { filterBy, isEditionListOpenForPieceId } = this.state;
const isEditionListOpen = isEditionListOpenForPieceId[pieceId] ? isEditionListOpenForPieceId[pieceId].show : false;
if(isEditionListOpen) {
if (isEditionListOpen) {
EditionListActions.toggleEditionList(pieceId); EditionListActions.toggleEditionList(pieceId);
} else { } else {
EditionListActions.toggleEditionList(pieceId); EditionListActions.toggleEditionList(pieceId);
EditionListActions.fetchEditionList(pieceId, null, null, null, null, this.state.filterBy); EditionListActions.fetchEditionList({ pieceId, filterBy });
} }
}, },
@ -68,7 +70,7 @@ let AccordionListItemEditionWidget = React.createClass({
getGlyphicon() { getGlyphicon() {
let pieceId = this.props.piece.id; let pieceId = this.props.piece.id;
let isEditionListOpen = this.state.isEditionListOpenForPieceId[pieceId] ? this.state.isEditionListOpenForPieceId[pieceId].show : false; let isEditionListOpen = this.state.isEditionListOpenForPieceId[pieceId] ? this.state.isEditionListOpenForPieceId[pieceId].show : false;
if(isEditionListOpen) { if(isEditionListOpen) {
// this is the loading feedback for the editions // this is the loading feedback for the editions
// button. // button.
@ -118,7 +120,7 @@ let AccordionListItemEditionWidget = React.createClass({
<button <button
disabled disabled
title={getLangText('All editions for this have been deleted already.')} title={getLangText('All editions for this have been deleted already.')}
className={classNames('btn', 'btn-default', 'btn-secondary', 'btn-sm', 'ascribe-accordion-list-item-edition-widget', this.props.className)}> className={classNames('btn', 'btn-secondary', 'btn-sm', 'ascribe-accordion-list-item-edition-widget', this.props.className)}>
{'0 ' + getLangText('Editions')} {'0 ' + getLangText('Editions')}
</button> </button>
); );

View File

@ -12,8 +12,11 @@ import { getLangText } from '../../utils/lang_utils';
let AccordionListItemPiece = React.createClass({ let AccordionListItemPiece = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, className: React.PropTypes.string,
artistName: React.PropTypes.string, artistName: React.PropTypes.oneOfType([
piece: React.PropTypes.object, React.PropTypes.string,
React.PropTypes.element
]),
piece: 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
@ -31,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}`;
} }
@ -51,17 +53,21 @@ let AccordionListItemPiece = React.createClass({
piece, piece,
subsubheading, subsubheading,
thumbnailPlaceholder: ThumbnailPlaceholder } = this.props; thumbnailPlaceholder: ThumbnailPlaceholder } = this.props;
const { url, url_safe } = piece.thumbnail; const { url: thumbnailUrl, url_safe: thumbnailSafeUrl } = piece.thumbnail;
// Display the 300x300 thumbnail if we have it, otherwise just use the safe url
const thumbnailDisplayUrl = (piece.thumbnail.thumbnail_sizes && piece.thumbnail.thumbnail_sizes['300x300']) || thumbnailSafeUrl;
let thumbnail; let thumbnail;
// Since we're going to refactor the thumbnail generation anyway at one point, // Since we're going to refactor the thumbnail generation anyway at one point,
// for not use the annoying ascribe_spiral.png, we're matching the url against // for not use the annoying ascribe_spiral.png, we're matching the url against
// this name and replace it with a CSS version of the new logo. // this name and replace it with a CSS version of the new logo.
if (url.match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/)) { if (thumbnailUrl.match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/)) {
thumbnail = (<ThumbnailPlaceholder />); thumbnail = (<ThumbnailPlaceholder />);
} else { } else {
thumbnail = ( thumbnail = (
<div style={{backgroundImage: 'url("' + url_safe + '")'}}/> <div style={{backgroundImage: 'url("' + thumbnailDisplayUrl + '")'}} />
); );
} }
@ -79,8 +85,7 @@ let AccordionListItemPiece = React.createClass({
subsubheading={subsubheading} subsubheading={subsubheading}
buttons={buttons} buttons={buttons}
badge={badge} badge={badge}
linkData={this.getLinkData()} linkData={this.getLinkData()}>
>
{children} {children}
</AccordionListItem> </AccordionListItem>
); );

View File

@ -66,22 +66,34 @@ let AccordionListItemTableEditions = React.createClass({
}, },
filterSelectedEditions() { filterSelectedEditions() {
let selectedEditions = this.state.editionList[this.props.parentId] return this.state
.filter((edition) => edition.selected); .editionList[this.props.parentId]
return selectedEditions; .filter((edition) => edition.selected);
}, },
loadFurtherEditions() { loadFurtherEditions() {
const { parentId: pieceId } = this.props;
const { page, pageSize, orderBy, orderAsc, filterBy } = this.state.editionList[pieceId];
// trigger loading animation // trigger loading animation
this.setState({ this.setState({
showMoreLoading: true showMoreLoading: true
}); });
let editionList = this.state.editionList[this.props.parentId]; EditionListActions.fetchEditionList({
EditionListActions.fetchEditionList(this.props.parentId, editionList.page + 1, editionList.pageSize, pieceId,
editionList.orderBy, editionList.orderAsc, editionList.filterBy); pageSize,
orderBy,
orderAsc,
filterBy,
page: page + 1
});
}, },
render() { render() {
const { className, parentId } = this.props;
const { editionList, isEditionListOpenForPieceId, showMoreLoading } = this.state;
const editionsForPiece = editionList[parentId];
let selectedEditionsCount = 0; let selectedEditionsCount = 0;
let allEditionsCount = 0; let allEditionsCount = 0;
let orderBy; let orderBy;
@ -89,95 +101,97 @@ let AccordionListItemTableEditions = React.createClass({
let show = false; let show = false;
let showExpandOption = false; let showExpandOption = false;
let editionsForPiece = this.state.editionList[this.props.parentId];
let loadingSpinner = <AscribeSpinner size="sm" color="dark-blue" />;
// here we need to check if all editions of a specific // here we need to check if all editions of a specific
// piece are already defined. Otherwise .length will throw an error and we'll not // piece are already defined. Otherwise .length will throw an error and we'll not
// be notified about it. // be notified about it.
if(editionsForPiece) { if (editionsForPiece) {
selectedEditionsCount = this.filterSelectedEditions().length; selectedEditionsCount = this.filterSelectedEditions().length;
allEditionsCount = editionsForPiece.length; allEditionsCount = editionsForPiece.length;
orderBy = editionsForPiece.orderBy; orderBy = editionsForPiece.orderBy;
orderAsc = editionsForPiece.orderAsc; orderAsc = editionsForPiece.orderAsc;
} }
if(this.props.parentId in this.state.isEditionListOpenForPieceId) { if (parentId in isEditionListOpenForPieceId) {
show = this.state.isEditionListOpenForPieceId[this.props.parentId].show; show = isEditionListOpenForPieceId[parentId].show;
} }
// if the number of editions in the array is equal to the maximum number of editions, // if the number of editions in the array is equal to the maximum number of editions,
// then the "Show me more" dialog should be hidden from the user's view // then the "Show me more" dialog should be hidden from the user's view
if(editionsForPiece && editionsForPiece.count > editionsForPiece.length) { if (editionsForPiece && editionsForPiece.count > editionsForPiece.length) {
showExpandOption = true; showExpandOption = true;
} }
let transition = new TransitionModel('editions', 'editionId', 'bitcoin_id', (e) => e.stopPropagation() ); const transition = new TransitionModel({
to: 'editions',
queryKey: 'editionId',
valueKey: 'bitcoin_id',
callback: (e) => e.stopPropagation()
});
let columnList = [ const columnList = [
new ColumnModel( new ColumnModel({
(item) => { transformFn: (item) => {
return { return {
'editionId': item.id, 'editionId': item.id,
'pieceId': this.props.parentId, 'pieceId': parentId,
'selectItem': this.selectItem, 'selectItem': this.selectItem,
'selected': item.selected 'selected': item.selected
}; }, };
'', },
displayElement: (
<AccordionListItemTableSelectAllEditionsCheckbox <AccordionListItemTableSelectAllEditionsCheckbox
onChange={this.toggleAllItems} onChange={this.toggleAllItems}
numOfSelectedEditions={selectedEditionsCount} numOfSelectedEditions={selectedEditionsCount}
numOfAllEditions={allEditionsCount}/>, numOfAllEditions={allEditionsCount}/>
TableItemCheckbox, ),
1, displayType: TableItemCheckbox,
false rowWidth: 1
), }),
new ColumnModel( new ColumnModel({
(item) => { transition,
transformFn: (item) => {
return { return {
'content': item.edition_number + ' ' + getLangText('of') + ' ' + item.num_editions 'content': item.edition_number + ' ' + getLangText('of') + ' ' + item.num_editions
}; }, };
'edition_number', },
getLangText('Edition'), columnName: 'edition_number',
TableItemText, displayElement: getLangText('Edition'),
1, displayType: TableItemText,
false, rowWidth: 1
transition }),
), new ColumnModel({
new ColumnModel( transition,
(item) => { transformFn: (item) => {
return { return {
'content': item.bitcoin_id 'content': item.bitcoin_id
}; }, };
'bitcoin_id', },
getLangText('ID'), columnName: 'bitcoin_id',
TableItemText, displayElement: getLangText('ID'),
5, displayType: TableItemText,
false, rowWidth: 5,
transition, className: 'hidden-xs visible-sm visible-md visible-lg'
'hidden-xs visible-sm visible-md visible-lg' }),
), new ColumnModel({
new ColumnModel( transition,
(item) => { transformFn: (item) => {
let content = item.acl;
return { return {
'content': content, 'content': item.acl,
'notifications': item.notifications 'notifications': item.notifications
}; }, };
'acl', },
getLangText('Actions'), columnName: 'acl',
TableItemAclFiltered, displayElement: getLangText('Actions'),
4, displayType: TableItemAclFiltered,
false, rowWidth: 4
transition })
)
]; ];
if(show && editionsForPiece && editionsForPiece.length > 0) { if (show && editionsForPiece && editionsForPiece.length) {
return ( return (
<div className={this.props.className}> <div className={className}>
<AccordionListItemTable <AccordionListItemTable
parentId={this.props.parentId} parentId={parentId}
itemList={editionsForPiece} itemList={editionsForPiece}
columnList={columnList} columnList={columnList}
show={show} show={show}
@ -188,7 +202,14 @@ let AccordionListItemTableEditions = React.createClass({
<AccordionListItemTableToggle <AccordionListItemTableToggle
className="ascribe-accordion-list-table-toggle" className="ascribe-accordion-list-table-toggle"
onClick={this.loadFurtherEditions} onClick={this.loadFurtherEditions}
message={show && showExpandOption ? <span>{this.state.showMoreLoading ? loadingSpinner : <span className="glyphicon glyphicon-option-horizontal" aria-hidden="true" style={{top: 3}} />} Show me more</span> : null} /> message={show && showExpandOption ? (
<span>
{showMoreLoading ? <AscribeSpinner size="sm" color="dark-blue" />
: <span className="glyphicon glyphicon-option-horizontal" aria-hidden="true" style={{top: 3}} />}
{getLangText('Show me more')}
</span>
) : null
} />
</div> </div>
); );
} else { } else {

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() {
@ -88,11 +88,12 @@ let AccordionListItemWallet = React.createClass({
}, },
onPollingSuccess(pieceId) { onPollingSuccess(pieceId) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state;
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy });
EditionListActions.toggleEditionList(pieceId); EditionListActions.toggleEditionList(pieceId);
let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); const notification = new GlobalNotificationModel(getLangText('Editions successfully created'), 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
@ -110,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

@ -28,6 +28,12 @@ let CreateEditionsButton = React.createClass({
EditionListStore.listen(this.onChange); EditionListStore.listen(this.onChange);
}, },
componentDidUpdate() {
if(this.props.piece.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') {
this.startPolling();
}
},
componentWillUnmount() { componentWillUnmount() {
EditionListStore.unlisten(this.onChange); EditionListStore.unlisten(this.onChange);
clearInterval(this.state.pollingIntervalIndex); clearInterval(this.state.pollingIntervalIndex);
@ -37,28 +43,24 @@ let CreateEditionsButton = React.createClass({
this.setState(state); this.setState(state);
}, },
componentDidUpdate() {
if(this.props.piece.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') {
this.startPolling();
}
},
startPolling() { startPolling() {
// start polling until editions are defined // start polling until editions are defined
let pollingIntervalIndex = setInterval(() => { let pollingIntervalIndex = setInterval(() => {
// requests, will try to merge the filterBy parameter with other parameters (mergeOptions). // requests, will try to merge the filterBy parameter with other parameters (mergeOptions).
// Therefore it can't but null but instead has to be an empty object // Therefore it can't but null but instead has to be an empty object
EditionListActions.fetchEditionList(this.props.piece.id, null, null, null, null, {}) EditionListActions
.then((res) => { .fetchEditionList({
pieceId: this.props.piece.id,
clearInterval(this.state.pollingIntervalIndex); filterBy: {}
this.props.onPollingSuccess(this.props.piece.id, res.editions[0].num_editions); })
.then((res) => {
}) clearInterval(this.state.pollingIntervalIndex);
.catch((err) => { this.props.onPollingSuccess(this.props.piece.id, res.editions[0].num_editions);
/* Ignore and keep going */ })
}); .catch((err) => {
/* Ignore and keep going */
});
}, 5000); }, 5000);
this.setState({ this.setState({

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

@ -1,9 +1,10 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import classNames from 'classnames';
let DetailProperty = React.createClass({ const DetailProperty = React.createClass({
propTypes: { propTypes: {
label: React.PropTypes.string, label: React.PropTypes.string,
value: React.PropTypes.oneOfType([ value: React.PropTypes.oneOfType([
@ -12,6 +13,7 @@ let DetailProperty = React.createClass({
React.PropTypes.element React.PropTypes.element
]), ]),
separator: React.PropTypes.string, separator: React.PropTypes.string,
className: React.PropTypes.string,
labelClassName: React.PropTypes.string, labelClassName: React.PropTypes.string,
valueClassName: React.PropTypes.string, valueClassName: React.PropTypes.string,
ellipsis: React.PropTypes.bool, ellipsis: React.PropTypes.bool,
@ -30,31 +32,23 @@ let DetailProperty = React.createClass({
}, },
render() { render() {
let styles = {}; const {
const { labelClassName, children,
label, className,
separator, ellipsis,
valueClassName, label,
children, labelClassName,
value } = this.props; separator,
valueClassName,
if(this.props.ellipsis) { value } = this.props;
styles = {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
};
}
return ( return (
<div className="row ascribe-detail-property"> <div className={classNames('row ascribe-detail-property', className)}>
<div className="row-same-height"> <div className="row-same-height">
<div className={labelClassName}> <div className={labelClassName}>
{label} {separator} {label} {separator}
</div> </div>
<div <div className={classNames(valueClassName, {'add-overflow-ellipsis': ellipsis})}>
className={valueClassName}
style={styles}>
{children || value} {children || value}
</div> </div>
</div> </div>

View File

@ -1,35 +1,30 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import { Link, History } from 'react-router'; import { Link } from 'react-router';
import Moment from 'moment'; import Moment from 'moment';
import Row from 'react-bootstrap/lib/Row'; 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 UserActions from '../../actions/user_actions'; import EditionActions from '../../actions/edition_actions';
import UserStore from '../../stores/user_store';
import CoaActions from '../../actions/coa_actions';
import CoaStore from '../../stores/coa_store';
import HistoryIterator from './history_iterator';
import MediaContainer from './media_container';
import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph';
import Form from './../ascribe_forms/form';
import Property from './../ascribe_forms/property';
import EditionDetailProperty from './detail_property';
import LicenseDetail from './license_detail';
import FurtherDetails from './further_details';
import DetailProperty from './detail_property';
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';
@ -41,137 +36,116 @@ 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,
coaError: React.PropTypes.object,
furtherDetailsType: React.PropTypes.func, furtherDetailsType: React.PropTypes.func,
edition: React.PropTypes.object,
loadEdition: React.PropTypes.func loadEdition: React.PropTypes.func
}, },
mixins: [History],
getDefaultProps() { getDefaultProps() {
return { return {
furtherDetailsType: FurtherDetails furtherDetailsType: FurtherDetails
}; };
}, },
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
// Flushing the coa state is essential to not displaying the same
// data to the user while he's on another edition
//
// BUGFIX: Previously we had this line in the componentWillUnmount of
// CoaDetails, but since we're reloading the edition after performing an ACL action
// on it, this resulted in multiple events occupying the dispatcher, which eventually
// resulted in crashing the app.
CoaActions.flushCoa();
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() { render() {
let FurtherDetailsType = this.props.furtherDetailsType; const {
actionPanelButtonListType,
coaError,
currentUser,
edition,
furtherDetailsType: FurtherDetailsType,
loadEdition,
whitelabel } = this.props;
return ( return (
<Row> <Row>
<Col md={6}> <Col md={6} className="ascribe-print-col-left">
<MediaContainer <MediaContainer
content={this.props.edition}/> content={edition}
currentUser={currentUser}
refreshObject={loadEdition} />
</Col> </Col>
<Col md={6} className="ascribe-edition-details"> <Col md={6} className="ascribe-edition-details ascribe-print-col-right">
<div className="ascribe-detail-header"> <div className="ascribe-detail-header">
<hr style={{marginTop: 0}}/> <hr className="hidden-print" style={{marginTop: 0}} />
<h1 className="ascribe-detail-title">{this.props.edition.title}</h1> <h1 className="ascribe-detail-title">{edition.title}</h1>
<EditionDetailProperty label="BY" value={this.props.edition.artist_name} /> <DetailProperty label="CREATED BY" value={edition.artist_name} />
<EditionDetailProperty label="DATE" value={Moment(this.props.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={this.props.actionPanelButtonListType} actionPanelButtonListType={actionPanelButtonListType}
edition={this.props.edition} edition={edition}
currentUser={this.state.currentUser} currentUser={currentUser}
handleSuccess={this.props.loadEdition}/> handleSuccess={loadEdition}
whitelabel={whitelabel} />
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Certificate of Authenticity')} title={getLangText('Certificate of Authenticity')}
show={this.props.edition.acl.acl_coa === true}> show={edition.acl.acl_coa === true}>
<CoaDetails <CoaDetails
edition={this.props.edition}/> coa={edition.coa}
coaError={coaError}
editionId={edition.bitcoin_id} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Provenance/Ownership History')} title={getLangText('Provenance/Ownership History')}
show={this.props.edition.ownership_history && this.props.edition.ownership_history.length > 0}> show={edition.ownership_history && edition.ownership_history.length}>
<HistoryIterator <HistoryIterator history={edition.ownership_history} />
history={this.props.edition.ownership_history} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Consignment History')} title={getLangText('Consignment History')}
show={this.props.edition.consign_history && this.props.edition.consign_history.length > 0}> show={edition.consign_history && edition.consign_history.length > 0}>
<HistoryIterator <HistoryIterator history={edition.consign_history} />
history={this.props.edition.consign_history} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Loan History')} title={getLangText('Loan History')}
show={this.props.edition.loan_history && this.props.edition.loan_history.length > 0}> show={edition.loan_history && edition.loan_history.length > 0}>
<HistoryIterator <HistoryIterator history={edition.loan_history} />
history={this.props.edition.loan_history} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title="Notes" title="Notes"
show={!!(this.state.currentUser.username show={!!(currentUser.username || edition.acl.acl_edit || edition.public_note)}>
|| this.props.edition.acl.acl_edit
|| this.props.edition.public_note)}>
<Note <Note
id={() => {return {'bitcoin_id': this.props.edition.bitcoin_id}; }} id={() => {return {'bitcoin_id': edition.bitcoin_id}; }}
label={getLangText('Personal note (private)')} label={getLangText('Personal note (private)')}
defaultValue={this.props.edition.private_note ? this.props.edition.private_note : null} defaultValue={edition.private_note ? edition.private_note : null}
placeholder={getLangText('Enter your comments ...')} placeholder={getLangText('Enter your comments ...')}
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={this.state.currentUser}/> currentUser={currentUser} />
<Note <Note
id={() => {return {'bitcoin_id': this.props.edition.bitcoin_id}; }} id={() => {return {'bitcoin_id': edition.bitcoin_id}; }}
label={getLangText('Personal note (public)')} label={getLangText('Personal note (public)')}
defaultValue={this.props.edition.public_note ? this.props.edition.public_note : null} defaultValue={edition.public_note ? edition.public_note : null}
placeholder={getLangText('Enter your comments ...')} placeholder={getLangText('Enter your comments ...')}
editable={!!this.props.edition.acl.acl_edit} editable={!!edition.acl.acl_edit}
show={!!this.props.edition.public_note || !!this.props.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={this.state.currentUser}/> currentUser={currentUser} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Further Details')} title={getLangText('Further Details')}
show={this.props.edition.acl.acl_edit show={edition.acl.acl_edit || Object.keys(edition.extra_data).length || edition.other_data.length}>
|| Object.keys(this.props.edition.extra_data).length > 0
|| this.props.edition.other_data.length > 0}>
<FurtherDetailsType <FurtherDetailsType
editable={this.props.edition.acl.acl_edit} editable={edition.acl.acl_edit}
pieceId={this.props.edition.parent} pieceId={edition.parent}
extraData={this.props.edition.extra_data} extraData={edition.extra_data}
otherData={this.props.edition.other_data} otherData={edition.other_data}
handleSuccess={this.props.loadEdition} /> handleSuccess={loadEdition} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph title={getLangText('SPOOL Details')}>
title={getLangText('SPOOL Details')}> <SpoolDetails edition={edition} />
<SpoolDetails
edition={this.props.edition} />
</CollapsibleParagraph> </CollapsibleParagraph>
</Col> </Col>
</Row> </Row>
@ -182,60 +156,57 @@ 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 = <EditionDetailProperty label="STATUS" value={ statusStr }/>; ) : null;
if (this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer){
status = (
<EditionDetailProperty 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">
<EditionDetailProperty <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} />
<EditionDetailProperty <DetailProperty
label={getLangText('ID')} label={getLangText('ID')}
value={ edition.bitcoin_id } value={edition.bitcoin_id}
ellipsis={true} /> ellipsis={true} />
<EditionDetailProperty <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}>
<EditionDetailProperty <DetailProperty
label={getLangText('ACTIONS')}> label={getLangText('ACTIONS')}
className="hidden-print">
<EditionActionPanel <EditionActionPanel
actionPanelButtonListType={actionPanelButtonListType} actionPanelButtonListType={actionPanelButtonListType}
edition={edition}
currentUser={currentUser} currentUser={currentUser}
handleSuccess={this.handleSuccess} /> edition={edition}
</EditionDetailProperty> handleSuccess={handleSuccess}
whitelabel={whitelabel} />
</DetailProperty>
</AclProxy> </AclProxy>
<hr/> <hr/>
</div> </div>
@ -246,61 +217,77 @@ let EditionSummary = React.createClass({
let CoaDetails = React.createClass({ let CoaDetails = React.createClass({
propTypes: { propTypes: {
edition: React.PropTypes.object editionId: React.PropTypes.string,
coa: React.PropTypes.oneOfType([
React.PropTypes.number,
React.PropTypes.string,
React.PropTypes.object
]),
coaError: React.PropTypes.object
}, },
getInitialState() { contactOnIntercom() {
return CoaStore.getState(); const { coaError, editionId } = this.props;
},
componentDidMount() { window.Intercom('showNewMessage', getLangText("Hi, I'm having problems generating a Certificate of Authenticity for Edition: %s", editionId));
let { edition } = this.props; console.logGlobal(new Error(`Coa couldn't be created for edition: ${editionId}`), coaError);
CoaStore.listen(this.onChange);
if(edition.coa) {
CoaActions.fetchOrCreate(edition.coa, edition.bitcoin_id);
}
else {
CoaActions.create(edition.bitcoin_id);
}
},
componentWillUnmount() {
CoaStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
}, },
render() { render() {
if(this.state.coa && this.state.coa.url_safe) { const { coa, coaError } = this.props;
return ( let coaDetailElement;
<div>
<p className="text-center ascribe-button-list">
<a href={this.state.coa.url_safe} target="_blank">
<button className="btn btn-default btn-xs">
{getLangText('Download')} <Glyphicon glyph="cloud-download"/>
</button>
</a>
<Link to="/coa_verify">
<button className="btn btn-default btn-xs">
{getLangText('Verify')} <Glyphicon glyph="check"/>
</button>
</Link>
</p> if (coaError) {
coaDetailElement = [
<p>{getLangText('There was an error generating your Certificate of Authenticity.')}</p>,
<p>
{getLangText('Try to refresh the page. If this happens repeatedly, please ')}
<a style={{ cursor: 'pointer' }} onClick={this.contactOnIntercom}>{getLangText('contact us')}</a>.
</p>
];
} else if (coa && coa.url_safe) {
coaDetailElement = [
<div
className="notification-contract-pdf"
style={{paddingBottom: '1em'}}>
<embed
className="embed-form"
src={coa.url_safe}
alt="pdf"
pluginspage="http://www.adobe.com/products/acrobat/readstep2.html"/>
</div>,
<div className="text-center ascribe-button-list">
<a href={coa.url_safe} target="_blank">
<button className="btn btn-default btn-xs">
{getLangText('Download')} <Glyphicon glyph="cloud-download"/>
</button>
</a>
<Link to="/coa_verify">
<button className="btn btn-default btn-xs">
{getLangText('Verify')} <Glyphicon glyph="check"/>
</button>
</Link>
</div> </div>
); ];
} else if(typeof this.state.coa === 'string'){ } else if (typeof coa === 'string') {
return ( coaDetailElement = coa;
<div className="text-center"> } else {
{this.state.coa} coaDetailElement = [
</div> <AscribeSpinner color='dark-blue' size='md'/>,
); <p>{getLangText("Just a sec, we're generating your COA")}</p>,
<p>{getLangText('(you may leave the page)')}</p>
];
} }
return ( return (
<div className="text-center"> <div>
<AscribeSpinner color='dark-blue' size='lg'/> <div className="text-center hidden-print">
{coaDetailElement}
</div>
{/* Hide the COA and just show that it's a seperate document when printing */}
<div className="visible-print ascribe-coa-print-placeholder">
{getLangText('The COA is available as a seperate document')}
</div>
</div> </div>
); );
} }
@ -312,16 +299,34 @@ let SpoolDetails = React.createClass({
}, },
render() { render() {
let bitcoinIdValue = ( const { edition: {
<a target="_blank" href={'https://www.blocktrail.com/BTC/address/' + this.props.edition.bitcoin_id}>{this.props.edition.bitcoin_id}</a> bitcoin_id: bitcoinId,
hash_as_address: hashAsAddress,
btc_owner_address_noprefix: bitcoinOwnerAddress
} } = this.props;
const bitcoinIdValue = (
<a className="anchor-no-expand-print"
target="_blank"
href={'https://www.blocktrail.com/BTC/address/' + bitcoinId}>
{bitcoinId}
</a>
); );
let hashOfArtwork = ( const hashOfArtwork = (
<a target="_blank" href={'https://www.blocktrail.com/BTC/address/' + this.props.edition.hash_as_address}>{this.props.edition.hash_as_address}</a> <a className="anchor-no-expand-print"
target="_blank"
href={'https://www.blocktrail.com/BTC/address/' + hashAsAddress}>
{hashAsAddress}
</a>
); );
let ownerAddress = ( const ownerAddress = (
<a target="_blank" href={'https://www.blocktrail.com/BTC/address/' + this.props.edition.btc_owner_address_noprefix}>{this.props.edition.btc_owner_address_noprefix}</a> <a className="anchor-no-expand-print"
target="_blank"
href={'https://www.blocktrail.com/BTC/address/' + bitcoinOwnerAddress}>
{bitcoinOwnerAddress}
</a>
); );
return ( return (
@ -349,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
}, },
@ -72,53 +74,57 @@ let EditionActionPanel = React.createClass({
EditionListActions.closeAllEditionLists(); EditionListActions.closeAllEditionLists();
EditionListActions.clearAllEditionSelections(); EditionListActions.clearAllEditionSelections();
let notification = new GlobalNotificationModel(response.notification, 'success'); const notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
this.history.pushState(null, '/collection'); this.history.push('/collection');
}, },
refreshCollection() { refreshCollection() {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state;
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
EditionListActions.refreshEditionList({pieceId: this.props.edition.parent}); PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy });
EditionListActions.refreshEditionList({ pieceId: this.props.edition.parent });
}, },
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

@ -24,39 +24,41 @@ 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 EditionStore.getState(); return EditionStore.getInitialState();
}, },
componentDidMount() { componentDidMount() {
EditionStore.listen(this.onChange); EditionStore.listen(this.onChange);
// Every time we're entering the edition detail page,
// just reset the edition that is saved in the edition store
// as it will otherwise display wrong/old data once the user loads
// the edition detail a second time
EditionActions.updateEdition({});
this.loadEdition(); this.loadEdition();
}, },
// 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
// button to update the URL parameter (and therefore to switch pieces) // button to update the URL parameter (and therefore to switch pieces)
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if(this.props.params.editionId !== nextProps.params.editionId) { if (this.props.params.editionId !== nextProps.params.editionId) {
EditionActions.updateEdition({}); EditionActions.flushEdition();
EditionActions.fetchOne(nextProps.params.editionId); this.loadEdition(nextProps.params.editionId);
} }
}, },
componentDidUpdate() { componentDidUpdate() {
const { editionError } = this.state; const { err: editionErr } = this.state.editionMeta;
if(editionError && editionError.status === 404) { if (editionErr && editionErr.json && editionErr.json.status === 404) {
this.throws(new ResourceNotFoundError(getLangText("Oops, the edition you're looking for doesn't exist."))); this.throws(new ResourceNotFoundError(getLangText("Oops, the edition you're looking for doesn't exist.")));
} }
}, },
@ -68,30 +70,28 @@ let EditionContainer = React.createClass({
onChange(state) { onChange(state) {
this.setState(state); this.setState(state);
if (!state.edition.digital_work) {
return;
}
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.fetchOne(this.props.params.editionId), 10000);
this.setState({timerId: timerId});
}
}, },
loadEdition() { loadEdition(editionId = this.props.params.editionId) {
EditionActions.fetchOne(this.props.params.editionId); EditionActions.fetchEdition(editionId);
}, },
render() { render() {
if(this.state.edition && this.state.edition.id) { const { actionPanelButtonListType, currentUser, furtherDetailsType, whitelabel } = this.props;
setDocumentTitle([this.state.edition.artist_name, this.state.edition.title].join(', ')); const { edition, coaMeta } = this.state;
if (edition.id) {
setDocumentTitle(`${edition.artist_name}, ${edition.title}`);
return ( return (
<Edition <Edition
actionPanelButtonListType={this.props.actionPanelButtonListType} actionPanelButtonListType={actionPanelButtonListType}
furtherDetailsType={this.props.furtherDetailsType} coaError={coaMeta.err}
edition={this.state.edition} currentUser={currentUser}
loadEdition={this.loadEdition} /> edition={edition}
furtherDetailsType={furtherDetailsType}
loadEdition={this.loadEdition}
whitelabel={whitelabel} />
); );
} else { } else {
return ( return (

View File

@ -5,25 +5,27 @@ import React from 'react';
import Row from 'react-bootstrap/lib/Row'; import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col'; import Col from 'react-bootstrap/lib/Col';
import Form from './../ascribe_forms/form';
import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata';
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 FurtherDetailsFileuploader from './further_details_fileuploader'; import FurtherDetailsFileuploader from './further_details_fileuploader';
import Form from './../ascribe_forms/form';
import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata';
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
import { getLangText } from '../../utils/lang_utils';
let FurtherDetails = React.createClass({ let FurtherDetails = React.createClass({
propTypes: { propTypes: {
pieceId: React.PropTypes.number.isRequired,
editable: React.PropTypes.bool, editable: React.PropTypes.bool,
pieceId: React.PropTypes.number,
extraData: React.PropTypes.object, extraData: React.PropTypes.object,
handleSuccess: React.PropTypes.func,
otherData: React.PropTypes.arrayOf(React.PropTypes.object), otherData: React.PropTypes.arrayOf(React.PropTypes.object),
handleSuccess: React.PropTypes.func
}, },
getInitialState() { getInitialState() {
@ -32,13 +34,18 @@ let FurtherDetails = React.createClass({
}; };
}, },
showNotification(){ showNotification() {
this.props.handleSuccess(); const { handleSuccess } = this.props;
let notification = new GlobalNotificationModel('Details updated', 'success');
if (typeof handleSucess === 'function') {
handleSuccess();
}
const notification = new GlobalNotificationModel(getLangText('Details updated'), 'success');
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
submitFile(file){ submitFile(file) {
this.setState({ this.setState({
otherDataKey: file.key otherDataKey: file.key
}); });
@ -51,40 +58,42 @@ let FurtherDetails = React.createClass({
}, },
render() { render() {
const { editable, extraData, otherData, pieceId } = this.props;
return ( return (
<Row> <Row>
<Col md={12} className="ascribe-edition-personal-note"> <Col md={12} className="ascribe-edition-personal-note">
<PieceExtraDataForm <PieceExtraDataForm
name='artist_contact_info' name='artist_contact_info'
title='Artist Contact Info' title='Artist Contact Info'
convertLinks
editable={editable}
extraData={extraData}
handleSuccess={this.showNotification} handleSuccess={this.showNotification}
editable={this.props.editable} pieceId={pieceId} />
pieceId={this.props.pieceId}
extraData={this.props.extraData}
/>
<PieceExtraDataForm <PieceExtraDataForm
name='display_instructions' name='display_instructions'
title='Display Instructions' title='Display Instructions'
editable={editable}
extraData={extraData}
handleSuccess={this.showNotification} handleSuccess={this.showNotification}
editable={this.props.editable} pieceId={pieceId} />
pieceId={this.props.pieceId}
extraData={this.props.extraData} />
<PieceExtraDataForm <PieceExtraDataForm
name='technology_details' name='technology_details'
title='Technology Details' title='Technology Details'
editable={editable}
extraData={extraData}
handleSuccess={this.showNotification} handleSuccess={this.showNotification}
editable={this.props.editable} pieceId={pieceId} />
pieceId={this.props.pieceId}
extraData={this.props.extraData} />
<Form> <Form>
<FurtherDetailsFileuploader <FurtherDetailsFileuploader
submitFile={this.submitFile} submitFile={this.submitFile}
setIsUploadReady={this.setIsUploadReady} setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
editable={this.props.editable} editable={editable}
overrideForm={true} overrideForm={true}
pieceId={this.props.pieceId} pieceId={pieceId}
otherData={this.props.otherData} otherData={otherData}
multiple={true} /> multiple={true} />
</Form> </Form>
</Col> </Col>

View File

@ -8,6 +8,7 @@ import ReactS3FineUploader from './../ascribe_uploader/react_s3_fine_uploader';
import ApiUrls from '../../constants/api_urls'; import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { validationTypes } from '../../constants/uploader_constants';
import { getCookie } from '../../utils/fetch_api_utils'; import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
@ -17,24 +18,29 @@ const { func, bool, number, object, string, arrayOf } = React.PropTypes;
let FurtherDetailsFileuploader = React.createClass({ let FurtherDetailsFileuploader = React.createClass({
propTypes: { propTypes: {
label: string, pieceId: number.isRequired,
pieceId: number,
otherData: arrayOf(object),
editable: bool, editable: bool,
label: string,
otherData: arrayOf(object),
// Props for ReactS3FineUploader // Props for ReactS3FineUploader
multiple: bool, areAssetsDownloadable: bool,
showErrorPrompt: bool, isReadyForFormSubmission: func,
submitFile: func, // TODO: rename to onSubmitFile submitFile: func, // TODO: rename to onSubmitFile
onValidationFailed: func,
multiple: bool,
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
isReadyForFormSubmission: func showErrorPrompt: bool,
validation: ReactS3FineUploader.propTypes.validation
}, },
getDefaultProps() { getDefaultProps() {
return { return {
areAssetsDownloadable: true,
label: getLangText('Additional files'), label: getLangText('Additional files'),
multiple: false multiple: false,
validation: validationTypes.additionalData
}; };
}, },
@ -43,11 +49,13 @@ let FurtherDetailsFileuploader = React.createClass({
editable, editable,
isReadyForFormSubmission, isReadyForFormSubmission,
multiple, multiple,
onValidationFailed,
otherData, otherData,
pieceId, pieceId,
setIsUploadReady, setIsUploadReady,
showErrorPrompt, showErrorPrompt,
submitFile } = this.props; submitFile,
validation } = this.props;
// Essentially there a three cases important to the fileuploader // Essentially there a three cases important to the fileuploader
// //
@ -65,19 +73,29 @@ let FurtherDetailsFileuploader = React.createClass({
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: pieceId pieceId: pieceId
}} }}
createBlobRoutine={{ multiple={multiple}
url: ApiUrls.blob_otherdatas, onValidationFailed={onValidationFailed}
pieceId: pieceId
}}
validation={AppConstants.fineUploader.validation.additionalData}
submitFile={submitFile}
setIsUploadReady={setIsUploadReady} setIsUploadReady={setIsUploadReady}
isReadyForFormSubmission={isReadyForFormSubmission}
session={{ session={{
endpoint: AppConstants.serverUrl + 'api/blob/otherdatas/fineuploader_session/', endpoint: AppConstants.serverUrl + 'api/blob/otherdatas/fineuploader_session/',
customHeaders: { customHeaders: {
@ -97,18 +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={true}
areAssetsEditable={editable}
multiple={multiple}
showErrorPrompt={showErrorPrompt} />
</Property> </Property>
); );
} }

View File

@ -22,7 +22,11 @@ let HistoryIterator = React.createClass({
return ( return (
<span> <span>
{historicalEventDescription} {historicalEventDescription}
<a href={historicalEvent[2]} target="_blank">{contractName}</a> <a className="anchor-no-expand-print"
target="_blank"
href={historicalEvent[2]}>
{contractName}
</a>
</span> </span>
); );
} else if(historicalEvent.length === 2) { } else if(historicalEvent.length === 2) {

View File

@ -14,42 +14,41 @@ import CollapsibleButton from './../ascribe_collapsible/collapsible_button';
import AclProxy from '../acl_proxy'; import AclProxy from '../acl_proxy';
import UserActions from '../../actions/user_actions'; import { getLangText } from '../../utils/lang_utils';
import UserStore from '../../stores/user_store'; import { extractFileExtensionFromString } from '../../utils/file_utils';
import { mergeOptions } from '../../utils/general_utils.js';
import { getLangText } from '../../utils/lang_utils.js';
const EMBED_IFRAME_HEIGHT = { 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,
refreshObject: React.PropTypes.func refreshObject: React.PropTypes.func.isRequired,
currentUser: React.PropTypes.object
}, },
getInitialState() { getInitialState() {
return mergeOptions( return {
UserStore.getState(), timerId: null
{ };
timerId: null
});
}, },
componentDidMount() { componentDidMount() {
UserStore.listen(this.onChange); const { content: { digital_work: digitalWork }, refreshObject } = this.props;
UserActions.fetchCurrentUser();
if (!this.props.content.digital_work) { if (digitalWork) {
return; const isEncoding = digitalWork.isEncoding;
}
let isEncoding = this.props.content.digital_work.isEncoding; if (digitalWork.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) {
if (this.props.content.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { this.setState({
let timerId = window.setInterval(this.props.refreshObject, 10000); timerId: window.setInterval(refreshObject, ENCODE_UPDATE_TIME)
this.setState({timerId: timerId}); });
}
} }
}, },
@ -60,22 +59,26 @@ let MediaContainer = React.createClass({
}, },
componentWillUnmount() { componentWillUnmount() {
UserStore.unlisten(this.onChange);
window.clearInterval(this.state.timerId); window.clearInterval(this.state.timerId);
}, },
onChange(state) {
this.setState(state);
},
render() { render() {
const { content } = this.props; const { content, currentUser } = this.props;
// Pieces and editions are joined to the user by a foreign key in the database, so // Pieces and editions are joined to the user by a foreign key in the database, so
// the information in content will be updated if a user updates their username. // the information in content will be updated if a user updates their username.
// We also force uniqueness of usernames, so this check is safe to dtermine if the // We also force uniqueness of usernames, so this check is safe to dtermine if the
// content was registered by the current user. // content was registered by the current user.
const didUserRegisterContent = this.state.currentUser && (this.state.currentUser.username === content.user_registered); const didUserRegisterContent = currentUser && (currentUser.username === content.user_registered);
// We want to show the file's extension as a label of the download button.
// We can however not only use `extractFileExtensionFromString` on the url for that
// as files might be saved on S3 without a file extension which leads
// `extractFileExtensionFromString` to extract everything starting from the top level
// domain: e.g. '.net/live/<hash>'.
// Therefore, we extract the file's name (last part of url, separated with a slash)
// and try to extract the file extension from there.
const fileName = content.digital_work.url.split('/').pop();
const fileExtension = extractFileExtensionFromString(fileName);
let thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ? let thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ?
content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe; content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe;
@ -94,8 +97,11 @@ let MediaContainer = React.createClass({
embed = ( embed = (
<CollapsibleButton <CollapsibleButton
button={ button={
<Button bsSize="xsmall" className="ascribe-margin-1px" disabled={isEmbedDisabled ? '"disabled"' : ''}> <Button
Embed bsSize="xsmall"
className="ascribe-margin-1px"
disabled={isEmbedDisabled}>
{getLangText('Embed')}
</Button> </Button>
} }
panel={ panel={
@ -103,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 (
@ -114,7 +120,7 @@ let MediaContainer = React.createClass({
url={content.digital_work.url} url={content.digital_work.url}
extraData={extraData} extraData={extraData}
encodingStatus={content.digital_work.isEncoding} /> encodingStatus={content.digital_work.isEncoding} />
<p className="text-center"> <p className="text-center hidden-print">
<span className="ascribe-social-button-list"> <span className="ascribe-social-button-list">
<FacebookShareButton /> <FacebookShareButton />
<TwitterShareButton <TwitterShareButton
@ -125,8 +131,16 @@ let MediaContainer = React.createClass({
show={['video', 'audio', 'image'].indexOf(mimetype) === -1 || content.acl.acl_download} show={['video', 'audio', 'image'].indexOf(mimetype) === -1 || content.acl.acl_download}
aclObject={content.acl} aclObject={content.acl}
aclName="acl_download"> aclName="acl_download">
<Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.content.digital_work.url} target="_blank"> <Button
Download .{mimetype} <Glyphicon glyph="cloud-download"/> bsSize="xsmall"
className="ascribe-margin-1px"
href={content.digital_work.url}
target="_blank">
{/*
If it turns out that `fileExtension` is an empty string, we're just
using the label 'file'.
*/}
{getLangText('Download')} .{fileExtension || 'file'} <Glyphicon glyph="cloud-download" />
</Button> </Button>
</AclProxy> </AclProxy>
{embed} {embed}

View File

@ -2,64 +2,68 @@
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;
} }
}); });
export default Note; export default Note;

View File

@ -15,37 +15,39 @@ 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,
header: React.PropTypes.object, header: React.PropTypes.object,
subheader: React.PropTypes.object, subheader: React.PropTypes.object,
buttons: React.PropTypes.object,
loadPiece: 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
]) ])
}, },
updatePiece() {
updateObject() { return PieceActions.fetchPiece(this.props.piece.id);
return PieceActions.fetchOne(this.props.piece.id);
}, },
render() { render() {
const { buttons, children, currentUser, header, piece, subheader } = this.props;
return ( return (
<Row> <Row>
<Col md={6}> <Col md={6} className="ascribe-print-col-left">
<MediaContainer <MediaContainer
refreshObject={this.updateObject} content={piece}
content={this.props.piece}/> currentUser={currentUser}
refreshObject={this.updatePiece} />
</Col> </Col>
<Col md={6} className="ascribe-edition-details"> <Col md={6} className="ascribe-edition-details ascribe-print-col-right">
{this.props.header} {header}
{this.props.subheader} {subheader}
{this.props.buttons} {buttons}
{this.props.children}
{children}
</Col> </Col>
</Row> </Row>
); );

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,9 +70,8 @@ let PieceContainer = React.createClass({
getInitialState() { getInitialState() {
return mergeOptions( return mergeOptions(
UserStore.getState(),
PieceListStore.getState(), PieceListStore.getState(),
PieceStore.getState(), PieceStore.getInitialState(),
{ {
showCreateEditionsDialog: false showCreateEditionsDialog: false
} }
@ -77,29 +79,32 @@ 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);
// Every time we enter the piece detail page, just reset the piece
// store as it will otherwise display wrong/old data once the user loads
// the piece detail a second time
PieceActions.updatePiece({});
this.loadPiece(); this.loadPiece();
UserActions.fetchCurrentUser(); },
// This is done to update the container when the user clicks on the prev or next
// button to update the URL parameter (and therefore to switch pieces) or
// when the user clicks on a notification while being in another piece view
componentWillReceiveProps(nextProps) {
if (this.props.params.pieceId !== nextProps.params.pieceId) {
PieceActions.flushPiece();
this.loadPiece(nextProps.params.pieceId);
}
}, },
componentDidUpdate() { componentDidUpdate() {
const { pieceError } = this.state; const { err: pieceErr } = this.state.pieceMeta;
if(pieceError && pieceError.status === 404) { if (pieceErr && pieceErr.json && pieceErr.json.status === 404) {
this.throws(new ResourceNotFoundError(getLangText("Oops, the piece you're looking for doesn't exist."))); this.throws(new ResourceNotFoundError(getLangText("Oops, the piece you're looking for doesn't exist.")));
} }
}, },
componentWillUnmount() { componentWillUnmount() {
PieceStore.unlisten(this.onChange); PieceStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange);
}, },
@ -115,8 +120,7 @@ let PieceContainer = React.createClass({
ALSO, WE ENABLED THE LOAN BUTTON FOR IKONOTV TO LET THEM LOAN ON A PIECE LEVEL ALSO, WE ENABLED THE LOAN BUTTON FOR IKONOTV TO LET THEM LOAN ON A PIECE LEVEL
*/ */
if(state && state.piece && state.piece.acl && typeof state.piece.acl.acl_loan !== 'undefined') { if (state && state.piece && state.piece.acl && typeof state.piece.acl.acl_loan !== 'undefined') {
let pieceState = mergeOptions({}, state.piece); let pieceState = mergeOptions({}, state.piece);
pieceState.acl.acl_loan = false; pieceState.acl.acl_loan = false;
this.setState({ this.setState({
@ -128,11 +132,10 @@ let PieceContainer = React.createClass({
} }
}, },
loadPiece() { loadPiece(pieceId = this.props.params.pieceId) {
PieceActions.fetchOne(this.props.params.pieceId); PieceActions.fetchPiece(pieceId);
}, },
toggleCreateEditionsDialog() { toggleCreateEditionsDialog() {
this.setState({ this.setState({
showCreateEditionsDialog: !this.state.showCreateEditionsDialog showCreateEditionsDialog: !this.state.showCreateEditionsDialog
@ -140,43 +143,47 @@ let PieceContainer = React.createClass({
}, },
handleEditionCreationSuccess() { handleEditionCreationSuccess() {
PieceActions.updateProperty({key: 'num_editions', value: 0}); const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state;
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy); PieceActions.updateProperty({ key: 'num_editions', value: 0 });
PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy });
this.toggleCreateEditionsDialog(); this.toggleCreateEditionsDialog();
}, },
handleDeleteSuccess(response) { handleDeleteSuccess(response) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state;
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy });
// since we're deleting a piece, we just need to close // since we're deleting a piece, we just need to close
// all editions dialogs and not reload them // all editions dialogs and not reload them
EditionListActions.closeAllEditionLists(); EditionListActions.closeAllEditionLists();
EditionListActions.clearAllEditionSelections(); EditionListActions.clearAllEditionSelections();
let notification = new GlobalNotificationModel(response.notification, 'success'); const notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
this.history.pushState(null, '/collection'); this.history.push('/collection');
}, },
getCreateEditionsDialog() { getCreateEditionsDialog() {
if(this.state.piece.num_editions < 1 && this.state.showCreateEditionsDialog) { if (this.state.piece.num_editions < 1 && this.state.showCreateEditionsDialog) {
return ( return (
<div style={{marginTop: '1em'}}> <div style={{marginTop: '1em'}}>
<CreateEditionsForm <CreateEditionsForm
pieceId={this.state.piece.id} pieceId={this.state.piece.id}
handleSuccess={this.handleEditionCreationSuccess} /> handleSuccess={this.handleEditionCreationSuccess} />
<hr/> <hr />
</div> </div>
); );
} else { } else {
return (<hr/>); return (<hr />);
} }
}, },
handlePollingSuccess(pieceId, numEditions) { handlePollingSuccess(pieceId, numEditions) {
const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state;
// we need to refresh the num_editions property of the actual piece we're looking at // we need to refresh the num_editions property of the actual piece we're looking at
PieceActions.updateProperty({ PieceActions.updateProperty({
@ -188,27 +195,28 @@ let PieceContainer = React.createClass({
// btw.: It's not sufficient to just set num_editions to numEditions, since a single accordion // btw.: It's not sufficient to just set num_editions to numEditions, since a single accordion
// list item also uses the firstEdition property which we can only get from the server in that case. // list item also uses the firstEdition property which we can only get from the server in that case.
// Therefore we need to at least refetch the changed piece from the server or on our case simply all // Therefore we need to at least refetch the changed piece from the server or on our case simply all
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy });
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); const notification = new GlobalNotificationModel(getLangText('Editions successfully created'), 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
getId() { getId() {
return {'id': this.state.piece.id}; return { 'id': this.state.piece.id };
}, },
getActions() { getActions() {
const { piece, currentUser } = this.state; const { piece } = this.state;
const { currentUser } = this.props;
if (piece && 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
@ -218,10 +226,13 @@ let PieceContainer = React.createClass({
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
*/} */}
<DetailProperty label={getLangText('ACTIONS')}> <DetailProperty
label={getLangText('ACTIONS')}
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
@ -232,12 +243,12 @@ let PieceContainer = React.createClass({
onPollingSuccess={this.handlePollingSuccess}/> onPollingSuccess={this.handlePollingSuccess}/>
<DeleteButton <DeleteButton
handleSuccess={this.handleDeleteSuccess} handleSuccess={this.handleDeleteSuccess}
piece={piece}/> piece={piece} />
<AclInformation <AclInformation
aim="button" aim="button"
verbs={['acl_share', 'acl_transfer', 'acl_create_editions', 'acl_loan', 'acl_delete', verbs={['acl_share', 'acl_transfer', 'acl_create_editions', 'acl_loan', 'acl_delete',
'acl_consign']} 'acl_consign']}
aclObject={piece.acl}/> aclObject={piece.acl} />
</AclButtonList> </AclButtonList>
</DetailProperty> </DetailProperty>
</AclProxy> </AclProxy>
@ -246,76 +257,76 @@ let PieceContainer = React.createClass({
}, },
render() { render() {
if (this.state.piece && this.state.piece.id) { const { currentUser, furtherDetailsType: FurtherDetailsType } = this.props;
let FurtherDetailsType = this.props.furtherDetailsType; const { piece } = this.state;
setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', '));
if (piece.id) {
setDocumentTitle(`${piece.artist_name}, ${piece.title}`);
return ( return (
<Piece <Piece
piece={this.state.piece} piece={piece}
loadPiece={this.loadPiece} currentUser={currentUser}
header={ header={
<div className="ascribe-detail-header"> <div className="ascribe-detail-header">
<hr style={{marginTop: 0}}/> <hr className="hidden-print" style={{marginTop: 0}} />
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1> <h1 className="ascribe-detail-title">{piece.title}</h1>
<DetailProperty label="BY" value={this.state.piece.artist_name} /> <DetailProperty label="CREATED BY" value={piece.artist_name} />
<DetailProperty label="DATE" value={Moment(this.state.piece.date_created, 'YYYY-MM-DD').year() } /> <DetailProperty label="DATE" value={Moment(piece.date_created, 'YYYY-MM-DD').year() } />
{this.state.piece.num_editions > 0 ? <DetailProperty label="EDITIONS" value={ this.state.piece.num_editions } /> : null} {piece.num_editions > 0 ? <DetailProperty label="EDITIONS" value={ piece.num_editions } /> : null}
<hr/> <hr/>
</div> </div>
} }
subheader={ subheader={
<div className="ascribe-detail-header"> <div className="ascribe-detail-header">
<DetailProperty label={getLangText('REGISTREE')} value={ this.state.piece.user_registered } /> <DetailProperty label={getLangText('ASCRIBED BY')} value={ piece.user_registered } />
<DetailProperty label={getLangText('ID')} value={ this.state.piece.bitcoin_id } ellipsis={true} /> <DetailProperty label={getLangText('ID')} value={ piece.bitcoin_id } ellipsis={true} />
<LicenseDetail license={this.state.piece.license_type} /> <LicenseDetail license={piece.license_type} />
</div> </div>
} }
buttons={this.getActions()}> buttons={this.getActions()}>
{this.getCreateEditionsDialog()} {this.getCreateEditionsDialog()}
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Loan History')} title={getLangText('Loan History')}
show={this.state.piece.loan_history && this.state.piece.loan_history.length > 0}> show={piece.loan_history && piece.loan_history.length > 0}>
<HistoryIterator <HistoryIterator
history={this.state.piece.loan_history} /> history={piece.loan_history} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Notes')} title={getLangText('Notes')}
show={!!(this.state.currentUser.username show={!!(currentUser.username || piece.acl.acl_edit || piece.public_note)}>
|| this.state.piece.acl.acl_edit
|| this.state.piece.public_note)}>
<Note <Note
id={this.getId} id={this.getId}
label={getLangText('Personal note (private)')} label={getLangText('Personal note (private)')}
defaultValue={this.state.piece.private_note || null} defaultValue={piece.private_note || null}
show = {!!this.state.currentUser.username} show = {!!currentUser.username}
placeholder={getLangText('Enter your comments ...')} placeholder={getLangText('Enter your comments ...')}
editable={true} editable={true}
successMessage={getLangText('Private note saved')} successMessage={getLangText('Private note saved')}
url={ApiUrls.note_private_piece} url={ApiUrls.note_private_piece}
currentUser={this.state.currentUser}/> currentUser={currentUser} />
<Note <Note
id={this.getId} id={this.getId}
label={getLangText('Personal note (public)')} label={getLangText('Personal note (public)')}
defaultValue={this.state.piece.public_note || null} defaultValue={piece.public_note || null}
placeholder={getLangText('Enter your comments ...')} placeholder={getLangText('Enter your comments ...')}
editable={!!this.state.piece.acl.acl_edit} editable={!!piece.acl.acl_edit}
show={!!(this.state.piece.public_note || this.state.piece.acl.acl_edit)} show={!!(piece.public_note || piece.acl.acl_edit)}
successMessage={getLangText('Public note saved')} successMessage={getLangText('Public note saved')}
url={ApiUrls.note_public_piece} url={ApiUrls.note_public_piece}
currentUser={this.state.currentUser}/> currentUser={currentUser} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Further Details')} title={getLangText('Further Details')}
show={this.state.piece.acl.acl_edit show={piece.acl.acl_edit
|| Object.keys(this.state.piece.extra_data).length > 0 || Object.keys(piece.extra_data).length > 0
|| this.state.piece.other_data.length > 0} || piece.other_data.length > 0}
defaultExpanded={true}> defaultExpanded={true}>
<FurtherDetailsType <FurtherDetailsType
editable={this.state.piece.acl.acl_edit} editable={piece.acl.acl_edit}
pieceId={this.state.piece.id} pieceId={piece.id}
extraData={this.state.piece.extra_data} extraData={piece.extra_data}
otherData={this.state.piece.other_data} otherData={piece.other_data}
handleSuccess={this.loadPiece} /> handleSuccess={this.loadPiece} />
</CollapsibleParagraph> </CollapsibleParagraph>

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

@ -19,7 +19,7 @@ let CreateEditionsForm = React.createClass({
pieceId: React.PropTypes.number pieceId: React.PropTypes.number
}, },
getFormData(){ getFormData() {
return { return {
piece_id: parseInt(this.props.pieceId, 10) piece_id: parseInt(this.props.pieceId, 10)
}; };
@ -58,11 +58,12 @@ let CreateEditionsForm = React.createClass({
<input <input
type="number" type="number"
placeholder="(e.g. 32)" placeholder="(e.g. 32)"
min={1}/> min={1}
max={100} />
</Property> </Property>
</Form> </Form>
); );
} }
}); });
export default CreateEditionsForm; export default CreateEditionsForm;

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({
@ -178,20 +178,20 @@ let Form = React.createClass({
let formData = this.getFormData(); let formData = this.getFormData();
// sentry shouldn't post the user's password // sentry shouldn't post the user's password
if(formData.password) { if (formData.password) {
delete formData.password; delete formData.password;
} }
console.logGlobal(err, false, formData); console.logGlobal(err, formData);
if(this.props.isInline) { if (this.props.isInline) {
let notification = new GlobalNotificationModel(getLangText('Something went wrong, please try again later'), 'danger'); let notification = new GlobalNotificationModel(getLangText('Something went wrong, please try again later'), 'danger');
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
} else { } else {
this.setState({errors: [getLangText('Something went wrong, please try again later')]}); this.setState({errors: [getLangText('Something went wrong, please try again later')]});
} }
} }
this.setState({submitted: false}); this.setState({submitted: false});
}, },
@ -205,16 +205,15 @@ let Form = React.createClass({
}, },
getButtons() { getButtons() {
if (this.state.submitted){ if (this.state.submitted) {
return this.props.spinner; return this.props.spinner;
} }
if (this.props.buttons){ if (this.props.buttons !== undefined) {
return this.props.buttons; return this.props.buttons;
} }
let buttons = null;
if (this.state.edited && !this.props.disabled){ if (this.state.edited && !this.props.disabled) {
buttons = ( return (
<div className="row" style={{margin: 0}}> <div className="row" style={{margin: 0}}>
<p className="pull-right"> <p className="pull-right">
<Button <Button
@ -230,9 +229,9 @@ let Form = React.createClass({
</p> </p>
</div> </div>
); );
} else {
return null;
} }
return buttons;
}, },
getErrors() { getErrors() {

View File

@ -41,6 +41,14 @@ let ConsignForm = React.createClass({
}; };
}, },
componentWillReceiveProps(nextProps) {
if (this.props.email !== nextProps.email) {
this.setState({
email: nextProps.email
});
}
},
getFormData() { getFormData() {
return this.props.id; return this.props.id;
}, },
@ -115,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,9 +71,10 @@ let CopyrightAssociationForm = React.createClass({
<hr /> <hr />
</Form> </Form>
); );
} else {
return null;
} }
return null;
} }
}); });
export default CopyrightAssociationForm; export default CopyrightAssociationForm;

View File

@ -2,19 +2,20 @@
import React from 'react'; import React from 'react';
import Form from '../ascribe_forms/form'; import ContractListActions from '../../actions/contract_list_actions';
import Property from '../ascribe_forms/property';
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 ContractListActions from '../../actions/contract_list_actions';
import AppConstants from '../../constants/application_constants';
import ApiUrls from '../../constants/api_urls';
import InputFineUploader from './input_fineuploader'; import InputFineUploader from './input_fineuploader';
import Form from '../ascribe_forms/form';
import Property from '../ascribe_forms/property';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import { validationTypes } from '../../constants/uploader_constants';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
@ -78,8 +79,8 @@ let CreateContractForm = React.createClass({
url: ApiUrls.blob_contracts url: ApiUrls.blob_contracts
}} }}
validation={{ validation={{
itemLimit: AppConstants.fineUploader.validation.additionalData.itemLimit, itemLimit: validationTypes.additionalData.itemLimit,
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit, sizeLimit: validationTypes.additionalData.sizeLimit,
allowedExtensions: ['pdf'] allowedExtensions: ['pdf']
}} }}
areAssetsDownloadable={true} areAssetsDownloadable={true}

View File

@ -25,6 +25,7 @@ import { mergeOptions } from '../../utils/general_utils';
let LoanForm = React.createClass({ let LoanForm = React.createClass({
propTypes: { propTypes: {
loanHeading: React.PropTypes.string, loanHeading: React.PropTypes.string,
buttons: React.PropTypes.element,
email: React.PropTypes.string, email: React.PropTypes.string,
gallery: React.PropTypes.string, gallery: React.PropTypes.string,
startDate: React.PropTypes.object, startDate: React.PropTypes.object,
@ -60,6 +61,14 @@ let LoanForm = React.createClass({
}; };
}, },
componentWillReceiveProps(nextProps) {
if (this.props.email !== nextProps.email) {
this.setState({
email: nextProps.email
});
}
},
onChange(state) { onChange(state) {
this.setState(state); this.setState(state);
}, },
@ -80,7 +89,11 @@ let LoanForm = React.createClass({
}, },
getButtons() { getButtons() {
if(this.props.loanHeading) { const { buttons, loanHeading } = this.props;
if (buttons) {
return buttons;
} else if (loanHeading) {
return ( return (
<button <button
type="submit" type="submit"
@ -158,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>
@ -197,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,8 +76,7 @@ let LoginForm = React.createClass({
<input <input
type="email" type="email"
placeholder={getLangText('Enter your email')} placeholder={getLangText('Enter your email')}
name="email" defaultValue={emailQuery}
defaultValue={email}
required/> required/>
</Property> </Property>
<Property <Property
@ -103,7 +85,6 @@ let LoginForm = React.createClass({
<input <input
type="password" type="password"
placeholder={getLangText('Enter your password')} placeholder={getLangText('Enter your password')}
name="password"
required/> required/>
</Property> </Property>
</Form> </Form>

View File

@ -13,43 +13,48 @@ import InputTextAreaToggable from './input_textarea_toggable';
let PieceExtraDataForm = React.createClass({ let PieceExtraDataForm = React.createClass({
propTypes: { propTypes: {
pieceId: React.PropTypes.number, name: React.PropTypes.string.isRequired,
pieceId: React.PropTypes.number.isRequired,
convertLinks: React.PropTypes.bool,
editable: React.PropTypes.bool,
extraData: React.PropTypes.object, extraData: React.PropTypes.object,
handleSuccess: React.PropTypes.func, handleSuccess: React.PropTypes.func,
name: React.PropTypes.string, title: React.PropTypes.string
title: React.PropTypes.string,
editable: React.PropTypes.bool
}, },
getFormData() { getFormData() {
let extradata = {};
extradata[this.props.name] = this.refs.form.refs[this.props.name].state.value;
return { return {
extradata: extradata, extradata: {
[this.props.name]: this.refs.form.refs[this.props.name].state.value
},
piece_id: this.props.pieceId piece_id: this.props.pieceId
}; };
}, },
render() { render() {
let defaultValue = this.props.extraData[this.props.name] || ''; const { convertLinks, editable, extraData, handleSuccess, name, pieceId, title } = this.props;
if (defaultValue.length === 0 && !this.props.editable){ const defaultValue = (extraData && extraData[name]) || null;
if (!defaultValue && !editable) {
return null; return null;
} }
let url = requests.prepareUrl(ApiUrls.piece_extradata, {piece_id: this.props.pieceId});
return ( return (
<Form <Form
ref='form' ref='form'
url={url} disabled={!editable}
handleSuccess={this.props.handleSuccess}
getFormData={this.getFormData} getFormData={this.getFormData}
disabled={!this.props.editable}> handleSuccess={handleSuccess}
url={requests.prepareUrl(ApiUrls.piece_extradata, { piece_id: pieceId })}>
<Property <Property
name={this.props.name} name={name}
label={this.props.title}> label={title}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
convertLinks={convertLinks}
defaultValue={defaultValue} defaultValue={defaultValue}
placeholder={getLangText('Fill in%s', ' ') + this.props.title} placeholder={getLangText('Fill in%s', ' ') + title}
required /> required />
</Property> </Property>
<hr /> <hr />

View File

@ -2,39 +2,40 @@
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 UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
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 { FileStatus } from '../ascribe_uploader/react_s3_fine_uploader_utils';
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
import AscribeSpinner from '../ascribe_spinner'; import AscribeSpinner from '../ascribe_spinner';
import ApiUrls from '../../constants/api_urls'; import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_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),
@ -51,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);
}, },
/** /**
@ -112,6 +97,11 @@ let RegisterPieceForm = React.createClass({
); );
}, },
handleThumbnailValidationFailed(thumbnailFile) {
// If the validation fails, set the thumbnail as submittable since its optional
this.refs.submitButton.setReadyStateForKey('thumbnailKeyReady', true);
},
isThumbnailDialogExpanded() { isThumbnailDialogExpanded() {
const { enableSeparateThumbnail } = this.props; const { enableSeparateThumbnail } = this.props;
const { digitalWorkFile } = this.state; const { digitalWorkFile } = this.state;
@ -127,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;
@ -178,7 +169,7 @@ let RegisterPieceForm = React.createClass({
createBlobRoutine={{ createBlobRoutine={{
url: ApiUrls.blob_digitalworks url: ApiUrls.blob_digitalworks
}} }}
validation={AppConstants.fineUploader.validation.registerWork} validation={validationTypes.registerWork}
setIsUploadReady={this.setIsUploadReady('digitalWorkKeyReady')} setIsUploadReady={this.setIsUploadReady('digitalWorkKeyReady')}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
isFineUploaderActive={isFineUploaderActive} isFineUploaderActive={isFineUploaderActive}
@ -198,15 +189,16 @@ let RegisterPieceForm = React.createClass({
url: ApiUrls.blob_thumbnails url: ApiUrls.blob_thumbnails
}} }}
handleChangedFile={this.handleChangedThumbnail} handleChangedFile={this.handleChangedThumbnail}
onValidationFailed={this.handleThumbnailValidationFailed}
isReadyForFormSubmission={formSubmissionValidation.fileOptional} isReadyForFormSubmission={formSubmissionValidation.fileOptional}
keyRoutine={{ keyRoutine={{
url: AppConstants.serverUrl + 's3/key/', url: AppConstants.serverUrl + 's3/key/',
fileClass: 'thumbnail' fileClass: 'thumbnail'
}} }}
validation={{ validation={{
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit, itemLimit: validationTypes.workThumbnail.itemLimit,
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit, sizeLimit: validationTypes.workThumbnail.sizeLimit,
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif'] allowedExtensions: validationParts.allowedExtensions.images
}} }}
setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')} setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')}
fileClassToUpload={{ fileClassToUpload={{

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

@ -58,11 +58,15 @@ let SendContractAgreementForm = React.createClass({
notification = new GlobalNotificationModel(notification, 'success', 10000); notification = new GlobalNotificationModel(notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
this.history.pushState(null, '/collection'); this.history.push('/collection');
}, },
getFormData(){ getFormData() {
return {'appendix': {'default': this.refs.form.refs.appendix.state.value}}; const appendixValue = this.refs.form.refs.appendix.state.value;
if (appendixValue) {
return { 'appendix': { 'default': appendixValue } };
}
}, },
getContracts() { getContracts() {

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,27 +40,11 @@ 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
this.props.handleSuccess(getLangText('We sent an email to your address') + ' ' + response.user.email + ', ' + getLangText('please confirm') + '.'); this.props.handleSuccess(getLangText('We sent an email to your address') + ' ' + response.user.email + ', ' + getLangText('please confirm') + '.');
} else { } else {
@ -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

@ -25,49 +25,20 @@ const InputFineUploader = React.createClass({
// Props for ReactS3FineUploader // Props for ReactS3FineUploader
areAssetsDownloadable: bool, areAssetsDownloadable: bool,
setWarning: func, createBlobRoutine: ReactS3FineUploader.propTypes.createBlobRoutine,
showErrorPrompt: bool, enableLocalHashing: bool,
fileClassToUpload: ReactS3FineUploader.propTypes.fileClassToUpload,
fileInputElement: ReactS3FineUploader.propTypes.fileInputElement,
isReadyForFormSubmission: func,
keyRoutine: ReactS3FineUploader.propTypes.keyRoutine,
handleChangedFile: func, // TODO: rename to onChangedFile handleChangedFile: func, // TODO: rename to onChangedFile
submitFile: func, // TODO: rename to onSubmitFile submitFile: func, // TODO: rename to onSubmitFile
onValidationFailed: func,
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
isReadyForFormSubmission: func, setWarning: func,
showErrorPrompt: bool,
enableLocalHashing: bool,
uploadMethod: oneOf(['hash', 'upload']), uploadMethod: oneOf(['hash', 'upload']),
validation: ReactS3FineUploader.propTypes.validation,
fileClassToUpload: shape({
singular: string,
plural: string
}),
fileInputElement: oneOfType([
func,
element
]),
keyRoutine: shape({
url: string,
fileClass: string,
pieceId: oneOfType([
string,
number
])
}),
createBlobRoutine: shape({
url: string,
pieceId: oneOfType([
string,
number
])
}),
validation: shape({
itemLimit: number,
sizeLimit: string,
allowedExtensions: arrayOf(string)
})
}, },
getDefaultProps() { getDefaultProps() {
@ -121,6 +92,7 @@ const InputFineUploader = React.createClass({
isFineUploaderActive, isFineUploaderActive,
isReadyForFormSubmission, isReadyForFormSubmission,
keyRoutine, keyRoutine,
onValidationFailed,
setIsUploadReady, setIsUploadReady,
setWarning, setWarning,
showErrorPrompt, showErrorPrompt,
@ -142,6 +114,7 @@ const InputFineUploader = React.createClass({
createBlobRoutine={createBlobRoutine} createBlobRoutine={createBlobRoutine}
validation={validation} validation={validation}
submitFile={this.submitFile} submitFile={this.submitFile}
onValidationFailed={onValidationFailed}
setIsUploadReady={setIsUploadReady} setIsUploadReady={setIsUploadReady}
isReadyForFormSubmission={isReadyForFormSubmission} isReadyForFormSubmission={isReadyForFormSubmission}
areAssetsDownloadable={areAssetsDownloadable} areAssetsDownloadable={areAssetsDownloadable}

View File

@ -4,17 +4,20 @@ import React from 'react';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import { anchorize } from '../../utils/dom_utils';
let InputTextAreaToggable = React.createClass({ let InputTextAreaToggable = React.createClass({
propTypes: { propTypes: {
autoFocus: React.PropTypes.bool, autoFocus: React.PropTypes.bool,
disabled: React.PropTypes.bool, convertLinks: React.PropTypes.bool,
rows: React.PropTypes.number.isRequired,
required: React.PropTypes.bool,
defaultValue: React.PropTypes.string, defaultValue: React.PropTypes.string,
placeholder: React.PropTypes.string, disabled: React.PropTypes.bool,
onBlur: React.PropTypes.func, onBlur: React.PropTypes.func,
onChange: React.PropTypes.func onChange: React.PropTypes.func,
placeholder: React.PropTypes.string,
required: React.PropTypes.bool,
rows: React.PropTypes.number.isRequired
}, },
getInitialState() { getInitialState() {
@ -36,7 +39,7 @@ let InputTextAreaToggable = React.createClass({
componentDidUpdate() { componentDidUpdate() {
// If the initial value of state.value is null, we want to set props.defaultValue // If the initial value of state.value is null, we want to set props.defaultValue
// as a value. In all other cases TextareaAutosize.onChange is updating.handleChange already // as a value. In all other cases TextareaAutosize.onChange is updating.handleChange already
if(this.state.value === null && this.props.defaultValue) { if (this.state.value === null && this.props.defaultValue) {
this.setState({ this.setState({
value: this.props.defaultValue value: this.props.defaultValue
}); });
@ -49,28 +52,26 @@ let InputTextAreaToggable = React.createClass({
}, },
render() { render() {
let className = 'form-control ascribe-textarea'; const { convertLinks, disabled, onBlur, placeholder, required, rows } = this.props;
let textarea = null; const { value } = this.state;
if(!this.props.disabled) { if (!disabled) {
className = className + ' ascribe-textarea-editable'; return (
textarea = (
<TextareaAutosize <TextareaAutosize
ref='textarea' ref='textarea'
className={className} className='form-control ascribe-textarea ascribe-textarea-editable'
value={this.state.value} value={value}
rows={this.props.rows} rows={rows}
maxRows={10} maxRows={10}
required={this.props.required} required={required}
onChange={this.handleChange} onChange={this.handleChange}
onBlur={this.props.onBlur} onBlur={onBlur}
placeholder={this.props.placeholder} /> placeholder={placeholder} />
); );
} else { } else {
textarea = <pre className="ascribe-pre">{this.state.value}</pre>; // Can only convert links when not editable, as textarea does not support anchors
return <pre className="ascribe-pre">{convertLinks ? anchorize(value) : value}</pre>;
} }
return textarea;
} }
}); });

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

@ -244,7 +244,17 @@ const Property = React.createClass({
}, },
handleCheckboxToggle() { handleCheckboxToggle() {
this.setExpanded(!this.state.expanded); const expanded = !this.state.expanded;
this.setExpanded(expanded);
// Reset the value to be the initial value when the checkbox is unticked since the
// user doesn't want to specify their own value.
if (!expanded) {
this.setState({
value: this.state.initialValue
});
}
}, },
renderChildren(style) { renderChildren(style) {
@ -289,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

@ -184,7 +184,7 @@ let Video = React.createClass({
); );
} else { } else {
return ( return (
<Image src={this.props.preview} /> <Image preview={this.props.preview} />
); );
} }
} }

View File

@ -1,23 +1,24 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import ReactAddons from 'react/addons';
import Modal from 'react-bootstrap/lib/Modal'; import Modal from 'react-bootstrap/lib/Modal';
let ModalWrapper = React.createClass({ let ModalWrapper = React.createClass({
propTypes: { propTypes: {
trigger: React.PropTypes.element, 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,
React.PropTypes.string React.PropTypes.string
]).isRequired, ]).isRequired,
handleSuccess: React.PropTypes.func.isRequired,
children: React.PropTypes.oneOfType([ handleCancel: React.PropTypes.func,
React.PropTypes.arrayOf(React.PropTypes.element), handleSuccess: React.PropTypes.func,
React.PropTypes.element trigger: React.PropTypes.element
])
}, },
getInitialState() { getInitialState() {
@ -38,15 +39,32 @@ let ModalWrapper = React.createClass({
}); });
}, },
handleCancel() {
if (typeof this.props.handleCancel === 'function') {
this.props.handleCancel();
}
this.hide();
},
handleSuccess(response) { handleSuccess(response) {
this.props.handleSuccess(response); if (typeof this.props.handleSuccess === 'function') {
this.props.handleSuccess(response);
}
this.hide(); this.hide();
}, },
renderChildren() { renderChildren() {
return ReactAddons.Children.map(this.props.children, (child) => { return React.Children.map(this.props.children, (child) => {
return ReactAddons.addons.cloneWithProps(child, { return React.cloneElement(child, {
handleSuccess: this.handleSuccess handleSuccess: (response) => {
if (typeof child.props.handleSuccess === 'function') {
child.props.handleSuccess(response);
}
this.handleSuccess(response);
}
}); });
}); });
}, },
@ -54,14 +72,23 @@ let ModalWrapper = React.createClass({
render() { render() {
const { trigger, title } = this.props; const { trigger, title } = this.props;
// If the trigger component exists, we add the ModalWrapper's show() as its onClick method. // If the trigger component exists, we add the ModalWrapper's show() to its onClick method.
// The trigger component should, in most cases, be a button. // The trigger component should, in most cases, be a button.
const clonedTrigger = React.isValidElement(trigger) ? React.cloneElement(trigger, {onClick: this.show}) const clonedTrigger = React.isValidElement(trigger) ?
: null; React.cloneElement(trigger, {
onClick: (...params) => {
if (typeof trigger.props.onClick === 'function') {
trigger.props.onClick(...params);
}
this.show();
}
}) : null;
return ( return (
<span> <span>
{clonedTrigger} {clonedTrigger}
<Modal show={this.state.showModal} onHide={this.hide}> <Modal show={this.state.showModal} onHide={this.handleCancel}>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title> <Modal.Title>
{title} {title}

View File

@ -39,30 +39,6 @@ let PieceListToolbar = React.createClass({
]) ])
}, },
getFilterWidget(){
if (this.props.filterParams){
return (
<PieceListToolbarFilterWidget
filterParams={this.props.filterParams}
filterBy={this.props.filterBy}
applyFilterBy={this.props.applyFilterBy} />
);
}
return null;
},
getOrderWidget(){
if (this.props.orderParams){
return (
<PieceListToolbarOrderWidget
orderParams={this.props.orderParams}
orderBy={this.props.orderBy}
applyOrderBy={this.props.applyOrderBy}/>
);
}
return null;
},
render() { render() {
const { className, children, searchFor, searchQuery } = this.props; const { className, children, searchFor, searchQuery } = this.props;
@ -75,8 +51,14 @@ let PieceListToolbar = React.createClass({
{children} {children}
</span> </span>
<span className="pull-right"> <span className="pull-right">
{this.getOrderWidget()} <PieceListToolbarOrderWidget
{this.getFilterWidget()} orderParams={this.props.orderParams}
orderBy={this.props.orderBy}
applyOrderBy={this.props.applyOrderBy}/>
<PieceListToolbarFilterWidget
filterParams={this.props.filterParams}
filterBy={this.props.filterBy}
applyFilterBy={this.props.applyFilterBy} />
</span> </span>
<SearchBar <SearchBar
className="pull-right search-bar ascribe-input-glyph" className="pull-right search-bar ascribe-input-glyph"

View File

@ -30,15 +30,15 @@ let PieceListToolbarFilterWidget = React.createClass({
generateFilterByStatement(param) { generateFilterByStatement(param) {
const filterBy = Object.assign({}, this.props.filterBy); const filterBy = Object.assign({}, this.props.filterBy);
if(filterBy) { if (filterBy) {
// we need hasOwnProperty since the values are all booleans // we need hasOwnProperty since the values are all booleans
if(filterBy.hasOwnProperty(param)) { if (filterBy.hasOwnProperty(param)) {
filterBy[param] = !filterBy[param]; filterBy[param] = !filterBy[param];
// if the parameter is false, then we want to remove it again // if the parameter is false, then we want to remove it again
// from the list of queryParameters as this component is only about // from the list of queryParameters as this component is only about
// which actions *CAN* be done and not what *CANNOT* // which actions *CAN* be done and not what *CANNOT*
if(!filterBy[param]) { if (!filterBy[param]) {
delete filterBy[param]; delete filterBy[param];
} }
@ -66,7 +66,7 @@ let PieceListToolbarFilterWidget = React.createClass({
// We're hiding the star in that complicated matter so that, // We're hiding the star in that complicated matter so that,
// the surrounding button is not resized up on appearance // the surrounding button is not resized up on appearance
if(trueValuesOnly.length > 0) { if (trueValuesOnly.length) {
return { visibility: 'visible'}; return { visibility: 'visible'};
} else { } else {
return { visibility: 'hidden' }; return { visibility: 'hidden' };
@ -81,62 +81,67 @@ let PieceListToolbarFilterWidget = React.createClass({
</span> </span>
); );
return ( if (this.props.filterParams && this.props.filterParams.length) {
<DropdownButton return (
pullRight={true} <DropdownButton
title={filterIcon} id="ascribe-piece-list-toolbar-filter-widget-dropdown"
className="ascribe-piece-list-toolbar-filter-widget"> pullRight={true}
{/* We iterate over filterParams, to receive the label and then for each title={filterIcon}
label also iterate over its items, to get all filterable options */} className="ascribe-piece-list-toolbar-filter-widget">
{this.props.filterParams.map(({ label, items }, i) => { {/* We iterate over filterParams, to receive the label and then for each
return ( label also iterate over its items, to get all filterable options */}
<div> {this.props.filterParams.map(({ label, items }, i) => {
<li return (
style={{'textAlign': 'center'}} <div key={label}>
key={i}> <li style={{'textAlign': 'center'}}>
<em>{label}:</em> <em>{label}:</em>
</li> </li>
{items.map((param, j) => { {items.map((paramItem) => {
let itemLabel;
let param;
// As can be seen in the PropTypes, a param can either // As can be seen in the PropTypes, a param can either
// be a string or an object of the shape: // be a string or an object of the shape:
// //
// { // {
// key: <String>, // key: <String>,
// label: <String> // label: <String>
// } // }
// //
// This is why we need to distinguish between both here. // This is why we need to distinguish between both here.
if(typeof param !== 'string') { if (typeof paramItem !== 'string') {
label = param.label; param = paramItem.key;
param = param.key; itemLabel = paramItem.label;
} else { } else {
param = param; param = paramItem;
label = param.split('acl_')[1].replace(/_/g, ' '); itemLabel = paramItem.split('acl_')[1].replace(/_/g, ' ');
} }
return ( return (
<li <li
key={j} key={itemLabel}
onClick={this.filterBy(param)} onClick={this.filterBy(param)}
className="filter-widget-item"> className="filter-widget-item">
<div className="checkbox-line"> <div className="checkbox-line">
<span> <span>
{getLangText(label)} {getLangText(itemLabel)}
</span> </span>
<input <input
readOnly readOnly
type="checkbox" type="checkbox"
checked={this.props.filterBy[param]} /> checked={this.props.filterBy[param]} />
</div> </div>
</li> </li>
); );
})} })}
</div> </div>
); );
})} })}
</DropdownButton> </DropdownButton>
); );
} else {
return null;
}
} }
}); });

View File

@ -37,7 +37,7 @@ let PieceListToolbarOrderWidget = React.createClass({
isOrderActive() { isOrderActive() {
// We're hiding the star in that complicated matter so that, // We're hiding the star in that complicated matter so that,
// the surrounding button is not resized up on appearance // the surrounding button is not resized up on appearance
if(this.props.orderBy.length > 0) { if (this.props.orderBy && this.props.orderBy.length) {
return { visibility: 'visible'}; return { visibility: 'visible'};
} else { } else {
return { visibility: 'hidden' }; return { visibility: 'hidden' };
@ -45,24 +45,25 @@ let PieceListToolbarOrderWidget = React.createClass({
}, },
render() { render() {
let filterIcon = ( let orderIcon = (
<span> <span>
<span className="ascribe-icon icon-ascribe-sort" aria-hidden="true"></span> <span className="ascribe-icon icon-ascribe-sort" aria-hidden="true"></span>
<span style={this.isOrderActive()}>&middot;</span> <span style={this.isOrderActive()}>&middot;</span>
</span> </span>
); );
return (
<DropdownButton if (this.props.orderParams && this.props.orderParams.length) {
pullRight={true} return (
title={filterIcon} <DropdownButton
className="ascribe-piece-list-toolbar-filter-widget"> id="ascribe-piece-list-toolbar-order-widget-dropdown"
<li style={{'textAlign': 'center'}}> pullRight={true}
<em>{getLangText('Sort by')}:</em> className="ascribe-piece-list-toolbar-filter-widget"
</li> title={orderIcon}>
{this.props.orderParams.map((param) => { <li style={{'textAlign': 'center'}}>
return ( <em>{getLangText('Sort by')}:</em>
<div> </li>
{this.props.orderParams.map((param) => {
return (
<li <li
key={param} key={param}
onClick={this.orderBy(param)} onClick={this.orderBy(param)}
@ -77,12 +78,14 @@ let PieceListToolbarOrderWidget = React.createClass({
checked={param.indexOf(this.props.orderBy) > -1} /> checked={param.indexOf(this.props.orderBy) > -1} />
</div> </div>
</li> </li>
</div> );
); })}
})} </DropdownButton>
</DropdownButton> );
); } else {
return null;
}
} }
}); });
export default PieceListToolbarOrderWidget; export default PieceListToolbarOrderWidget;

View File

@ -0,0 +1,126 @@
'use strict';
import React from 'react';
import { RouteContext } from 'react-router';
import history from '../../history';
import UserStore from '../../stores/user_store';
import AppConstants from '../../constants/application_constants';
const { object } = React.PropTypes;
const WHEN_ENUM = ['loggedIn', 'loggedOut'];
/**
* Redirects the user conditionally according to his authentication
*
* @param {enum/string} options.when ('loggedIn' || 'loggedOut')
*/
export function AuthRedirect({to, when}) {
// validate `when`, must be contained in `WHEN_ENUM`.
// Throw an error otherwise.
if (WHEN_ENUM.indexOf(when) === -1) {
const whenValues = WHEN_ENUM.join(', ');
throw new Error(`"when" must be one of: [${whenValues}] got "${when}" instead`);
}
return function(currentUser, query) {
const { redirectAuthenticated, redirect } = query;
// The user of this handler specifies with `when`, what kind of status
// needs to be checked to conditionally do - if that state is `true` -
// a redirect.
//
// So if when === 'loggedIn', we're checking if the user is logged in (and
// vice versa)
const isLoggedIn = Object.keys(currentUser).length && currentUser.email;
const exprToValidate = when === 'loggedIn' ? isLoggedIn : !isLoggedIn;
// and redirect if `true`.
if (exprToValidate) {
window.setTimeout(() => history.replace({ query, pathname: to }));
return true;
// 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
} else if (!exprToValidate && when === 'loggedIn' && redirect) {
delete query.redirect;
window.setTimeout(() => history.replace({ query, pathname: '/' + redirect }));
return true;
} else if (!exprToValidate && when === 'loggedOut' && redirectAuthenticated) {
/*
* redirectAuthenticated contains an arbitrary path
* eg pieces/<id>, editions/<bitcoin_id>, collection, settings, ...
* hence transitionTo cannot be used directly.
*
* While we're getting rid of `query.redirect` explicitly in the
* above `else if` statement, here it's sufficient to just call
* `baseUrl` + `redirectAuthenticated`, as it gets rid of queries as well.
*/
window.location = AppConstants.baseUrl + redirectAuthenticated;
return true;
}
return false;
};
}
/**
* Can be used in combination with `Route` as an intermediate Handler
* between the actual component we want to display dependent on a certain state
* that is required to display that component.
*
* @param {[function]} redirectFn A function that conditionally redirects
*/
export function ProxyHandler(...redirectFunctions) {
return (Component) => {
return React.createClass({
displayName: 'ProxyHandler',
propTypes: {
// Provided from AscribeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object,
// Provided from router
location: object
},
// We need insert `RouteContext` here in order to be able
// to use the `Lifecycle` widget in further down nested components
mixins: [RouteContext],
componentDidMount() {
this.evaluateRedirectFunctions();
},
componentWillReceiveProps(nextProps) {
this.evaluateRedirectFunctions(nextProps);
},
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,
// it should return `true` and therefore
// stop/avoid the execution of all functions
// that follow
if (redirectFunctions[i](currentUser, query)) {
break;
}
}
}
},
render() {
return (
<Component {...this.props} />
);
}
});
};
}

View File

@ -1,115 +0,0 @@
'use strict';
import React from 'react';
import { History, RouteContext } from 'react-router';
import UserStore from '../../../stores/user_store';
import UserActions from '../../../actions/user_actions';
import AppConstants from '../../../constants/application_constants';
const { object } = React.PropTypes;
const WHEN_ENUM = ['loggedIn', 'loggedOut'];
/**
* Can be used in combination with `Route` as an intermediate Handler
* between the actual component we want to display dependent on a certain state
* that is required to display that component.
*
* @param {string} options.to Any type of route path that is defined in routes.js
* @param {enum/string} options.when ('loggedIn' || 'loggedOut')
*/
export default function AuthProxyHandler({to, when}) {
// validate `when`, must be contained in `WHEN_ENUM`.
// Throw an error otherwise.
if(WHEN_ENUM.indexOf(when) === -1) {
let whenValues = WHEN_ENUM.join(', ');
throw new Error(`"when" must be one of: [${whenValues}] got "${when}" instead`);
}
return (Component) => {
return React.createClass({
displayName: 'AuthProxyHandler',
propTypes: {
location: object
},
// We need insert `RouteContext` here in order to be able
// to use the `Lifecycle` widget in further down nested components
mixins: [History, RouteContext],
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentDidUpdate() {
// Only refresh this component, when UserSources are not loading
// data from the server
if(!UserStore.isLoading()) {
this.redirectConditionally();
}
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
redirectConditionally() {
const { query } = this.props.location;
const { redirectAuthenticated, redirect } = query;
// The user of this handler specifies with `when`, what kind of status
// needs to be checked to conditionally do - if that state is `true` -
// a redirect.
//
// So if when === 'loggedIn', we're checking if the user is logged in (and
// vice versa)
let exprToValidate = when === 'loggedIn' ?
this.state.currentUser && this.state.currentUser.email :
this.state.currentUser && !this.state.currentUser.email;
// and redirect if `true`.
if(exprToValidate) {
window.setTimeout(() => this.history.replaceState(null, to, query));
// 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
} else if(!exprToValidate && when === 'loggedIn' && redirect) {
delete query.redirect;
window.setTimeout(() => this.history.replaceState(null, '/' + redirect, query));
} else if(!exprToValidate && when === 'loggedOut' && redirectAuthenticated) {
/*
* redirectAuthenticated contains an arbirary path
* eg pieces/<id>, editions/<bitcoin_id>, collection, settings, ...
* hence transitionTo cannot be used directly.
*
* While we're getting rid of `query.redirect` explicitly in the
* above `else if` statement, here it's sufficient to just call
* `baseUrl` + `redirectAuthenticated`, as it gets rid of queries as well.
*/
window.location = AppConstants.baseUrl + redirectAuthenticated;
}
},
onChange(state) {
this.setState(state);
},
render() {
return (
<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,34 +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: { propTypes: {
// Provided from AscribeApp
currentUser: React.PropTypes.object.isRequired,
whitelabel: React.PropTypes.object.isRequired,
// Provided from router
location: React.PropTypes.object 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);
}, },
@ -64,40 +53,40 @@ let ContractSettings = React.createClass({
ContractListActions.removeContract(contract.id) ContractListActions.removeContract(contract.id)
.then((response) => { .then((response) => {
ContractListActions.fetchContractList(true); ContractListActions.fetchContractList(true);
let notification = new GlobalNotificationModel(response.notification, 'success', 4000); const notification = new GlobalNotificationModel(response.notification, 'success', 4000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}) })
.catch((err) => { .catch((err) => {
let notification = new GlobalNotificationModel(err, 'danger', 10000); const notification = new GlobalNotificationModel(err, 'danger', 10000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}); });
}; };
}, },
getPublicContracts(){ getPublicContracts() {
return this.state.contractList.filter((contract) => contract.is_public); return this.state.contractList.filter((contract) => contract.is_public);
}, },
getPrivateContracts(){ getPrivateContracts() {
return this.state.contractList.filter((contract) => !contract.is_public); return this.state.contractList.filter((contract) => !contract.is_public);
}, },
render() { render() {
let publicContracts = this.getPublicContracts(); const { currentUser, location, whitelabel } = this.props;
let privateContracts = this.getPrivateContracts(); const publicContracts = this.getPublicContracts();
const privateContracts = this.getPrivateContracts();
let createPublicContractForm = null; let createPublicContractForm = null;
setDocumentTitle(getLangText('Contracts settings')); setDocumentTitle(getLangText('Contracts settings'));
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'
}} }}
location={this.props.location}/> isPublic={true} />
); );
} }
@ -108,23 +97,21 @@ 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) => {
return ( return (
<ActionPanel <ActionPanel
key={i} key={contract.id}
title={contract.name} title={contract.name}
content={truncateTextAtCharIndex(contract.name, 120, '(...).pdf')} content={truncateTextAtCharIndex(contract.name, 120, '(...).pdf')}
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}
location={this.props.location}/>
</AclProxy> </AclProxy>
<a <a
className="btn btn-default btn-sm margin-left-2px" className="btn btn-default btn-sm margin-left-2px"
@ -147,29 +134,26 @@ 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} />
location={this.props.location}/>
{privateContracts.map((contract, i) => { {privateContracts.map((contract, i) => {
return ( return (
<ActionPanel <ActionPanel
key={i} key={contract.id}
title={contract.name} title={contract.name}
content={truncateTextAtCharIndex(contract.name, 120, '(...).pdf')} content={truncateTextAtCharIndex(contract.name, 120, '(...).pdf')}
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}
location={this.props.location}/>
</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,17 +2,18 @@
import React from 'react'; import React from 'react';
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
import AppConstants from '../../constants/application_constants';
import ApiUrls from '../../constants/api_urls';
import ContractListActions from '../../actions/contract_list_actions'; import ContractListActions from '../../actions/contract_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 ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import { validationTypes } from '../../constants/uploader_constants';
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
import { getCookie } from '../../utils/fetch_api_utils'; import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
@ -24,71 +25,75 @@ let ContractSettingsUpdateButton = React.createClass({
}, },
submitFile(file) { submitFile(file) {
let contract = this.props.contract;
// override the blob with the key's value // override the blob with the key's value
contract.blob = file.key; const contract = Object.assign(this.props.contract, { blob: file.key });
// send it to the server // send it to the server
ContractListActions ContractListActions
.changeContract(contract) .changeContract(contract)
.then((res) => { .then((res) => {
// Display feedback to the user // Display feedback to the user
let notification = new GlobalNotificationModel(getLangText('Contract %s successfully updated', res.name), 'success', 5000); const notification = new GlobalNotificationModel(getLangText('Contract %s successfully updated', contract.name), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
// and refresh the contract list to get the updated contracs // and refresh the contract list to get the updated contracs
return ContractListActions.fetchContractList(true); return ContractListActions
}) .fetchContractList(true)
.then(() => { // Also, reset the fineuploader component if fetch is successful so that the user can again 'update' his contract
// Also, reset the fineuploader component so that the user can again 'update' his contract .then(this.refs.fineuploader.reset)
this.refs.fineuploader.reset(); .catch((err) => {
}) const notification = new GlobalNotificationModel(getLangText('Latest contract failed to load'), 'danger', 5000);
.catch((err) => { GlobalNotificationActions.appendGlobalNotification(notification);
console.logGlobal(err);
let notification = new GlobalNotificationModel(getLangText('Contract could not be updated'), 'success', 5000); return Promise.reject(err);
});
}, (err) => {
const notification = new GlobalNotificationModel(getLangText('Contract could not be updated'), 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
});
return Promise.reject(err);
})
.catch(console.logGlobal);
}, },
render() { render() {
return ( return (
<ReactS3FineUploader <ReactS3FineUploader
fileInputElement={UploadButton()} ref='fineuploader'
keyRoutine={{ fileInputElement={UploadButton({ showLabel: false })}
url: AppConstants.serverUrl + 's3/key/', keyRoutine={{
fileClass: 'contract' url: AppConstants.serverUrl + 's3/key/',
}} fileClass: 'contract'
createBlobRoutine={{ }}
url: ApiUrls.blob_contracts createBlobRoutine={{
}} url: ApiUrls.blob_contracts
validation={{ }}
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit, validation={{
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit, itemLimit: validationTypes.registerWork.itemLimit,
allowedExtensions: ['pdf'] sizeLimit: validationTypes.additionalData.sizeLimit,
}} allowedExtensions: ['pdf']
setIsUploadReady={() =>{/* So that ReactS3FineUploader is not complaining */}} }}
signature={{ setIsUploadReady={() =>{/* So that ReactS3FineUploader is not complaining */}}
endpoint: AppConstants.serverUrl + 's3/signature/', signature={{
customHeaders: { endpoint: AppConstants.serverUrl + 's3/signature/',
'X-CSRFToken': getCookie(AppConstants.csrftoken) customHeaders: {
} 'X-CSRFToken': getCookie(AppConstants.csrftoken)
}} }
deleteFile={{ }}
enabled: true, deleteFile={{
method: 'DELETE', enabled: true,
endpoint: AppConstants.serverUrl + 's3/delete', method: 'DELETE',
customHeaders: { endpoint: AppConstants.serverUrl + 's3/delete',
'X-CSRFToken': getCookie(AppConstants.csrftoken) customHeaders: {
} 'X-CSRFToken': getCookie(AppConstants.csrftoken)
}} }
fileClassToUpload={{ }}
singular: getLangText('UPDATE'), fileClassToUpload={{
plural: getLangText('UPDATE') singular: getLangText('UPDATE'),
}} plural: getLangText('UPDATE')
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} }}
submitFile={this.submitFile} /> isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
submitFile={this.submitFile} />
); );
} }
}); });

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

@ -34,7 +34,6 @@ let WebhookSettings = React.createClass({
componentDidMount() { componentDidMount() {
WebhookStore.listen(this.onChange); WebhookStore.listen(this.onChange);
WebhookActions.fetchWebhooks(); WebhookActions.fetchWebhooks();
WebhookActions.fetchWebhookEvents();
}, },
componentWillUnmount() { componentWillUnmount() {
@ -49,7 +48,7 @@ let WebhookSettings = React.createClass({
return (event) => { return (event) => {
WebhookActions.removeWebhook(webhookId); WebhookActions.removeWebhook(webhookId);
let notification = new GlobalNotificationModel(getLangText('Webhook deleted'), 'success', 2000); const notification = new GlobalNotificationModel(getLangText('Webhook deleted'), 'success', 2000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}; };
}, },
@ -57,16 +56,16 @@ let WebhookSettings = React.createClass({
handleCreateSuccess() { handleCreateSuccess() {
this.refs.webhookCreateForm.reset(); this.refs.webhookCreateForm.reset();
WebhookActions.fetchWebhooks(true); WebhookActions.fetchWebhooks(true);
let notification = new GlobalNotificationModel(getLangText('Webhook successfully created'), 'success', 5000);
const notification = new GlobalNotificationModel(getLangText('Webhook successfully created'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
getWebhooks(){ getWebhooks() {
let content = <AscribeSpinner color='dark-blue' size='lg'/>;
if (this.state.webhooks) { if (this.state.webhooks) {
content = this.state.webhooks.map(function(webhook, i) { return this.state.webhooks.map(function(webhook, i) {
const event = webhook.event.split('.')[0]; const event = webhook.event.split('.')[0];
return ( return (
<ActionPanel <ActionPanel
name={webhook.event} name={webhook.event}
@ -91,11 +90,14 @@ let WebhookSettings = React.createClass({
</button> </button>
</div> </div>
</div> </div>
}/> } />
); );
}, this); }, this);
} else {
return (
<AscribeSpinner color='dark-blue' size='lg'/>
);
} }
return content;
}, },
getEvents() { getEvents() {
@ -110,18 +112,18 @@ let WebhookSettings = React.createClass({
<option <option
name={i} name={i}
key={i} key={i}
value={ event + '.webhook' }> value={event + '.webhook'}>
{ event.toUpperCase() } {event.toUpperCase()}
</option> </option>
); );
})} })}
</select> </select>
</Property>); </Property>);
} else {
return null;
} }
return null;
}, },
render() { render() {
return ( return (
<CollapsibleParagraph <CollapsibleParagraph
@ -138,20 +140,19 @@ let WebhookSettings = React.createClass({
a target url. a target url.
</p> </p>
</div> </div>
<AclProxy <AclProxy show={this.state.webhookEvents && this.state.webhookEvents.length}>
show={this.state.webhookEvents && this.state.webhookEvents.length}>
<Form <Form
ref="webhookCreateForm" ref="webhookCreateForm"
url={ApiUrls.webhooks} url={ApiUrls.webhooks}
handleSuccess={this.handleCreateSuccess}> handleSuccess={this.handleCreateSuccess}>
{ this.getEvents() } {this.getEvents()}
<Property <Property
name='target' name='target'
label={getLangText('Redirect Url')}> label={getLangText('Redirect Url')}>
<input <input
type="text" type="text"
placeholder={getLangText('Enter the url to be triggered')} placeholder={getLangText('Enter the url to be triggered')}
required/> required />
</Property> </Property>
<hr /> <hr />
</Form> </Form>
@ -162,4 +163,4 @@ let WebhookSettings = React.createClass({
} }
}); });
export default WebhookSettings; export default WebhookSettings;

View File

@ -57,21 +57,21 @@ const SlidesContainer = React.createClass({
// When the start_from parameter is used, this.setSlideNum can not simply be used anymore. // When the start_from parameter is used, this.setSlideNum can not simply be used anymore.
nextSlide(additionalQueryParams) { nextSlide(additionalQueryParams) {
const slideNum = parseInt(this.props.location.query.slide_num, 10) || 0; const slideNum = parseInt(this.props.location.query.slide_num, 10) || 0;
let nextSlide = slideNum + 1; this.setSlideNum(slideNum + 1, additionalQueryParams);
this.setSlideNum(nextSlide, additionalQueryParams);
}, },
setSlideNum(nextSlideNum, additionalQueryParams = {}) { setSlideNum(nextSlideNum, additionalQueryParams = {}) {
let queryParams = Object.assign(this.props.location.query, additionalQueryParams); const { location: { pathname } } = this.props;
queryParams.slide_num = nextSlideNum; const query = Object.assign({}, this.props.location.query, additionalQueryParams, { slide_num: nextSlideNum });
this.history.pushState(null, this.props.location.pathname, queryParams);
this.history.push({ pathname, query });
}, },
// breadcrumbs are defined as attributes of the slides. // breadcrumbs are defined as attributes of the slides.
// To extract them we have to read the DOM element's attributes // To extract them we have to read the DOM element's attributes
extractBreadcrumbs() { extractBreadcrumbs() {
const startFrom = parseInt(this.props.location.query.start_from, 10) || -1; const startFrom = parseInt(this.props.location.query.start_from, 10) || -1;
let breadcrumbs = []; const breadcrumbs = [];
React.Children.map(this.props.children, (child, i) => { React.Children.map(this.props.children, (child, i) => {
if(child && i >= startFrom && child.props['data-slide-title']) { if(child && i >= startFrom && child.props['data-slide-title']) {
@ -179,4 +179,4 @@ const SlidesContainer = React.createClass({
} }
}); });
export default SlidesContainer; export default SlidesContainer;

View File

@ -2,6 +2,8 @@
import React from 'react'; import React from 'react';
import FacebookHandler from '../../third_party/facebook_handler';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { InjectInHeadUtils } from '../../utils/inject_utils'; import { InjectInHeadUtils } from '../../utils/inject_utils';
@ -17,24 +19,40 @@ let FacebookShareButton = React.createClass({
}; };
}, },
componentDidMount() { getInitialState() {
/** return FacebookHandler.getState();
* Ideally we would only use FB.XFBML.parse() on the component that we're
* mounting, but doing this when we first load the FB sdk causes unpredictable behaviour.
* The button sometimes doesn't get initialized, likely because FB hasn't properly
* been initialized yet.
*
* To circumvent this, we always have the sdk parse the entire DOM on the initial load
* (see FacebookHandler) and then use FB.XFBML.parse() on the mounting component later.
*/
InjectInHeadUtils
.inject(AppConstants.facebook.sdkUrl)
.then(() => { FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement) });
}, },
shouldComponentUpdate(nextProps) { componentDidMount() {
return this.props.type !== nextProps.type; FacebookHandler.listen(this.onChange);
this.loadFacebook();
},
shouldComponentUpdate(nextProps, nextState) {
// Don't update if the props haven't changed or the FB SDK loading status is still the same
return this.props.type !== nextProps.type || nextState.loaded !== this.state.loaded;
},
componentDidUpdate() {
// If the component changes, we need to reparse the share button's XFBML.
// To prevent cases where the Facebook SDK hasn't been loaded yet at this stage,
// let's make sure that it's injected before trying to reparse.
this.loadFacebook();
},
onChange(state) {
this.setState(state);
},
loadFacebook() {
InjectInHeadUtils
.inject(AppConstants.facebook.sdkUrl)
.then(() => {
if (this.state.loaded) {
FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parent)
}
});
}, },
render() { render() {

View File

@ -2,15 +2,15 @@
export class ColumnModel { export class ColumnModel {
// ToDo: Add validation for all passed-in parameters // ToDo: Add validation for all passed-in parameters
constructor(transformFn, columnName, displayName, displayType, rowWidth, canBeOrdered, transition, className) { constructor({ transformFn, columnName = '', displayElement, displayType, rowWidth, canBeOrdered, transition, className = '' }) {
this.transformFn = transformFn; this.transformFn = transformFn;
this.columnName = columnName; this.columnName = columnName;
this.displayName = displayName; this.displayElement = displayElement;
this.displayType = displayType; this.displayType = displayType;
this.rowWidth = rowWidth; this.rowWidth = rowWidth;
this.canBeOrdered = canBeOrdered; this.canBeOrdered = canBeOrdered;
this.transition = transition; this.transition = transition;
this.className = className ? className : ''; this.className = className;
} }
} }
@ -28,7 +28,7 @@ export class ColumnModel {
* our selfes, using this TransitionModel. * our selfes, using this TransitionModel.
*/ */
export class TransitionModel { export class TransitionModel {
constructor(to, queryKey, valueKey, callback) { constructor({ to, queryKey, valueKey, callback }) {
this.to = to; this.to = to;
this.queryKey = queryKey; this.queryKey = queryKey;
this.valueKey = valueKey; this.valueKey = valueKey;
@ -38,4 +38,4 @@ export class TransitionModel {
toReactRouterLink(queryValue) { toReactRouterLink(queryValue) {
return '/' + this.to + '/' + queryValue; return '/' + this.to + '/' + queryValue;
} }
} }

View File

@ -1,5 +1,5 @@
'use strict';
'use strict';
import React from 'react'; import React from 'react';
import TableHeaderItem from './table_header_item'; import TableHeaderItem from './table_header_item';
@ -29,7 +29,7 @@ let TableHeader = React.createClass({
<TableHeaderItem <TableHeaderItem
className={column.className} className={column.className}
key={i} key={i}
displayName={column.displayName} displayElement={column.displayElement}
columnName={columnName} columnName={columnName}
canBeOrdered={canBeOrdered} canBeOrdered={canBeOrdered}
orderAsc={this.props.orderAsc} orderAsc={this.props.orderAsc}

View File

@ -7,7 +7,7 @@ import TableHeaderItemCarret from './table_header_item_carret';
let TableHeaderItem = React.createClass({ let TableHeaderItem = React.createClass({
propTypes: { propTypes: {
displayName: React.PropTypes.oneOfType([ displayElement: React.PropTypes.oneOfType([
React.PropTypes.string, React.PropTypes.string,
React.PropTypes.element React.PropTypes.element
]).isRequired, ]).isRequired,
@ -24,29 +24,31 @@ let TableHeaderItem = React.createClass({
}, },
render() { render() {
if(this.props.canBeOrdered && this.props.changeOrder && this.props.orderAsc != null && this.props.orderBy) { const { canBeOrdered, changeOrder, className, columnName, displayElement, orderAsc, orderBy } = this.props;
if(this.props.columnName === this.props.orderBy) {
if (canBeOrdered && changeOrder && orderAsc != null && orderBy) {
if (columnName === orderBy) {
return ( return (
<th <th
className={'ascribe-table-header-column ' + this.props.className} className={'ascribe-table-header-column ' + className}
onClick={this.changeOrder}> onClick={this.changeOrder}>
<span>{this.props.displayName} <TableHeaderItemCarret orderAsc={this.props.orderAsc} /></span> <span>{displayElement} <TableHeaderItemCarret orderAsc={orderAsc} /></span>
</th> </th>
); );
} else { } else {
return ( return (
<th <th
className={'ascribe-table-header-column ' + this.props.className} className={'ascribe-table-header-column ' + className}
onClick={this.changeOrder}> onClick={this.changeOrder}>
<span>{this.props.displayName}</span> <span>{displayElement}</span>
</th> </th>
); );
} }
} else { } else {
return ( return (
<th className={'ascribe-table-header-column ' + this.props.className}> <th className={'ascribe-table-header-column ' + className}>
<span> <span>
{this.props.displayName} {displayElement}
</span> </span>
</th> </th>
); );

View File

@ -3,15 +3,15 @@
import React from 'react'; import React from 'react';
let TableItemAclFiltered = React.createClass({ const TableItemAclFiltered = React.createClass({
propTypes: { propTypes: {
content: React.PropTypes.object, content: React.PropTypes.object,
notifications: React.PropTypes.string notifications: React.PropTypes.array
}, },
render() { render() {
var availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete']; const availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete'];
if (this.props.notifications && this.props.notifications.length > 0){ if (this.props.notifications && this.props.notifications.length) {
return ( return (
<span> <span>
{this.props.notifications[0].action_str} {this.props.notifications[0].action_str}
@ -19,15 +19,14 @@ let TableItemAclFiltered = React.createClass({
); );
} }
let filteredAcls = Object.keys(this.props.content).filter((key) => { const filteredAcls = Object.keys(this.props.content)
return availableAcls.indexOf(key) > -1 && this.props.content[key]; .filter((key) => availableAcls.indexOf(key) > -1 && this.props.content[key])
}); .map((acl) => acl.split('acl_')[1])
.join('/');
filteredAcls = filteredAcls.map((acl) => acl.split('acl_')[1]);
return ( return (
<span> <span>
{filteredAcls.join('/')} {filteredAcls}
</span> </span>
); );
} }

View File

@ -25,7 +25,7 @@ let FileDragAndDropDialog = React.createClass({
getDragDialog(fileClass) { getDragDialog(fileClass) {
if (dragAndDropAvailable) { if (dragAndDropAvailable) {
return [ return [
<p>{getLangText('Drag %s here', fileClass)}</p>, <p className="file-drag-and-drop-dialog-title">{getLangText('Drag %s here', fileClass)}</p>,
<p>{getLangText('or')}</p> <p>{getLangText('or')}</p>
]; ];
} else { } else {
@ -40,6 +40,7 @@ let FileDragAndDropDialog = React.createClass({
uploadMethod, uploadMethod,
fileClassToUpload, fileClassToUpload,
onClick } = this.props; onClick } = this.props;
let dialogElement;
if (enableLocalHashing && !uploadMethod) { if (enableLocalHashing && !uploadMethod) {
const currentQueryParams = getCurrentQueryParams(); const currentQueryParams = getCurrentQueryParams();
@ -50,9 +51,9 @@ let FileDragAndDropDialog = React.createClass({
const queryParamsUpload = Object.assign({}, currentQueryParams); const queryParamsUpload = Object.assign({}, currentQueryParams);
queryParamsUpload.method = 'upload'; queryParamsUpload.method = 'upload';
return ( dialogElement = (
<div className="file-drag-and-drop-dialog present-options"> <div className="present-options">
<p>{getLangText('Would you rather')}</p> <p className="file-drag-and-drop-dialog-title">{getLangText('Would you rather')}</p>
{/* {/*
The frontend in live is hosted under /app, The frontend in live is hosted under /app,
Since `Link` is appending that base url, if its defined Since `Link` is appending that base url, if its defined
@ -67,7 +68,7 @@ let FileDragAndDropDialog = React.createClass({
</span> </span>
</Link> </Link>
<span> or </span> <span> {getLangText('or')} </span>
<Link <Link
to={`/${window.location.pathname.split('/').pop()}`} to={`/${window.location.pathname.split('/').pop()}`}
@ -80,31 +81,39 @@ let FileDragAndDropDialog = React.createClass({
); );
} else { } else {
if (multipleFiles) { if (multipleFiles) {
return ( dialogElement = [
<span className="file-drag-and-drop-dialog"> this.getDragDialog(fileClassToUpload.plural),
{this.getDragDialog(fileClassToUpload.plural)} (<span
<span className="btn btn-default"
className="btn btn-default" onClick={onClick}>
onClick={onClick}> {getLangText('choose %s to upload', fileClassToUpload.plural)}
{getLangText('choose %s to upload', fileClassToUpload.plural)} </span>)
</span> ];
</span>
);
} else { } else {
const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular) const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular)
: getLangText('choose a %s to upload', fileClassToUpload.singular); : getLangText('choose a %s to upload', fileClassToUpload.singular);
return ( dialogElement = [
<span className="file-drag-and-drop-dialog"> this.getDragDialog(fileClassToUpload.singular),
{this.getDragDialog(fileClassToUpload.singular)} (<span
<span className="btn btn-default"
className="btn btn-default" onClick={onClick}>
onClick={onClick}> {dialog}
{dialog} </span>)
</span> ];
</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

@ -24,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 === FileStatus.UPLOADING) { const { file, handlePauseFile, handleResumeFile } = this.props;
this.props.handlePauseFile(this.props.file.id);
} else if(this.props.file.status === FileStatus.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
@ -55,13 +58,13 @@ const FileDragAndDropPreview = React.createClass({
(file.status === FileStatus.UPLOAD_SUCCESSFUL || file.status === FileStatus.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);
} }
@ -70,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))}
@ -82,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
@ -108,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}
@ -124,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

@ -49,7 +49,7 @@ const FileDragAndDropPreviewImage = React.createClass({
}; };
let actionSymbol; let actionSymbol;
// only if assets are actually downloadable, there should be a download icon if the process is already at // only if assets are actually downloadable, there should be a download icon if the process is already at
// 100%. If not, no actionSymbol should be displayed // 100%. If not, no actionSymbol should be displayed
if(progress === 100 && areAssetsDownloadable) { if(progress === 100 && areAssetsDownloadable) {
@ -68,7 +68,7 @@ const FileDragAndDropPreviewImage = React.createClass({
return ( return (
<div <div
className="file-drag-and-drop-preview-image" className="file-drag-and-drop-preview-image hidden-print"
style={imageStyle}> style={imageStyle}>
<AclProxy <AclProxy
show={showProgress}> show={showProgress}>

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

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { displayValidProgressFilesFilter, FileStatus } 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';
@ -9,7 +10,7 @@ import { truncateTextAtCharIndex } from '../../../utils/general_utils';
const { func, array, bool, shape, string } = React.PropTypes; const { func, array, bool, shape, string } = React.PropTypes;
export default function UploadButton({ className = 'btn btn-default btn-sm' } = {}) { export default function UploadButton({ className = 'btn btn-default btn-sm', showLabel = true } = {}) {
return React.createClass({ return React.createClass({
displayName: 'UploadButton', displayName: 'UploadButton',
@ -71,17 +72,9 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } =
handleOnClick() { handleOnClick() {
if(!this.state.disabled) { if(!this.state.disabled) {
let evt; let evt;
const uploadingFiles = this.getUploadingFiles();
const uploadedFile = this.getUploadedFile();
this.clearSelection(); // First, remove any currently uploading or uploaded items
if(uploadingFiles.length) { this.onClickRemove();
this.props.handleCancelFile(uploadingFiles[0].id);
} else if(uploadedFile && !uploadedFile.s3UrlSafe) {
this.props.handleCancelFile(uploadedFile.id);
} else if(uploadedFile && uploadedFile.s3UrlSafe) {
this.props.handleDeleteFile(uploadedFile.id);
}
try { try {
evt = new MouseEvent('click', { evt = new MouseEvent('click', {
@ -99,18 +92,19 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } =
} }
}, },
onClickCancel() {
this.clearSelection();
const uploadingFile = this.getUploadingFiles()[0];
this.props.handleCancelFile(uploadingFile.id);
},
onClickRemove() { onClickRemove() {
this.clearSelection(); const uploadingFiles = this.getUploadingFiles();
const uploadedFile = this.getUploadedFile(); const uploadedFile = this.getUploadedFile();
this.props.handleDeleteFile(uploadedFile.id);
},
this.clearSelection();
if(uploadingFiles.length) {
this.props.handleCancelFile(uploadingFiles[0].id);
} else if(uploadedFile && !uploadedFile.s3UrlSafe) {
this.props.handleCancelFile(uploadedFile.id);
} else if(uploadedFile && uploadedFile.s3UrlSafe) {
this.props.handleDeleteFile(uploadedFile.id);
}
},
getButtonLabel() { getButtonLabel() {
let { filesToUpload, fileClassToUpload } = this.props; let { filesToUpload, fileClassToUpload } = this.props;
@ -126,28 +120,28 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } =
}, },
getUploadedFileLabel() { getUploadedFileLabel() {
const uploadedFile = this.getUploadedFile(); if (showLabel) {
const uploadingFiles = this.getUploadingFiles(); const uploadedFile = this.getUploadedFile();
const uploadingFiles = this.getUploadingFiles();
if(uploadingFiles.length) { if (uploadingFiles.length) {
return ( return (
<span> <span>
{' ' + truncateTextAtCharIndex(uploadingFiles[0].name, 40) + ' '} {' ' + truncateTextAtCharIndex(uploadingFiles[0].name, 40) + ' '}
[<a onClick={this.onClickCancel}>{getLangText('cancel upload')}</a>] [<a onClick={this.onClickRemove}>{getLangText('cancel upload')}</a>]
</span> </span>
); );
} else if(uploadedFile) { } else if (uploadedFile) {
return ( return (
<span> <span>
<span className='ascribe-icon icon-ascribe-ok'/> <span className='ascribe-icon icon-ascribe-ok'/>
{' ' + truncateTextAtCharIndex(uploadedFile.name, 40) + ' '} {' ' + truncateTextAtCharIndex(uploadedFile.name, 40) + ' '}
[<a onClick={this.onClickRemove}>{getLangText('remove')}</a>] [<a onClick={this.onClickRemove}>{getLangText('remove')}</a>]
</span> </span>
); );
} else { } else {
return ( return <span>{getLangText('No file chosen')}</span>;
<span>{getLangText('No file chosen')}</span> }
);
} }
}, },
@ -165,7 +159,7 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } =
* Therefore the wrapping component needs to be an `anchor` tag instead of a `button` * Therefore the wrapping component needs to be an `anchor` tag instead of a `button`
*/ */
return ( return (
<div className="upload-button-wrapper"> <div className={classNames('ascribe-upload-button', {'ascribe-upload-button-has-label': showLabel})}>
{/* {/*
The button needs to be of `type="button"` as it would The button needs to be of `type="button"` as it would
otherwise submit the form its in. otherwise submit the form its in.

View File

@ -17,8 +17,8 @@ import AppConstants from '../../constants/application_constants';
import { ErrorClasses, testErrorAgainstAll } from '../../constants/error_constants'; import { ErrorClasses, testErrorAgainstAll } from '../../constants/error_constants';
import { displayValidFilesFilter, FileStatus, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils'; import { displayValidFilesFilter, FileStatus, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils';
import { computeHashOfFile } from '../../utils/file_utils';
import { getCookie } from '../../utils/fetch_api_utils'; import { getCookie } from '../../utils/fetch_api_utils';
import { computeHashOfFile, extractFileExtensionFromString } from '../../utils/file_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
@ -42,11 +42,11 @@ const ReactS3FineUploader = React.createClass({
areAssetsDownloadable: bool, areAssetsDownloadable: bool,
areAssetsEditable: bool, areAssetsEditable: bool,
errorNotificationMessage: string, errorNotificationMessage: string,
showErrorPrompt: bool,
setWarning: func, // for when the parent component wants to be notified of uploader warnings (ie. upload failed)
handleChangedFile: func, // for when a file is dropped or selected, TODO: rename to onChangedFile 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 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 // Handle form validation
setIsUploadReady: func, //TODO: rename to setIsUploaderValidated setIsUploadReady: func, //TODO: rename to setIsUploaderValidated
@ -82,25 +82,36 @@ const ReactS3FineUploader = React.createClass({
]), ]),
// S3 helpers // S3 helpers
createBlobRoutine: shape({
url: string,
pieceId: number
}),
keyRoutine: shape({ keyRoutine: shape({
url: string, url: string,
fileClass: string, fileClass: string,
pieceId: oneOfType([ pieceId: number
string,
number
])
}),
createBlobRoutine: shape({
url: string,
pieceId: oneOfType([
string,
number
])
}), }),
// FineUploader options // FineUploader options
autoUpload: bool,
debug: bool, 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, multiple: bool,
objectProperties: shape({ objectProperties: shape({
acl: string acl: string
@ -112,6 +123,18 @@ const ReactS3FineUploader = React.createClass({
csrfmiddlewaretoken: string csrfmiddlewaretoken: string
}) })
}), }),
resume: shape({
enabled: bool
}),
retry: shape({
enableAuto: bool
}),
session: shape({
customHeaders: object,
endpoint: string,
params: object,
refreshOnRequests: bool
}),
signature: shape({ signature: shape({
endpoint: string endpoint: string
}).isRequired, }).isRequired,
@ -123,38 +146,10 @@ const ReactS3FineUploader = React.createClass({
bitcoin_ID_noPrefix: string 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({ validation: shape({
itemLimit: number, itemLimit: number,
sizeLimit: string, sizeLimit: number,
allowedExtensions: arrayOf(string) allowedExtensions: arrayOf(string)
}),
messages: shape({
unsupportedBrowser: string
}),
formatFileName: func,
retry: shape({
enableAuto: bool
}) })
}, },
@ -314,22 +309,6 @@ const ReactS3FineUploader = React.createClass({
this.setState(this.getInitialState()); this.setState(this.getInitialState());
}, },
// Cancel uploads and clear previously selected files on the input element
cancelUploads(id) {
typeof id !== 'undefined' ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll();
// Reset the file input element to clear the previously selected files so that
// the user can reselect them again.
this.clearFileSelection();
},
clearFileSelection() {
const { fileInput } = this.refs;
if (fileInput && typeof fileInput.clearSelection === 'function') {
fileInput.clearSelection();
}
},
requestKey(fileId) { requestKey(fileId) {
let filename = this.state.uploader.getName(fileId); let filename = this.state.uploader.getName(fileId);
let uuid = this.state.uploader.getUuid(fileId); let uuid = this.state.uploader.getUuid(fileId);
@ -371,10 +350,11 @@ 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();
return;
} }
window.fetch(createBlobRoutine.url, { window.fetch(createBlobRoutine.url, {
@ -419,25 +399,13 @@ const ReactS3FineUploader = React.createClass({
}); });
}, },
setThumbnailForFileId(fileId, url) { // Cancel uploads and clear previously selected files on the input element
const { filesToUpload } = this.state; cancelUploads(id) {
typeof id !== 'undefined' ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll();
if(fileId < filesToUpload.length) { // Reset the file input element to clear the previously selected files so that
const changeSet = { $set: url }; // the user can reselect them again.
const newFilesToUpload = React.addons.update(filesToUpload, { this.clearFileSelection();
[fileId]: { thumbnailUrl: changeSet }
});
this.setState({ filesToUpload: newFilesToUpload });
} else {
throw new Error('Accessing an index out of range of filesToUpload');
}
},
setWarning(hasWarning) {
if (typeof this.props.setWarning === 'function') {
this.props.setWarning(hasWarning);
}
}, },
checkFormSubmissionReady() { checkFormSubmissionReady() {
@ -453,18 +421,20 @@ const ReactS3FineUploader = React.createClass({
} }
}, },
isFileValid(file) { clearFileSelection() {
const { validation } = this.props; const { fileInput } = this.refs;
if (fileInput && typeof fileInput.clearSelection === 'function') {
fileInput.clearSelection();
}
},
if (validation && file.size > validation.sizeLimit) { getAllowedExtensions() {
const fileSizeInMegaBytes = validation.sizeLimit / 1000000; const { validation: { allowedExtensions } = {} } = this.props;
const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000); if (allowedExtensions && allowedExtensions.length) {
GlobalNotificationActions.appendGlobalNotification(notification); return transformAllowedExtensionsToInputAcceptProp(allowedExtensions);
return false;
} else { } else {
return true; return null;
} }
}, },
@ -503,8 +473,99 @@ const ReactS3FineUploader = React.createClass({
} }
}, },
/* FineUploader specific callback function handlers */ isDropzoneInactive() {
const { areAssetsEditable, enableLocalHashing, multiple, showErrorPrompt, uploadMethod } = this.props;
const { errorState, filesToUpload } = this.state;
const filesToDisplay = filesToUpload.filter((file) => {
return file.status !== FileStatus.DELETED &&
file.status !== FileStatus.CANCELED &&
file.status !== FileStatus.UPLOAD_FAILED &&
file.size !== -1;
});
return (enableLocalHashing && !uploadMethod) ||
!areAssetsEditable ||
(showErrorPrompt && errorState.errorClass) ||
(!multiple && filesToDisplay.length);
},
isFileValid(file) {
const { validation: { allowedExtensions, sizeLimit = 0 }, onValidationFailed } = this.props;
const fileExt = extractFileExtensionFromString(file.name);
if (file.size > sizeLimit) {
const fileSizeInMegaBytes = sizeLimit / 1000000;
const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
if (typeof onValidationFailed === 'function') {
onValidationFailed(file);
}
return false;
} else if (allowedExtensions && !allowedExtensions.includes(fileExt)) {
const notification = new GlobalNotificationModel(getLangText(`The file you've submitted is of an invalid file format: Valid format(s): ${allowedExtensions.join(', ')}`), 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
return false;
} else {
return true;
}
},
selectValidFiles(files) {
return Array.from(files).reduce((validFiles, file) => {
if (this.isFileValid(file)) {
validFiles.push(file);
}
return validFiles;
}, []);
},
// This method has been made promise-based to immediately afterwards
// call a callback function (instantly after this.setState went through)
// This is e.g. needed when showing/hiding the optional thumbnail upload
// field in the registration form
setStatusOfFile(fileId, status) {
return Q.Promise((resolve) => {
let changeSet = {};
if (status === FileStatus.DELETED || status === FileStatus.CANCELED || status === FileStatus.UPLOAD_FAILED) {
changeSet.progress = { $set: 0 };
}
changeSet.status = { $set: status };
let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet });
this.setState({ filesToUpload }, resolve);
});
},
setThumbnailForFileId(fileId, url) {
const { filesToUpload } = this.state;
if(fileId < filesToUpload.length) {
const changeSet = { $set: url };
const newFilesToUpload = React.addons.update(filesToUpload, {
[fileId]: { thumbnailUrl: changeSet }
});
this.setState({ filesToUpload: newFilesToUpload });
} else {
throw new Error('Accessing an index out of range of filesToUpload');
}
},
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;
@ -546,7 +607,7 @@ const ReactS3FineUploader = React.createClass({
onComplete(id, name, res, xhr) { onComplete(id, name, res, xhr) {
// There has been an issue with the server's connection // There has been an issue with the server's connection
if (xhr && xhr.status === 0 && res.success) { if (xhr && xhr.status === 0 && res.success) {
console.logGlobal(new Error('Upload succeeded with a status code 0'), false, { console.logGlobal(new Error('Upload succeeded with a status code 0'), {
files: this.state.filesToUpload, files: this.state.filesToUpload,
chunks: this.state.chunks, chunks: this.state.chunks,
xhr: this.getXhrErrorComment(xhr) xhr: this.getXhrErrorComment(xhr)
@ -592,7 +653,7 @@ const ReactS3FineUploader = React.createClass({
const { errorNotificationMessage, showErrorPrompt } = this.props; const { errorNotificationMessage, showErrorPrompt } = this.props;
const { chunks, filesToUpload } = this.state; const { chunks, filesToUpload } = this.state;
console.logGlobal(errorReason, false, { console.logGlobal(errorReason, {
files: filesToUpload, files: filesToUpload,
chunks: chunks, chunks: chunks,
xhr: this.getXhrErrorComment(xhr) xhr: this.getXhrErrorComment(xhr)
@ -755,6 +816,13 @@ const ReactS3FineUploader = React.createClass({
this.cancelUploads(fileId); this.cancelUploads(fileId);
}, },
handleCancelHashing() {
// Every progress tick of the hashing function in handleUploadFile there is a
// check if this.state.hashingProgress is -1. If so, there is an error thrown that cancels
// the hashing of all files immediately.
this.setState({ hashingProgress: -1 });
},
handlePauseFile(fileId) { handlePauseFile(fileId) {
if(this.state.uploader.pauseUpload(fileId)) { if(this.state.uploader.pauseUpload(fileId)) {
this.setStatusOfFile(fileId, FileStatus.PAUSED); this.setStatusOfFile(fileId, FileStatus.PAUSED);
@ -806,15 +874,8 @@ const ReactS3FineUploader = React.createClass({
return; return;
} }
// validate each submitted file if it fits the file size // Select only the submitted files that fit the file size and allowed extensions
let validFiles = []; files = this.selectValidFiles(files);
for(let i = 0; i < files.length; i++) {
if(this.isFileValid(files[i])) {
validFiles.push(files[i]);
}
}
// override standard files list with only valid files
files = validFiles;
// if multiple is set to false and user drops multiple files into the dropzone, // if multiple is set to false and user drops multiple files into the dropzone,
// take the first one and notify user that only one file can be submitted // take the first one and notify user that only one file can be submitted
@ -928,13 +989,6 @@ const ReactS3FineUploader = React.createClass({
} }
}, },
handleCancelHashing() {
// Every progress tick of the hashing function in handleUploadFile there is a
// check if this.state.hashingProgress is -1. If so, there is an error thrown that cancels
// the hashing of all files immediately.
this.setState({ hashingProgress: -1 });
},
// ReactFineUploader is essentially just a react layer around s3 fineuploader. // ReactFineUploader is essentially just a react layer around s3 fineuploader.
// However, since we need to display the status of a file (progress, uploading) as well as // However, since we need to display the status of a file (progress, uploading) as well as
// be able to execute actions on a currently uploading file we need to exactly sync the file list // be able to execute actions on a currently uploading file we need to exactly sync the file list
@ -1004,56 +1058,6 @@ const ReactS3FineUploader = React.createClass({
}); });
}, },
// This method has been made promise-based to immediately afterwards
// call a callback function (instantly after this.setState went through)
// This is e.g. needed when showing/hiding the optional thumbnail upload
// field in the registration form
setStatusOfFile(fileId, status) {
return Q.Promise((resolve) => {
let changeSet = {};
if (status === FileStatus.DELETED || status === FileStatus.CANCELED || status === FileStatus.UPLOAD_FAILED) {
changeSet.progress = { $set: 0 };
}
changeSet.status = { $set: status };
let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet });
this.setState({ filesToUpload }, resolve);
});
},
isDropzoneInactive() {
const { areAssetsEditable, enableLocalHashing, multiple, showErrorPrompt, uploadMethod } = this.props;
const { errorState, filesToUpload } = this.state;
const filesToDisplay = filesToUpload.filter((file) => {
return file.status !== FileStatus.DELETED &&
file.status !== FileStatus.CANCELED &&
file.status !== FileStatus.UPLOAD_FAILED &&
file.size !== -1;
});
if ((enableLocalHashing && !uploadMethod) || !areAssetsEditable ||
(showErrorPrompt && errorState.errorClass) ||
(!multiple && filesToDisplay.length > 0)) {
return true;
} else {
return false;
}
},
getAllowedExtensions() {
const { validation } = this.props;
if (validation && validation.allowedExtensions && validation.allowedExtensions.length > 0) {
return transformAllowedExtensionsToInputAcceptProp(validation.allowedExtensions);
} else {
return null;
}
},
render() { render() {
const { errorState: { errorClass }, filesToUpload, uploadInProgress } = this.state; const { errorState: { errorClass }, filesToUpload, uploadInProgress } = this.state;
const { const {

View File

@ -1,6 +1,8 @@
'use strict'; 'use strict';
import fineUploader from 'fineUploader'; import fineUploader from 'fineUploader';
import MimeTypes from '../../constants/mime_types';
// Re-export qq.status from FineUploader with an additional online // Re-export qq.status from FineUploader with an additional online
// state that we use to keep track of files from S3. // state that we use to keep track of files from S3.
@ -85,12 +87,15 @@ export function displayValidProgressFilesFilter(file) {
* *
* Takes an array of file extensions (['pdf', 'png', ...]) and transforms them into a string * Takes an array of file extensions (['pdf', 'png', ...]) and transforms them into a string
* that can be passed into an html5 input via its 'accept' prop. * that can be passed into an html5 input via its 'accept' prop.
* @param {array} allowedExtensions Array of strings without a dot prefixed * @param {array} allowedExtensions Array of strings without a dot prefixed
* @return {string} Joined string (comma-separated) of the passed-in array * @return {string} Joined string (comma-separated) of the passed-in array
*/ */
export function transformAllowedExtensionsToInputAcceptProp(allowedExtensions) { export function transformAllowedExtensionsToInputAcceptProp(allowedExtensions) {
// 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
const prefixedAllowedExtensions = allowedExtensions.map((ext) => '.' + ext); // This is important for Safari as it doesn't understand just the extension.
const prefixedAllowedExtensions = allowedExtensions.map((ext) => {
return MimeTypes[ext] || ('.' + ext);
});
// generate a comma separated list to add them to the DOM element // generate a comma separated list to add them to the DOM element
// See: http://stackoverflow.com/questions/4328947/limit-file-format-when-using-input-type-file // See: http://stackoverflow.com/questions/4328947/limit-file-format-when-using-input-type-file

View File

@ -17,20 +17,32 @@ import { setDocumentTitle } from '../utils/dom_utils';
let CoaVerifyContainer = React.createClass({ let CoaVerifyContainer = React.createClass({
propTypes: {
// Provided from AscribeApp
currentUser: React.PropTypes.object,
whitelabel: React.PropTypes.object,
// Provided from router
location: React.PropTypes.object
},
render() { render() {
const { message, signature } = this.props.location.query;
setDocumentTitle(getLangText('Verify your Certificate of Authenticity')); setDocumentTitle(getLangText('Verify your Certificate of Authenticity'));
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>
<CoaVerifyForm /> <CoaVerifyForm
message={message}
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-----
@ -47,54 +59,60 @@ let CoaVerifyContainer = React.createClass({
let CoaVerifyForm = React.createClass({ let CoaVerifyForm = React.createClass({
propTypes: {
message: React.PropTypes.string,
signature: React.PropTypes.string
},
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);
} }
}, },
render() { render() {
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"
name="username" defaultValue={message}
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')}
required/> defaultValue={signature}
</Property> required />
<hr /> </Property>
</Form> <hr />
</div> </Form>
); );
} }
}); });

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">
@ -32,4 +53,4 @@ let ErrorNotFoundPage = React.createClass({
} }
}); });
export default ErrorNotFoundPage; export default ErrorNotFoundPage;

View File

@ -5,9 +5,13 @@ 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"> <div className="ascribe-footer hidden-print">
<p className="ascribe-sub-sub-statement"> <p className="ascribe-sub-sub-statement">
<br /> <br />
<a href="http://docs.ascribe.apiary.io/" target="_blank">api</a> | <a href="http://docs.ascribe.apiary.io/" target="_blank">api</a> |
@ -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

@ -1,43 +0,0 @@
'use strict';
import React from 'react';
let GlobalAction = React.createClass({
propTypes: {
requestActions: React.PropTypes.object
},
render() {
let pieceActions = null;
if (this.props.requestActions && this.props.requestActions.pieces){
pieceActions = this.props.requestActions.pieces.map((item) => {
return (
<div className="ascribe-global-action">
{item}
</div>);
});
}
let editionActions = null;
if (this.props.requestActions && this.props.requestActions.editions){
editionActions = Object.keys(this.props.requestActions.editions).map((pieceId) => {
return this.props.requestActions.editions[pieceId].map((item) => {
return (
<div className="ascribe-global-action">
{item}
</div>);
});
});
}
if (pieceActions || editionActions) {
return (
<div className="ascribe-global-action-wrapper">
{pieceActions}
{editionActions}
</div>);
}
return null;
}
});
export default GlobalAction;

View File

@ -14,51 +14,34 @@ 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 UserActions from '../actions/user_actions';
import UserStore from '../stores/user_store';
import WhitelabelActions from '../actions/whitelabel_actions';
import WhitelabelStore from '../stores/whitelabel_store';
import EventActions from '../actions/event_actions'; import EventActions from '../actions/event_actions';
import PieceListStore from '../stores/piece_list_store';
import AclProxy from './acl_proxy';
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: {
showAddWork: React.PropTypes.bool, currentUser: React.PropTypes.object.isRequired,
routes: React.PropTypes.arrayOf(React.PropTypes.object) routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
}, whitelabel: React.PropTypes.object.isRequired
getDefaultProps() {
return {
showAddWork: true
};
}, },
getInitialState() { getInitialState() {
return mergeOptions( return PieceListStore.getState();
WhitelabelStore.getState(),
UserStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
UserActions.fetchCurrentUser(); // Listen to the piece list store, but don't fetch immediately to avoid
UserStore.listen(this.onChange); // conflicts with routes that may need to wait to load the piece list
WhitelabelActions.fetchWhitelabel(); PieceListStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
// 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.
@ -67,13 +50,16 @@ let Header = React.createClass({
}, },
componentWillUnmount() { componentWillUnmount() {
UserStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
//history.unlisten(this.onRouteChange); //history.unlisten(this.onRouteChange);
}, },
onChange(state) {
this.setState(state);
},
getLogo() { getLogo() {
let { whitelabel } = this.state; const { whitelabel } = this.props;
if (whitelabel.head) { if (whitelabel.head) {
constructHead(whitelabel.head); constructHead(whitelabel.head);
@ -85,19 +71,19 @@ let Header = React.createClass({
<img className="img-brand" src={whitelabel.logo} alt="Whitelabel brand"/> <img className="img-brand" src={whitelabel.logo} alt="Whitelabel brand"/>
</Link> </Link>
); );
} else {
return (
<span>
<Link className="icon-ascribe-logo" to="/collection"/>
</span>
);
} }
return (
<span>
<Link className="icon-ascribe-logo" to="/collection"/>
</span>
);
}, },
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">
@ -109,14 +95,6 @@ let Header = React.createClass({
); );
}, },
onChange(state) {
this.setState(state);
if(this.state.currentUser && this.state.currentUser.email) {
EventActions.profileDidLoad.defer(this.state.currentUser);
}
},
onMenuItemClick() { onMenuItemClick() {
/* /*
This is a hack to make the dropdown close after clicking on an item This is a hack to make the dropdown close after clicking on an item
@ -146,63 +124,76 @@ let Header = React.createClass({
// the collapsibleNav by itself on click. setState() isn't available on a ref so // the collapsibleNav by itself on click. setState() isn't available on a ref so
// doing this explicitly is the only way for now. // doing this explicitly is the only way for now.
onRouteChange() { onRouteChange() {
this.refs.navbar.state.navExpanded = false; if (this.refs.navbar) {
this.refs.navbar.state.navExpanded = false;
}
}, },
render() { render() {
const { currentUser, routes } = this.props;
const { unfilteredPieceListCount } = this.state;
let account; let account;
let signup; let signup;
let navRoutesLinks; let navRoutesLinks;
if (this.state.currentUser.username){
if (currentUser.username) {
account = ( account = (
<DropdownButton <DropdownButton
ref='dropdownbutton' ref='dropdownbutton'
id="nav-route-user-dropdown"
eventKey="1" eventKey="1"
title={this.state.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>
<AclProxy <AclProxy
aclObject={this.state.currentUser.acl} aclObject={currentUser.acl}
aclName="acl_view_settings_contract"> aclName="acl_view_settings_contract">
<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>
</DropdownButton> </DropdownButton>
); );
navRoutesLinks = <NavRoutesLinks routes={this.props.routes} userAcl={this.state.currentUser.acl} navbar right/>;
} // Let's assume that if the piece list hasn't loaded yet (ie. when unfilteredPieceListCount === -1)
else { // then the user has pieces
// FIXME: this doesn't work that well as the user may not load their piece list
// until much later, so we would show the 'Collection' header as available until
// they actually click on it and get redirected to piece registration.
navRoutesLinks = (
<NavRoutesLinks
navbar
right
hasPieces={!!unfilteredPieceListCount}
routes={routes}
userAcl={currentUser.acl} />
);
} 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>
@ -213,17 +204,17 @@ let Header = React.createClass({
return ( return (
<div> <div>
<Navbar <Navbar
ref="navbar"
brand={this.getLogo()} brand={this.getLogo()}
toggleNavKey={0} toggleNavKey={0}
fixedTop={true} fixedTop={true}
ref="navbar"> 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>
@ -231,6 +222,9 @@ let Header = React.createClass({
{navRoutesLinks} {navRoutesLinks}
</CollapsibleNav> </CollapsibleNav>
</Navbar> </Navbar>
<p className="ascribe-print-header visible-print">
<span className="icon-ascribe-logo" />
</p>
</div> </div>
); );
} }

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

@ -11,14 +11,33 @@ import AclProxy from './acl_proxy';
import { sanitizeList } from '../utils/general_utils'; import { sanitizeList } from '../utils/general_utils';
const DISABLE_ENUM = ['hasPieces', 'noPieces'];
let NavRoutesLinks = React.createClass({ let NavRoutesLinks = React.createClass({
propTypes: { propTypes: {
hasPieces: React.PropTypes.bool,
routes: React.PropTypes.arrayOf(React.PropTypes.object), routes: React.PropTypes.arrayOf(React.PropTypes.object),
userAcl: React.PropTypes.object userAcl: React.PropTypes.object
}, },
isRouteDisabled(disableOn) {
const { hasPieces } = this.props;
if (disableOn) {
if (!DISABLE_ENUM.includes(disableOn)) {
throw new Error(`"disableOn" must be one of: [${DISABLE_ENUM.join(', ')}] got "${disableOn}" instead`);
}
if (disableOn === 'hasPieces') {
return hasPieces;
} else if (disableOn === 'noPieces') {
return !hasPieces;
}
}
},
/** /**
* This method generales a bunch of react-bootstrap specific links * This method generates a bunch of react-bootstrap specific links
* from the routes we defined in one of the specific routes.js file * from the routes we defined in one of the specific routes.js file
* *
* We can define a headerTitle as well as a aclName and according to that the * We can define a headerTitle as well as a aclName and according to that the
@ -29,48 +48,50 @@ let NavRoutesLinks = React.createClass({
* @return {Array} Array of ReactElements that can be displayed to the user * @return {Array} Array of ReactElements that can be displayed to the user
*/ */
extractLinksFromRoutes(node, userAcl, i) { extractLinksFromRoutes(node, userAcl, i) {
if(!node) { if (!node) {
return; return;
} }
let links = node.childRoutes.map((child, j) => { const links = node.childRoutes.map((child, j) => {
let childrenFn = null; const { aclName, disableOn, headerTitle, path, childRoutes } = child;
let { aclName, headerTitle, path, childRoutes } = child;
// If the node has children that could be rendered, then we want
// to execute this function again with the child as the root
//
// Otherwise we'll just pass childrenFn as false
if(child.childRoutes && child.childRoutes.length > 0) {
childrenFn = this.extractLinksFromRoutes(child, userAcl, i++);
}
// We validate if the user has set the title correctly, // We validate if the user has set the title correctly,
// otherwise we're not going to render his route // otherwise we're not going to render his route
if(headerTitle && typeof headerTitle === 'string') { if (headerTitle && typeof headerTitle === 'string') {
let nestedChildren = null;
// If the node has children that could be rendered, then we want
// to execute this function again with the child as the root
//
// Otherwise we'll just pass nestedChildren as false
if (child.childRoutes && child.childRoutes.length) {
nestedChildren = this.extractLinksFromRoutes(child, userAcl, i++);
}
const navLinkProps = {
headerTitle,
children: nestedChildren,
depth: i,
disabled: this.isRouteDisabled(disableOn),
routePath: `/${path}`
};
// if there is an aclName present on the route definition, // if there is an aclName present on the route definition,
// we evaluate it against the user's acl // we evaluate it against the user's acl
if(aclName && typeof aclName !== 'undefined') { if (aclName && typeof aclName !== 'undefined') {
return ( return (
<AclProxy <AclProxy
key={j} key={j}
aclName={aclName} aclName={aclName}
aclObject={this.props.userAcl}> aclObject={this.props.userAcl}>
<NavRoutesLinksLink <NavRoutesLinksLink {...navLinkProps} />
headerTitle={headerTitle}
routePath={'/' + path}
depth={i}
children={childrenFn}/>
</AclProxy> </AclProxy>
); );
} else { } else {
return ( return (
<NavRoutesLinksLink <NavRoutesLinksLink
key={j} key={j}
headerTitle={headerTitle} {...navLinkProps} />
routePath={'/' + path}
depth={i}
children={childrenFn}/>
); );
} }
} else { } else {
@ -84,7 +105,7 @@ let NavRoutesLinks = React.createClass({
}, },
render() { render() {
let {routes, userAcl} = this.props; const {routes, userAcl} = this.props;
return ( return (
<Nav {...this.props}> <Nav {...this.props}>
@ -94,4 +115,4 @@ let NavRoutesLinks = React.createClass({
} }
}); });
export default NavRoutesLinks; export default NavRoutesLinks;

View File

@ -11,40 +11,46 @@ import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
let NavRoutesLinksLink = React.createClass({ let NavRoutesLinksLink = React.createClass({
propTypes: { propTypes: {
headerTitle: React.PropTypes.string,
routePath: React.PropTypes.string,
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
]), ]),
disabled: React.PropTypes.bool,
depth: React.PropTypes.number depth: React.PropTypes.number,
headerTitle: React.PropTypes.string,
routePath: React.PropTypes.string
}, },
render() { render() {
let { children, headerTitle, depth, routePath } = this.props; const { children, headerTitle, depth, disabled, routePath } = this.props;
// if the route has children, we're returning a DropdownButton that will get filled // if the route has children, we're returning a DropdownButton that will get filled
// with MenuItems // with MenuItems
if(children) { if (children) {
return ( return (
<DropdownButton title={headerTitle}> <DropdownButton
disabled={disabled}
id={`nav-route-${headerTitle.toLowerCase()}-dropdown`}
title={headerTitle}>
{children} {children}
</DropdownButton> </DropdownButton>
); );
} else { } else {
if(depth === 1) { if (depth === 1) {
// if the node's child is actually a node of level one (a child of a node), we're // if the node's child is actually a node of level one (a child of a node), we're
// returning a DropdownButton matching MenuItem // returning a DropdownButton matching MenuItem
return ( return (
<LinkContainer to={routePath}> <LinkContainer
disabled={disabled}
to={routePath}>
<MenuItem>{headerTitle}</MenuItem> <MenuItem>{headerTitle}</MenuItem>
</LinkContainer> </LinkContainer>
); );
} else if(depth === 0) { } else if (depth === 0) {
return ( return (
<LinkContainer to={routePath}> <LinkContainer
disabled={disabled}
to={routePath}>
<NavItem>{headerTitle}</NavItem> <NavItem>{headerTitle}</NavItem>
</LinkContainer> </LinkContainer>
); );
@ -55,4 +61,4 @@ let NavRoutesLinksLink = React.createClass({
} }
}); });
export default NavRoutesLinksLink; export default NavRoutesLinksLink;

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