mirror of
https://github.com/ascribe/onion.git
synced 2025-02-11 16:11:34 +01:00
Merge with master
This commit is contained in:
parent
b24e66ed11
commit
826ca08073
3
.env-template
Normal file
3
.env-template
Normal file
@ -0,0 +1,3 @@
|
||||
SAUCE_USERNAME=ascribe
|
||||
SAUCE_ACCESS_KEY=
|
||||
SAUCE_DEFAULT_URL=
|
@ -2,7 +2,7 @@
|
||||
"parser": "babel-eslint",
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
"es6": true,
|
||||
},
|
||||
"rules": {
|
||||
"new-cap": [2, {newIsCap: true, capIsNew: false}],
|
||||
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -16,9 +16,14 @@ webapp-dependencies.txt
|
||||
pids
|
||||
logs
|
||||
results
|
||||
|
||||
|
||||
build/*
|
||||
|
||||
gemini-coverage/*
|
||||
gemini-report/*
|
||||
test/gemini/screenshots/*
|
||||
|
||||
node_modules/*
|
||||
|
||||
build
|
||||
|
||||
.DS_Store
|
||||
.env
|
||||
|
91
README.md
91
README.md
@ -1,18 +1,19 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
Onion is the web client for Ascribe. The idea is to have a well documented,
|
||||
easy to test, easy to hack, JavaScript application.
|
||||
Onion is the web client for Ascribe. The idea is to have a well documented, modern, easy to test, easy to hack, JavaScript application.
|
||||
|
||||
The code is JavaScript ECMA 6.
|
||||
The code is JavaScript 2015 / ECMAScript 6.
|
||||
|
||||
|
||||
Getting started
|
||||
===============
|
||||
|
||||
Install some nice extension for Chrom(e|ium):
|
||||
|
||||
- [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi)
|
||||
|
||||
- [Alt Developer Tools](https://github.com/goatslacker/alt-devtool)
|
||||
|
||||
```bash
|
||||
git clone git@github.com:ascribe/onion.git
|
||||
cd onion
|
||||
@ -37,43 +38,62 @@ Additionally, to work on the white labeling functionality, you need to edit your
|
||||
|
||||
JavaScript Code Conventions
|
||||
===========================
|
||||
|
||||
For this project, we're using:
|
||||
|
||||
* 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 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 don't use Javascript's `Date` object, as its interface introduced bugs previously and we're including `momentjs` for other dependencies anyways
|
||||
* 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
|
||||
|
||||
Branch names
|
||||
=====================
|
||||
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.
|
||||
Make sure to check out the [style guide](https://github.com/ascribe/javascript).
|
||||
|
||||
```
|
||||
AD-<JIRA-ticket-id>-brief-and-sane-description-of-the-ticket
|
||||
```
|
||||
Linting
|
||||
-------
|
||||
|
||||
where `brief-and-sane-description-of-the-ticket` does not need to equal to the ticket's title.
|
||||
This allows JIRA to still track branches and pull-requests while allowing us to keep our peace of mind.
|
||||
We use [ESLint](https://github.com/eslint/eslint) with our own [custom ruleset](.eslintrc).
|
||||
|
||||
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
|
||||
=====================
|
||||
|
||||
Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor.
|
||||
|
||||
Some interesting links:
|
||||
* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom)
|
||||
|
||||
|
||||
Branch names
|
||||
============
|
||||
|
||||
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
|
||||
===============
|
||||
=======
|
||||
|
||||
Unit Testing
|
||||
------------
|
||||
|
||||
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.
|
||||
@ -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.
|
||||
|
||||
## 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.
|
||||
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
|
||||
```
|
||||
|
||||
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.
|
||||
A: ```browserify -e js/app.js --list > webapp-dependencies.txt```
|
||||
|
||||
|
||||
Reading list
|
||||
============
|
||||
|
||||
@ -149,7 +193,6 @@ Start here
|
||||
- [alt.js](http://alt.js.org/)
|
||||
- [alt.js readme](https://github.com/goatslacker/alt)
|
||||
|
||||
|
||||
Moar stuff
|
||||
----------
|
||||
|
||||
|
@ -97,7 +97,8 @@ gulp.task('browser-sync', function() {
|
||||
proxy: 'http://localhost:4000',
|
||||
port: 3000,
|
||||
open: false, // does not open the browser-window anymore (handled manually)
|
||||
ghostMode: false
|
||||
ghostMode: false,
|
||||
notify: false // stop showing the browsersync pop up
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -28,12 +28,10 @@ class ContractListActions {
|
||||
}
|
||||
|
||||
|
||||
changeContract(contract){
|
||||
changeContract(contract) {
|
||||
return Q.Promise((resolve, reject) => {
|
||||
OwnershipFetcher.changeContract(contract)
|
||||
.then((res) => {
|
||||
resolve(res);
|
||||
})
|
||||
.then(resolve)
|
||||
.catch((err)=> {
|
||||
console.logGlobal(err);
|
||||
reject(err);
|
||||
@ -41,13 +39,11 @@ class ContractListActions {
|
||||
});
|
||||
}
|
||||
|
||||
removeContract(contractId){
|
||||
return Q.Promise( (resolve, reject) => {
|
||||
removeContract(contractId) {
|
||||
return Q.Promise((resolve, reject) => {
|
||||
OwnershipFetcher.deleteContract(contractId)
|
||||
.then((res) => {
|
||||
resolve(res);
|
||||
})
|
||||
.catch( (err) => {
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
console.logGlobal(err);
|
||||
reject(err);
|
||||
});
|
||||
|
@ -7,11 +7,11 @@ class EditionActions {
|
||||
constructor() {
|
||||
this.generateActions(
|
||||
'fetchEdition',
|
||||
'successFetchEdition',
|
||||
'successFetchCoa',
|
||||
'flushEdition',
|
||||
'successFetchEdition',
|
||||
'errorCoa',
|
||||
'errorEdition'
|
||||
'errorEdition',
|
||||
'flushEdition'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -17,23 +17,31 @@ class EditionListActions {
|
||||
);
|
||||
}
|
||||
|
||||
fetchEditionList(pieceId, page, pageSize, orderBy, orderAsc, filterBy) {
|
||||
if((!orderBy && typeof orderAsc === 'undefined') || !orderAsc) {
|
||||
fetchEditionList({ pieceId, page, pageSize, orderBy, orderAsc, filterBy, maxEdition }) {
|
||||
if ((!orderBy && typeof orderAsc === 'undefined') || !orderAsc) {
|
||||
orderBy = 'edition_number';
|
||||
orderAsc = true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
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) => {
|
||||
EditionListFetcher
|
||||
.fetch(pieceId, page, pageSize, orderBy, orderAsc, filterBy)
|
||||
.fetch({ pieceId, page, orderBy, orderAsc, filterBy, pageSize: itemsToFetch })
|
||||
.then((res) => {
|
||||
if(res && !res.editions) {
|
||||
if (res && !res.editions) {
|
||||
throw new Error('Piece has no editions to fetch.');
|
||||
}
|
||||
|
||||
@ -44,8 +52,9 @@ class EditionListActions {
|
||||
orderBy,
|
||||
orderAsc,
|
||||
filterBy,
|
||||
'editionListOfPiece': res.editions,
|
||||
'count': res.count
|
||||
maxEdition,
|
||||
count: res.count,
|
||||
editionListOfPiece: res.editions
|
||||
});
|
||||
resolve(res);
|
||||
})
|
||||
@ -54,7 +63,6 @@ class EditionListActions {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
14
js/actions/facebook_actions.js
Normal file
14
js/actions/facebook_actions.js
Normal file
@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
import { altThirdParty } from '../alt';
|
||||
|
||||
|
||||
class FacebookActions {
|
||||
constructor() {
|
||||
this.generateActions(
|
||||
'sdkReady'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default altThirdParty.createActions(FacebookActions);
|
@ -1,28 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
import { alt } from '../alt';
|
||||
import PieceFetcher from '../fetchers/piece_fetcher';
|
||||
|
||||
|
||||
class PieceActions {
|
||||
constructor() {
|
||||
this.generateActions(
|
||||
'fetchPiece',
|
||||
'successFetchPiece',
|
||||
'errorPiece',
|
||||
'flushPiece',
|
||||
'updatePiece',
|
||||
'updateProperty',
|
||||
'pieceFailed'
|
||||
'updateProperty'
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -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,
|
||||
// we overwrite the piecelist with an empty list before
|
||||
// pieceListCount === -1 defines the loading state
|
||||
@ -34,7 +34,7 @@ class PieceListActions {
|
||||
// afterwards, we can load the list
|
||||
return Q.Promise((resolve, reject) => {
|
||||
PieceListFetcher
|
||||
.fetch(page, pageSize, search, orderBy, orderAsc, filterBy)
|
||||
.fetch({ page, pageSize, search, orderBy, orderAsc, filterBy })
|
||||
.then((res) => {
|
||||
this.actions.updatePieceList({
|
||||
page,
|
||||
|
@ -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);
|
86
js/app.js
86
js/app.js
@ -1,14 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
import 'babel/polyfill';
|
||||
import 'classlist-polyfill';
|
||||
|
||||
import React from 'react';
|
||||
import { Router, Redirect } from 'react-router';
|
||||
import history from './history';
|
||||
|
||||
/* eslint-disable */
|
||||
import fetch from 'isomorphic-fetch';
|
||||
/* eslint-enable */
|
||||
|
||||
import ApiUrls from './constants/api_urls';
|
||||
|
||||
@ -17,44 +16,27 @@ import getRoutes from './routes';
|
||||
import requests from './utils/requests';
|
||||
|
||||
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 { getSubdomain } from './utils/general_utils';
|
||||
|
||||
import EventActions from './actions/event_actions';
|
||||
|
||||
/* eslint-disable */
|
||||
// You can comment out the modules you don't need
|
||||
// import DebugHandler from './third_party/debug';
|
||||
import GoogleAnalyticsHandler from './third_party/ga';
|
||||
import RavenHandler from './third_party/raven';
|
||||
import IntercomHandler from './third_party/intercom';
|
||||
import NotificationsHandler from './third_party/notifications';
|
||||
import FacebookHandler from './third_party/facebook';
|
||||
/* eslint-enable */
|
||||
// import DebugHandler from './third_party/debug_handler';
|
||||
import FacebookHandler from './third_party/facebook_handler';
|
||||
import GoogleAnalyticsHandler from './third_party/ga_handler';
|
||||
import IntercomHandler from './third_party/intercom_handler';
|
||||
import NotificationsHandler from './third_party/notifications_handler';
|
||||
import RavenHandler from './third_party/raven_handler';
|
||||
|
||||
initLogging();
|
||||
|
||||
let headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
requests.defaults({
|
||||
urlMap: ApiUrls,
|
||||
http: {
|
||||
headers: headers,
|
||||
credentials: 'include'
|
||||
}
|
||||
});
|
||||
|
||||
class AppGateway {
|
||||
const AppGateway = {
|
||||
start() {
|
||||
let settings;
|
||||
let subdomain = getSubdomain();
|
||||
|
||||
try {
|
||||
settings = getSubdomainSettings(subdomain);
|
||||
const subdomain = getSubdomain();
|
||||
const settings = getSubdomainSettings(subdomain);
|
||||
|
||||
AppConstants.whitelabel = settings;
|
||||
updateApiUrls(settings.type, subdomain);
|
||||
this.load(settings);
|
||||
@ -62,28 +44,25 @@ class AppGateway {
|
||||
// if there are no matching subdomains, we're routing
|
||||
// to the default frontend
|
||||
console.logGlobal(err);
|
||||
this.load();
|
||||
this.load(getDefaultSubdomainSettings());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
load(settings) {
|
||||
let type = 'default';
|
||||
let subdomain = 'www';
|
||||
const { subdomain, type } = settings;
|
||||
let redirectRoute = (<Redirect from="/" to="/collection" />);
|
||||
|
||||
if (settings) {
|
||||
type = settings.type;
|
||||
subdomain = settings.subdomain;
|
||||
}
|
||||
if (subdomain) {
|
||||
// Some whitelabels have landing pages so we should not automatically redirect from / to /collection.
|
||||
// Only www and cc do not have a landing page.
|
||||
if (subdomain !== 'cc') {
|
||||
redirectRoute = null;
|
||||
}
|
||||
|
||||
// www and cc do not have a landing page
|
||||
if(subdomain && subdomain !== 'cc') {
|
||||
redirectRoute = null;
|
||||
// Adds a client specific class to the body for whitelabel styling
|
||||
window.document.body.classList.add('client--' + subdomain);
|
||||
}
|
||||
|
||||
// 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
|
||||
EventActions.applicationWillBoot(settings);
|
||||
|
||||
@ -101,8 +80,21 @@ class AppGateway {
|
||||
// Send the applicationDidBoot event to the third-party stores
|
||||
EventActions.applicationDidBoot(settings);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ag = new AppGateway();
|
||||
ag.start();
|
||||
// Initialize pre-start components
|
||||
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
90
js/components/app_base.js
Normal 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>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
@ -24,7 +24,7 @@ const AppRouteWrapper = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ascribe-body">
|
||||
<div className="container ascribe-body">
|
||||
{childrenWithProps}
|
||||
</div>
|
||||
);
|
||||
|
@ -8,9 +8,10 @@ import { getLangText } from '../../utils/lang_utils';
|
||||
|
||||
let AccordionList = React.createClass({
|
||||
propTypes: {
|
||||
className: React.PropTypes.string,
|
||||
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,
|
||||
itemList: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
search: React.PropTypes.string,
|
||||
@ -24,7 +25,7 @@ let AccordionList = React.createClass({
|
||||
render() {
|
||||
const { search } = this.props;
|
||||
|
||||
if(this.props.itemList && this.props.itemList.length > 0) {
|
||||
if (this.props.itemList && this.props.itemList.length > 0) {
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
{this.props.children}
|
||||
|
@ -19,9 +19,10 @@ import { getLangText } from '../../utils/lang_utils';
|
||||
|
||||
let AccordionListItemEditionWidget = React.createClass({
|
||||
propTypes: {
|
||||
className: React.PropTypes.string,
|
||||
piece: React.PropTypes.object.isRequired,
|
||||
toggleCreateEditionsDialog: React.PropTypes.func.isRequired,
|
||||
|
||||
className: React.PropTypes.string,
|
||||
onPollingSuccess: React.PropTypes.func
|
||||
},
|
||||
|
||||
@ -50,14 +51,15 @@ let AccordionListItemEditionWidget = React.createClass({
|
||||
* Calls the store to either show or hide the editionListTable
|
||||
*/
|
||||
toggleTable() {
|
||||
let pieceId = this.props.piece.id;
|
||||
let isEditionListOpen = this.state.isEditionListOpenForPieceId[pieceId] ? this.state.isEditionListOpenForPieceId[pieceId].show : false;
|
||||
const { piece: { id: pieceId } } = this.props;
|
||||
const { filterBy, isEditionListOpenForPieceId } = this.state;
|
||||
const isEditionListOpen = isEditionListOpenForPieceId[pieceId] ? isEditionListOpenForPieceId[pieceId].show : false;
|
||||
|
||||
if(isEditionListOpen) {
|
||||
if (isEditionListOpen) {
|
||||
EditionListActions.toggleEditionList(pieceId);
|
||||
} else {
|
||||
EditionListActions.toggleEditionList(pieceId);
|
||||
EditionListActions.fetchEditionList(pieceId, null, null, null, null, this.state.filterBy);
|
||||
EditionListActions.fetchEditionList({ pieceId, filterBy });
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -66,20 +66,28 @@ let AccordionListItemTableEditions = React.createClass({
|
||||
},
|
||||
|
||||
filterSelectedEditions() {
|
||||
let selectedEditions = this.state.editionList[this.props.parentId]
|
||||
.filter((edition) => edition.selected);
|
||||
return selectedEditions;
|
||||
return this.state
|
||||
.editionList[this.props.parentId]
|
||||
.filter((edition) => edition.selected);
|
||||
},
|
||||
|
||||
loadFurtherEditions() {
|
||||
const { parentId: pieceId } = this.props;
|
||||
const { page, pageSize, orderBy, orderAsc, filterBy } = this.state.editionList[pieceId];
|
||||
|
||||
// trigger loading animation
|
||||
this.setState({
|
||||
showMoreLoading: true
|
||||
});
|
||||
|
||||
let editionList = this.state.editionList[this.props.parentId];
|
||||
EditionListActions.fetchEditionList(this.props.parentId, editionList.page + 1, editionList.pageSize,
|
||||
editionList.orderBy, editionList.orderAsc, editionList.filterBy);
|
||||
EditionListActions.fetchEditionList({
|
||||
pieceId,
|
||||
pageSize,
|
||||
orderBy,
|
||||
orderAsc,
|
||||
filterBy,
|
||||
page: page + 1
|
||||
});
|
||||
},
|
||||
render() {
|
||||
const { className, parentId } = this.props;
|
||||
|
@ -1,4 +1,4 @@
|
||||
'use strict'
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
|
@ -31,12 +31,12 @@ let AccordionListItemWallet = React.createClass({
|
||||
content: React.PropTypes.object.isRequired,
|
||||
whitelabel: React.PropTypes.object.isRequired,
|
||||
|
||||
className: React.PropTypes.string,
|
||||
thumbnailPlaceholder: React.PropTypes.func,
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
React.PropTypes.element
|
||||
])
|
||||
]),
|
||||
className: React.PropTypes.string,
|
||||
thumbnailPlaceholder: React.PropTypes.func
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
@ -67,10 +67,12 @@ let AccordionListItemWallet = React.createClass({
|
||||
delay={500}
|
||||
placement="left"
|
||||
overlay={<Tooltip>{getLangText('You have actions pending')}</Tooltip>}>
|
||||
<Glyphicon glyph='bell' color="green"/>
|
||||
</OverlayTrigger>);
|
||||
<Glyphicon glyph='bell' color="green" />
|
||||
</OverlayTrigger>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
toggleCreateEditionsDialog() {
|
||||
@ -86,8 +88,9 @@ let AccordionListItemWallet = React.createClass({
|
||||
},
|
||||
|
||||
onPollingSuccess(pieceId) {
|
||||
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
|
||||
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
|
||||
const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state;
|
||||
|
||||
PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy });
|
||||
EditionListActions.toggleEditionList(pieceId);
|
||||
|
||||
const notification = new GlobalNotificationModel(getLangText('Editions successfully created'), 'success', 10000);
|
||||
|
@ -2,60 +2,28 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
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 AppBase from './app_base';
|
||||
import AppRouteWrapper from './app_route_wrapper';
|
||||
import Header from './header';
|
||||
import Footer from './footer';
|
||||
import GlobalNotification from './global_notification';
|
||||
|
||||
import { mergeOptions } from '../utils/general_utils';
|
||||
import Header from './header';
|
||||
|
||||
|
||||
let AscribeApp = React.createClass({
|
||||
propTypes: {
|
||||
activeRoute: React.PropTypes.object.isRequired,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
||||
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
React.PropTypes.element
|
||||
])
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return mergeOptions(
|
||||
UserStore.getState(),
|
||||
WhitelabelStore.getState()
|
||||
);
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
UserStore.listen(this.onChange);
|
||||
WhitelabelStore.listen(this.onChange);
|
||||
|
||||
UserActions.fetchCurrentUser();
|
||||
WhitelabelActions.fetchWhitelabel();
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
UserStore.unlisten(this.onChange);
|
||||
WhitelabelActions.unlisten(this.onChange);
|
||||
},
|
||||
|
||||
onChange(state) {
|
||||
this.setState(state);
|
||||
// Provided from AppBase
|
||||
currentUser: React.PropTypes.object,
|
||||
whitelabel: React.PropTypes.object
|
||||
},
|
||||
|
||||
render() {
|
||||
const { children, routes } = this.props;
|
||||
const { currentUser, whitelabel } = this.state;
|
||||
const { activeRoute, children, currentUser, routes, whitelabel } = this.props;
|
||||
|
||||
return (
|
||||
<div className="container ascribe-default-app">
|
||||
<div className="ascribe-default-app">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
routes={routes}
|
||||
@ -66,12 +34,10 @@ let AscribeApp = React.createClass({
|
||||
{/* Routes are injected here */}
|
||||
{children}
|
||||
</AppRouteWrapper>
|
||||
<Footer />
|
||||
<GlobalNotification />
|
||||
<div id="modal" className="container"></div>
|
||||
<Footer activeRoute={activeRoute} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default AscribeApp;
|
||||
export default AppBase(AscribeApp);
|
||||
|
@ -14,7 +14,7 @@ import AppConstants from '../../../constants/application_constants';
|
||||
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) {
|
||||
console.warn('Your specified aclName did not match a an acl class.');
|
||||
}
|
||||
|
@ -28,6 +28,12 @@ let CreateEditionsButton = React.createClass({
|
||||
EditionListStore.listen(this.onChange);
|
||||
},
|
||||
|
||||
componentDidUpdate() {
|
||||
if(this.props.piece.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') {
|
||||
this.startPolling();
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
EditionListStore.unlisten(this.onChange);
|
||||
clearInterval(this.state.pollingIntervalIndex);
|
||||
@ -37,28 +43,24 @@ let CreateEditionsButton = React.createClass({
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
componentDidUpdate() {
|
||||
if(this.props.piece.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') {
|
||||
this.startPolling();
|
||||
}
|
||||
},
|
||||
|
||||
startPolling() {
|
||||
// start polling until editions are defined
|
||||
let pollingIntervalIndex = setInterval(() => {
|
||||
|
||||
// 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
|
||||
EditionListActions.fetchEditionList(this.props.piece.id, null, null, null, null, {})
|
||||
.then((res) => {
|
||||
|
||||
clearInterval(this.state.pollingIntervalIndex);
|
||||
this.props.onPollingSuccess(this.props.piece.id, res.editions[0].num_editions);
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
/* Ignore and keep going */
|
||||
});
|
||||
EditionListActions
|
||||
.fetchEditionList({
|
||||
pieceId: this.props.piece.id,
|
||||
filterBy: {}
|
||||
})
|
||||
.then((res) => {
|
||||
clearInterval(this.state.pollingIntervalIndex);
|
||||
this.props.onPollingSuccess(this.props.piece.id, res.editions[0].num_editions);
|
||||
})
|
||||
.catch((err) => {
|
||||
/* Ignore and keep going */
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
this.setState({
|
||||
|
@ -74,7 +74,7 @@ let Edition = React.createClass({
|
||||
<div className="ascribe-detail-header">
|
||||
<hr className="hidden-print" style={{marginTop: 0}} />
|
||||
<h1 className="ascribe-detail-title">{edition.title}</h1>
|
||||
<DetailProperty label="BY" value={edition.artist_name} />
|
||||
<DetailProperty label="CREATED BY" value={edition.artist_name} />
|
||||
<DetailProperty label="DATE" value={Moment(edition.date_created, 'YYYY-MM-DD').year()} />
|
||||
<hr />
|
||||
</div>
|
||||
|
@ -81,9 +81,10 @@ let EditionActionPanel = React.createClass({
|
||||
},
|
||||
|
||||
refreshCollection() {
|
||||
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
|
||||
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
|
||||
EditionListActions.refreshEditionList({pieceId: this.props.edition.parent});
|
||||
const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state;
|
||||
|
||||
PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy });
|
||||
EditionListActions.refreshEditionList({ pieceId: this.props.edition.parent });
|
||||
},
|
||||
|
||||
handleSuccess(response) {
|
||||
|
@ -37,31 +37,28 @@ let EditionContainer = React.createClass({
|
||||
mixins: [History, ReactError],
|
||||
|
||||
getInitialState() {
|
||||
return EditionStore.getState();
|
||||
return EditionStore.getInitialState();
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
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.flushEdition();
|
||||
EditionActions.fetchEdition(this.props.params.editionId);
|
||||
this.loadEdition();
|
||||
},
|
||||
|
||||
// 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)
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.params.editionId !== nextProps.params.editionId) {
|
||||
EditionActions.fetchEdition(this.props.params.editionId);
|
||||
EditionActions.flushEdition();
|
||||
this.loadEdition(nextProps.params.editionId);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate() {
|
||||
const { editionMeta } = this.state;
|
||||
if (editionMeta.err && editionMeta.err.json && editionMeta.err.json.status === 404) {
|
||||
const { err: editionErr } = this.state.editionMeta;
|
||||
|
||||
if (editionErr && editionErr.json && editionErr.json.status === 404) {
|
||||
this.throws(new ResourceNotFoundError(getLangText("Oops, the edition you're looking for doesn't exist.")));
|
||||
}
|
||||
},
|
||||
@ -75,12 +72,16 @@ let EditionContainer = React.createClass({
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
loadEdition(editionId = this.props.params.editionId) {
|
||||
EditionActions.fetchEdition(editionId);
|
||||
},
|
||||
|
||||
render() {
|
||||
const { actionPanelButtonListType, currentUser, furtherDetailsType, whitelabel } = this.props;
|
||||
const { edition, coaMeta } = this.state;
|
||||
|
||||
if (Object.keys(edition).length && edition.id) {
|
||||
setDocumentTitle([edition.artist_name, edition.title].join(', '));
|
||||
if (edition.id) {
|
||||
setDocumentTitle(`${edition.artist_name}, ${edition.title}`);
|
||||
|
||||
return (
|
||||
<Edition
|
||||
@ -89,7 +90,7 @@ let EditionContainer = React.createClass({
|
||||
currentUser={currentUser}
|
||||
edition={edition}
|
||||
furtherDetailsType={furtherDetailsType}
|
||||
loadEdition={() => EditionActions.fetchEdition(this.props.params.editionId)}
|
||||
loadEdition={this.loadEdition}
|
||||
whitelabel={whitelabel} />
|
||||
);
|
||||
} else {
|
||||
|
@ -5,25 +5,27 @@ import React from 'react';
|
||||
import Row from 'react-bootstrap/lib/Row';
|
||||
import Col from 'react-bootstrap/lib/Col';
|
||||
|
||||
import Form from './../ascribe_forms/form';
|
||||
|
||||
import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata';
|
||||
|
||||
import GlobalNotificationModel from '../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../actions/global_notification_actions';
|
||||
|
||||
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 { getLangText } from '../../utils/lang_utils';
|
||||
|
||||
|
||||
let FurtherDetails = React.createClass({
|
||||
propTypes: {
|
||||
pieceId: React.PropTypes.number.isRequired,
|
||||
|
||||
editable: React.PropTypes.bool,
|
||||
pieceId: React.PropTypes.number,
|
||||
extraData: React.PropTypes.object,
|
||||
handleSuccess: React.PropTypes.func,
|
||||
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
handleSuccess: React.PropTypes.func
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
@ -32,13 +34,18 @@ let FurtherDetails = React.createClass({
|
||||
};
|
||||
},
|
||||
|
||||
showNotification(){
|
||||
this.props.handleSuccess();
|
||||
let notification = new GlobalNotificationModel('Details updated', 'success');
|
||||
showNotification() {
|
||||
const { handleSuccess } = this.props;
|
||||
|
||||
if (typeof handleSucess === 'function') {
|
||||
handleSuccess();
|
||||
}
|
||||
|
||||
const notification = new GlobalNotificationModel(getLangText('Details updated'), 'success');
|
||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||
},
|
||||
|
||||
submitFile(file){
|
||||
submitFile(file) {
|
||||
this.setState({
|
||||
otherDataKey: file.key
|
||||
});
|
||||
@ -51,40 +58,42 @@ let FurtherDetails = React.createClass({
|
||||
},
|
||||
|
||||
render() {
|
||||
const { editable, extraData, otherData, pieceId } = this.props;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col md={12} className="ascribe-edition-personal-note">
|
||||
<PieceExtraDataForm
|
||||
name='artist_contact_info'
|
||||
title='Artist Contact Info'
|
||||
convertLinks
|
||||
editable={editable}
|
||||
extraData={extraData}
|
||||
handleSuccess={this.showNotification}
|
||||
editable={this.props.editable}
|
||||
pieceId={this.props.pieceId}
|
||||
extraData={this.props.extraData}
|
||||
/>
|
||||
pieceId={pieceId} />
|
||||
<PieceExtraDataForm
|
||||
name='display_instructions'
|
||||
title='Display Instructions'
|
||||
editable={editable}
|
||||
extraData={extraData}
|
||||
handleSuccess={this.showNotification}
|
||||
editable={this.props.editable}
|
||||
pieceId={this.props.pieceId}
|
||||
extraData={this.props.extraData} />
|
||||
pieceId={pieceId} />
|
||||
<PieceExtraDataForm
|
||||
name='technology_details'
|
||||
title='Technology Details'
|
||||
editable={editable}
|
||||
extraData={extraData}
|
||||
handleSuccess={this.showNotification}
|
||||
editable={this.props.editable}
|
||||
pieceId={this.props.pieceId}
|
||||
extraData={this.props.extraData} />
|
||||
pieceId={pieceId} />
|
||||
<Form>
|
||||
<FurtherDetailsFileuploader
|
||||
submitFile={this.submitFile}
|
||||
setIsUploadReady={this.setIsUploadReady}
|
||||
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
|
||||
editable={this.props.editable}
|
||||
editable={editable}
|
||||
overrideForm={true}
|
||||
pieceId={this.props.pieceId}
|
||||
otherData={this.props.otherData}
|
||||
pieceId={pieceId}
|
||||
otherData={otherData}
|
||||
multiple={true} />
|
||||
</Form>
|
||||
</Col>
|
||||
|
@ -8,6 +8,7 @@ import ReactS3FineUploader from './../ascribe_uploader/react_s3_fine_uploader';
|
||||
|
||||
import ApiUrls from '../../constants/api_urls';
|
||||
import AppConstants from '../../constants/application_constants';
|
||||
import { validationTypes } from '../../constants/uploader_constants';
|
||||
|
||||
import { getCookie } from '../../utils/fetch_api_utils';
|
||||
import { getLangText } from '../../utils/lang_utils';
|
||||
@ -15,21 +16,26 @@ import { getLangText } from '../../utils/lang_utils';
|
||||
|
||||
let FurtherDetailsFileuploader = React.createClass({
|
||||
propTypes: {
|
||||
pieceId: React.PropTypes.number.isRequired,
|
||||
|
||||
areAssetsDownloadable: React.PropTypes.bool,
|
||||
editable: React.PropTypes.bool,
|
||||
isReadyForFormSubmission: React.PropTypes.func,
|
||||
label: React.PropTypes.string,
|
||||
pieceId: React.PropTypes.number,
|
||||
multiple: React.PropTypes.bool,
|
||||
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
onValidationFailed: React.PropTypes.func,
|
||||
setIsUploadReady: React.PropTypes.func,
|
||||
submitFile: React.PropTypes.func,
|
||||
onValidationFailed: React.PropTypes.func,
|
||||
isReadyForFormSubmission: React.PropTypes.func,
|
||||
editable: React.PropTypes.bool,
|
||||
multiple: React.PropTypes.bool
|
||||
validation: ReactS3FineUploader.propTypes.validation
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
areAssetsDownloadable: true,
|
||||
label: getLangText('Additional files'),
|
||||
multiple: false
|
||||
multiple: false,
|
||||
validation: validationTypes.additionalData
|
||||
};
|
||||
},
|
||||
|
||||
@ -59,7 +65,7 @@ let FurtherDetailsFileuploader = React.createClass({
|
||||
url: ApiUrls.blob_otherdatas,
|
||||
pieceId: this.props.pieceId
|
||||
}}
|
||||
validation={AppConstants.fineUploader.validation.additionalData}
|
||||
validation={this.props.validation}
|
||||
submitFile={this.props.submitFile}
|
||||
onValidationFailed={this.props.onValidationFailed}
|
||||
setIsUploadReady={this.props.setIsUploadReady}
|
||||
@ -91,7 +97,7 @@ let FurtherDetailsFileuploader = React.createClass({
|
||||
'X-CSRFToken': getCookie(AppConstants.csrftoken)
|
||||
}
|
||||
}}
|
||||
areAssetsDownloadable={true}
|
||||
areAssetsDownloadable={this.props.areAssetsDownloadable}
|
||||
areAssetsEditable={this.props.editable}
|
||||
multiple={this.props.multiple} />
|
||||
</Property>
|
||||
|
@ -14,7 +14,9 @@ import CollapsibleButton from './../ascribe_collapsible/collapsible_button';
|
||||
|
||||
import AclProxy from '../acl_proxy';
|
||||
|
||||
import { getLangText } from '../../utils/lang_utils.js';
|
||||
import { getLangText } from '../../utils/lang_utils';
|
||||
import { extractFileExtensionFromString } from '../../utils/file_utils';
|
||||
|
||||
|
||||
const EMBED_IFRAME_HEIGHT = {
|
||||
video: 315,
|
||||
@ -68,6 +70,16 @@ let MediaContainer = React.createClass({
|
||||
// content was registered by the current user.
|
||||
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'] ?
|
||||
content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe;
|
||||
let mimetype = content.digital_work.mime;
|
||||
@ -124,7 +136,11 @@ let MediaContainer = React.createClass({
|
||||
className="ascribe-margin-1px"
|
||||
href={content.digital_work.url}
|
||||
target="_blank">
|
||||
{getLangText('Download')} .{mimetype} <Glyphicon glyph="cloud-download" />
|
||||
{/*
|
||||
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>
|
||||
</AclProxy>
|
||||
{embed}
|
||||
|