1
0
mirror of https://github.com/ascribe/onion.git synced 2024-12-22 09:23:13 +01:00

Merge branch 'master' into AD-601-in-piece_detail-longer-titles-are

This commit is contained in:
Tim Daubenschütz 2015-07-30 11:49:48 +02:00
commit 52f10f178f
50 changed files with 1047 additions and 429 deletions

View File

@ -5,6 +5,7 @@
"es6": true
},
"rules": {
"new-cap": [2, {newIsCap: true, capIsNew: false}],
"quotes": [2, "single"],
"eol-last": [0],
"no-mixed-requires": [0],
@ -34,7 +35,8 @@
"globals": {
"Intercom": true,
"fetch": true,
"require": true
"require": true,
"File": true
},
"plugins": [
"react"

View File

@ -51,8 +51,6 @@
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-60614729-2', 'auto');
</script>
<!-- Intercom library -->

View File

@ -7,7 +7,8 @@ import CoaFetcher from '../fetchers/coa_fetcher';
class CoaActions {
constructor() {
this.generateActions(
'updateCoa'
'updateCoa',
'flushCoa'
);
}
@ -26,9 +27,7 @@ class CoaActions {
this.actions.updateCoa(res.coa);
})
.catch((err) => {
console.log(err)
console.logGlobal(err);
this.actions.updateCoa('Something went wrong, please try again later.');
});
}
}

View File

@ -1,6 +1,7 @@
'use strict';
import alt from '../alt';
import Q from 'q';
import EditionListFetcher from '../fetchers/edition_list_fetcher.js';
@ -28,7 +29,7 @@ class EditionListActions {
pageSize = 10;
}
return new Promise((resolve, reject) => {
return Q.Promise((resolve, reject) => {
EditionListFetcher
.fetch(pieceId, page, pageSize, orderBy, orderAsc)
.then((res) => {

View File

@ -0,0 +1,19 @@
'use strict';
import alt from '../alt';
class EventActions {
constructor() {
this.generateActions(
'applicationWillBoot',
'applicationDidBoot',
'profileDidLoad',
//'userDidLogin',
//'userDidLogout',
'routeDidChange'
);
}
}
export default alt.createActions(EventActions);

View File

@ -1,6 +1,7 @@
'use strict';
import alt from '../alt';
import Q from 'q';
import PieceListFetcher from '../fetchers/piece_list_fetcher';
@ -29,7 +30,7 @@ class PieceListActions {
// afterwards, we can load the list
return new Promise((resolve, reject) => {
return Q.Promise((resolve, reject) => {
PieceListFetcher
.fetch(page, pageSize, search, orderBy, orderAsc)
.then((res) => {

View File

@ -13,7 +13,7 @@ class UserActions {
}
fetchCurrentUser() {
UserFetcher.fetchOne()
return UserFetcher.fetchOne()
.then((res) => {
this.actions.updateCurrentUser(res.users[0]);
})

View File

@ -5,7 +5,9 @@ require('babel/polyfill');
import React from 'react';
import Router from 'react-router';
/* eslint-disable */
import fetch from 'isomorphic-fetch';
/* eslint-enable */
import ApiUrls from './constants/api_urls';
import { updateApiUrls } from './constants/api_urls';
@ -16,6 +18,16 @@ import requests from './utils/requests';
import { getSubdomainSettings } from './utils/constants_utils';
import { initLogging } from './utils/error_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';
/* eslint-enable */
initLogging();
let headers = {
@ -43,25 +55,30 @@ class AppGateway {
settings = getSubdomainSettings(subdomain);
appConstants.whitelabel = settings;
updateApiUrls(settings.type, subdomain);
this.load(settings.type);
this.load(settings);
} catch(err) {
// if there are no matching subdomains, we're routing
// to the default frontend
console.logGlobal(err);
this.load('default');
this.load();
}
}
load(type) {
load(settings) {
let type = 'default';
if (settings) {
type = settings.type;
}
EventActions.applicationWillBoot(settings);
Router.run(getRoutes(type), Router.HistoryLocation, (App) => {
if (window.ga) {
window.ga('send', 'pageview');
}
React.render(
<App />,
document.getElementById('main')
);
EventActions.routeDidChange();
});
EventActions.applicationDidBoot(settings);
}
}

View File

@ -68,6 +68,15 @@ let Edition = React.createClass({
},
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);
PieceListStore.unlisten(this.onChange);
},
@ -398,7 +407,7 @@ let CoaDetails = React.createClass({
componentDidMount() {
CoaStore.listen(this.onChange);
if (this.props.edition.coa) {
if(this.props.edition.coa) {
CoaActions.fetchOne(this.props.edition.coa);
}
else {
@ -415,7 +424,7 @@ let CoaDetails = React.createClass({
},
render() {
if (this.state.coa && this.state.coa.url_safe) {
if(this.state.coa && this.state.coa.url_safe) {
return (
<div>
<p className="text-center ascribe-button-list">
@ -433,8 +442,7 @@ let CoaDetails = React.createClass({
</p>
</div>
);
}
else if (typeof this.state.coa === 'string'){
} else if(typeof this.state.coa === 'string'){
return (
<div className="text-center">
{this.state.coa}

View File

@ -12,6 +12,11 @@ import CollapsibleButton from './../ascribe_collapsible/collapsible_button';
import AclProxy from '../acl_proxy';
const EMBED_IFRAME_HEIGHT = {
video: 315,
audio: 62
};
let MediaContainer = React.createClass({
propTypes: {
content: React.PropTypes.object
@ -29,7 +34,9 @@ let MediaContainer = React.createClass({
extraData = this.props.content.digital_work.encoding_urls.map(e => { return { url: e.url, type: e.label }; });
}
if (['video', 'audio'].indexOf(mimetype) > -1){
if (['video', 'audio'].indexOf(mimetype) > -1) {
let height = EMBED_IFRAME_HEIGHT[mimetype];
embed = (
<CollapsibleButton
button={
@ -39,7 +46,7 @@ let MediaContainer = React.createClass({
}
panel={
<pre className="">
{'<iframe width="560" height="315" src="http://embed.ascribe.io/content/'
{'<iframe width="560" height="' + height + '" src="http://embed.ascribe.io/content/'
+ this.props.content.bitcoin_id + '" frameborder="0" allowfullscreen></iframe>'}
</pre>
}/>
@ -55,6 +62,7 @@ let MediaContainer = React.createClass({
encodingStatus={this.props.content.digital_work.isEncoding} />
<p className="text-center">
<AclProxy
show={['video', 'audio', 'image'].indexOf(mimetype) === -1 || this.props.content.acl.acl_download}
aclObject={this.props.content.acl}
aclName="acl_download">
<Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.content.digital_work.url} target="_blank">

View File

@ -45,7 +45,7 @@ let Form = React.createClass({
this.setState({submitted: true});
this.clearErrors();
let action = (this.httpVerb && this.httpVerb()) || 'post';
this[action]();
window.setTimeout(() => this[action](), 100);
},
post(){
requests
@ -59,6 +59,7 @@ let Form = React.createClass({
for (let ref in this.refs){
data[this.refs[ref].props.name] = this.refs[ref].state.value;
}
if ('getFormData' in this.props){
data = mergeOptionsWithDuplicates(data, this.props.getFormData());
}
@ -90,7 +91,7 @@ let Form = React.createClass({
}
}
else {
console.logGlobal(err);
console.logGlobal(err, false, this.getFormData());
this.setState({errors: [getLangText('Something went wrong, please try again later')]});
}
this.setState({submitted: false});

View File

@ -15,7 +15,6 @@ import LoanContractActions from '../../actions/loan_contract_actions';
import AppConstants from '../../constants/application_constants';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
@ -59,7 +58,8 @@ let LoanForm = React.createClass({
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox>
<InputCheckbox
defaultChecked={false}>
<span>
{getLangText('I agree to the')}&nbsp;
<a href={this.state.contractUrl} target="_blank">
@ -72,14 +72,11 @@ let LoanForm = React.createClass({
} else {
return (
<Property
hidden={true}
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<input
ref="input"
type="checkbox"
defaultValue={true} />
style={{paddingBottom: 0}}
hidden={true}>
<InputCheckbox
defaultChecked={true} />
</Property>
);
}
@ -134,8 +131,7 @@ let LoanForm = React.createClass({
</Property>
<Property
name='gallery_name'
label={getLangText('Gallery/exhibition (optional)')}
onBlur={this.handleOnBlur}>
label={getLangText('Gallery/exhibition (optional)')}>
<input
type="text"
placeholder={getLangText('Gallery/exhibition (optional)')}/>

View File

@ -25,7 +25,8 @@ let LoginForm = React.createClass({
headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string,
redirectOnLoggedIn: React.PropTypes.bool,
redirectOnLoginSuccess: React.PropTypes.bool
redirectOnLoginSuccess: React.PropTypes.bool,
onLogin: React.PropTypes.func
},
mixins: [Router.Navigation],
@ -70,18 +71,29 @@ let LoginForm = React.createClass({
// The easiest way to check if the user was successfully logged in is to fetch the user
// in the user store (which is obviously only possible if the user is logged in), since
// register_piece is listening to the changes of the user_store.
UserActions.fetchCurrentUser();
UserActions.fetchCurrentUser()
.then(() => {
if(this.props.redirectOnLoginSuccess) {
/* Taken from http://stackoverflow.com/a/14916411 */
/*
We actually have to trick the Browser into showing the "save password" dialog
as Chrome expects the login page to be reloaded after the login.
Users on Stack Overflow claim this is a bug in chrome and should be fixed in the future.
Until then, we redirect the HARD way, but reloading the whole page using window.location
*/
window.location = AppConstants.baseUrl + 'collection';
} else if(this.props.onLogin) {
// In some instances we want to give a callback to an outer container,
// to show that the one login action the user triggered actually went through.
// We can not do this by listening on a store's state as it wouldn't really tell us
// if the user did log in or was just fetching the user's data again
this.props.onLogin();
}
})
.catch((err) => {
console.logGlobal(err);
});
/* Taken from http://stackoverflow.com/a/14916411 */
/*
We actually have to trick the Browser into showing the "save password" dialog
as Chrome expects the login page to be reloaded after the login.
Users on Stack Overflow claim this is a bug in chrome and should be fixed in the future.
Until then, we redirect the HARD way, but reloading the whole page using window.location
*/
if(this.props.redirectOnLoginSuccess) {
window.location = AppConstants.baseUrl + 'collection';
}
},
render() {

View File

@ -2,18 +2,21 @@
import React from 'react';
import AppConstants from '../../constants/application_constants';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
import Form from './form';
import Property from './property';
import FormPropertyHeader from './form_property_header';
import apiUrls from '../../constants/api_urls';
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
import AppConstants from '../../constants/application_constants';
import apiUrls from '../../constants/api_urls';
import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
let RegisterPieceForm = React.createClass({
@ -21,8 +24,9 @@ let RegisterPieceForm = React.createClass({
headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string,
handleSuccess: React.PropTypes.func,
isFineUploaderEditable: React.PropTypes.bool,
children: React.PropTypes.element
isFineUploaderActive: React.PropTypes.bool,
children: React.PropTypes.element,
onLoggedOut: React.PropTypes.func
},
getDefaultProps() {
@ -33,10 +37,26 @@ let RegisterPieceForm = React.createClass({
},
getInitialState(){
return {
digitalWorkKey: null,
isUploadReady: false
};
return mergeOptions(
{
digitalWorkKey: null,
isUploadReady: false
},
UserStore.getState()
);
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
getFormData(){
@ -67,6 +87,9 @@ let RegisterPieceForm = React.createClass({
},
render() {
let currentUser = this.state.currentUser;
let enableLocalHashing = currentUser && currentUser.profile ? currentUser.profile.hash_locally : false;
return (
<Form
className="ascribe-form-bordered"
@ -94,7 +117,10 @@ let RegisterPieceForm = React.createClass({
submitKey={this.submitKey}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={this.isReadyForFormSubmission}
editable={this.props.isFineUploaderEditable}/>
isFineUploaderActive={this.props.isFineUploaderActive}
onLoggedOut={this.props.onLoggedOut}
editable={this.props.isFineUploaderEditable}
enableLocalHashing={enableLocalHashing}/>
</Property>
<Property
name='artist_name'
@ -133,10 +159,14 @@ let FileUploader = React.createClass({
submitKey: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
onClick: React.PropTypes.func,
// editable is used to lock react fine uploader in case
// isFineUploaderActive is used to lock react fine uploader in case
// a user is actually not logged in already to prevent him from droping files
// before login in
editable: React.PropTypes.bool
isFineUploaderActive: React.PropTypes.bool,
onLoggedOut: React.PropTypes.func,
editable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool
},
render() {
@ -158,7 +188,7 @@ let FileUploader = React.createClass({
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
areAssetsDownloadable={false}
areAssetsEditable={this.props.editable}
areAssetsEditable={this.props.isFineUploaderActive}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/',
customHeaders: {
@ -172,7 +202,9 @@ let FileUploader = React.createClass({
customHeaders: {
'X-CSRFToken': getCookie(AppConstants.csrftoken)
}
}}/>
}}
onInactive={this.props.onLoggedOut}
enableLocalHashing={this.props.enableLocalHashing} />
);
}
});

View File

@ -63,9 +63,7 @@ let SignupForm = React.createClass({
this.props.handleSuccess(getLangText('We sent an email to your address') + ' ' + response.user.email + ', ' + getLangText('please confirm') + '.');
},
getFormData(){
return {terms: this.refs.form.refs.terms.refs.input.state.value};
},
render() {
let tooltipPassword = getLangText('Your password must be at least 10 characters') + '.\n ' +
getLangText('This password is securing your digital property like a bank account') + '.\n ' +
@ -76,7 +74,6 @@ let SignupForm = React.createClass({
ref='form'
url={apiUrls.users_signup}
handleSuccess={this.handleSuccess}
getFormData={this.getFormData}
buttons={
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
{this.props.submitMessage}

View File

@ -2,45 +2,87 @@
import React from 'react';
/**
* This component can be used as a custom input element for form properties.
* It exposes its state via state.value and can be considered as a reference implementation
* for custom input components that live inside of properties.
*/
let InputCheckbox = React.createClass({
propTypes: {
required: React.PropTypes.string.isRequired,
required: React.PropTypes.bool,
// As can be read here: https://facebook.github.io/react/docs/forms.html
// inputs of type="checkbox" define their state via checked.
// Their default state is defined via defaultChecked.
//
// Since this component even has checkbox in its name, it felt wrong to expose defaultValue
// as the default-setting prop to other developers, which is why we choose defaultChecked.
defaultChecked: React.PropTypes.bool,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]).isRequired
])
},
getInitialState() {
// As HTML inputs, we're setting the default value for an input to checked === false
getDefaultProps() {
return {
show: false
defaultChecked: false
};
},
handleFocus(event) {
this.refs.checkbox.getDOMNode().checked = !this.refs.checkbox.getDOMNode().checked;
// This is calling property.js's method handleChange which
// expects an event object
// Since we don't have a valid one, we'll just manipulate the one we get and send
// it to handleChange
event.target.value = this.refs.checkbox.getDOMNode().checked;
this.props.onChange(event);
event.stopPropagation();
// Setting value to null in initialState is essentially since we're deriving a certain state from
// value === null as can be seen in componentWillReceiveProps.
getInitialState() {
return {
value: null
};
},
this.setState({
show: this.refs.checkbox.getDOMNode().checked,
value: this.refs.checkbox.getDOMNode().checked
componentWillReceiveProps(nextProps) {
// Developer's are used to define defaultValues for inputs via defaultValue, but since this is a
// input of type checkbox we warn the dev to not do that.
if(this.props.defaultValue) {
console.warn('InputCheckbox is of type checkbox. Therefore its value is represented by checked and defaultChecked. defaultValue will do nothing!');
}
// The first time InputCheckbox is rendered, we want to set its value to the value of defaultChecked.
// This needs to be done in order to expose it for the Property component.
// We can determine the first render by checking if value still has it's initialState(from getInitialState)
if(this.state.value === null) {
this.setState({value: nextProps.defaultChecked });
}
},
onChange() {
// On every change, we're inversing the input's value
let inverseValue = !this.refs.checkbox.getDOMNode().checked;
// pass it to the state
this.setState({value: inverseValue});
// and also call Property's onChange method
// (in this case we're mocking event.target.value, since we can not use the event
// coming from onChange. Its coming from the span (user is clicking on the span) and not the input)
this.props.onChange({
target: {
value: inverseValue
}
});
},
render() {
return (
<span
onClick={this.handleFocus}
onFocus={this.handleFocus}>
<input type="checkbox" ref="checkbox" required="required"/>
onClick={this.onChange}>
<input
type="checkbox"
ref="checkbox"
onChange={this.onChange}
checked={this.state.value}
defaultChecked={this.props.defaultChecked}/>
<span className="checkbox">
{this.props.children}
</span>

View File

@ -1,39 +0,0 @@
'use strict';
import React from 'react';
import AlertMixin from '../../mixins/alert_mixin';
let InputHidden = React.createClass({
propTypes: {
submitted: React.PropTypes.bool,
value: React.PropTypes.string
},
mixins: [AlertMixin],
getInitialState() {
return {value: this.props.value,
alerts: null // needed in AlertMixin
};
},
handleChange(event) {
this.setState({value: event.target.value});
},
render() {
let alerts = (this.props.submitted) ? null : this.state.alerts;
return (
<div className="form-group">
{alerts}
<input
value={this.props.value}
type="hidden"
onChange={this.handleChange}
/>
</div>
);
}
});
export default InputHidden;

View File

@ -1,46 +0,0 @@
'use strict';
import React from 'react';
import AlertMixin from '../../mixins/alert_mixin';
let InputText = React.createClass({
propTypes: {
submitted: React.PropTypes.bool,
onBlur: React.PropTypes.func,
type: React.PropTypes.string,
required: React.PropTypes.string,
placeHolder: React.PropTypes.string
},
mixins: [AlertMixin],
getInitialState() {
return {value: null,
alerts: null // needed in AlertMixin
};
},
handleChange(event) {
this.setState({value: event.target.value});
},
render() {
let className = 'form-control input-text-ascribe';
let alerts = (this.props.submitted) ? null : this.state.alerts;
return (
<div className="form-group">
{alerts}
<input className={className}
placeholder={this.props.placeHolder}
required={this.props.required}
type={this.props.type}
onChange={this.handleChange}
onBlur={this.props.onBlur}/>
</div>
);
}
});
export default InputText;

View File

@ -1,41 +0,0 @@
'use strict';
import React from 'react';
import AlertMixin from '../../mixins/alert_mixin';
let InputTextArea = React.createClass({
propTypes: {
submitted: React.PropTypes.bool,
required: React.PropTypes.string,
defaultValue: React.PropTypes.string
},
mixins: [AlertMixin],
getInitialState() {
return {
value: this.props.defaultValue,
alerts: null // needed in AlertMixin
};
},
handleChange(event) {
this.setState({value: event.target.value});
},
render() {
let className = 'form-control input-text-ascribe textarea-ascribe-message';
let alerts = (this.props.submitted) ? null : this.state.alerts;
return (
<div className="form-group">
{alerts}
<textarea className={className}
defaultValue={this.props.defaultValue}
required={this.props.required}
onChange={this.handleChange}></textarea>
</div>
);
}
});
export default InputTextArea;

View File

@ -39,6 +39,9 @@ let Property = React.createClass({
getInitialState() {
return {
// Please don't confuse initialValue with react's defaultValue.
// initialValue is set by us to ensure that a user can reset a specific
// property (after editing) to its initial value
initialValue: null,
value: null,
isFocused: false,
@ -47,19 +50,29 @@ let Property = React.createClass({
},
componentWillReceiveProps() {
let childInput = this.refs.input;
// In order to set this.state.value from another component
// the state of value should only be set if its not undefined and
// actually references something
if(typeof this.refs.input.getDOMNode().value !== 'undefined') {
if(typeof childInput.getDOMNode().value !== 'undefined') {
this.setState({
value: this.refs.input.getDOMNode().value
value: childInput.getDOMNode().value
});
// When implementing custom input components, their value isn't exposed like the one
// from native HTML elements.
// To enable developers to create input elements, they can expose a property called value
// in their state that will be picked up by property.js
} else if(childInput.state && typeof childInput.state.value !== 'undefined') {
this.setState({
value: childInput.state.value
});
}
if(!this.state.initialValue) {
this.setState({
initialValue: this.refs.input.getDOMNode().defaultValue
initialValue: childInput.defaultValue
});
}
},
@ -158,7 +171,6 @@ let Property = React.createClass({
renderChildren() {
return ReactAddons.Children.map(this.props.children, (child) => {
return ReactAddons.addons.cloneWithProps(child, {
value: this.state.value,
onChange: this.handleChange,
onFocus: this.handleFocus,
onBlur: this.handleBlur,

View File

@ -1,6 +1,8 @@
'use strict';
import React from 'react';
import Q from 'q';
import InjectInHeadMixin from '../../mixins/inject_in_head_mixin';
import Panel from 'react-bootstrap/lib/Panel';
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
@ -47,7 +49,7 @@ let Image = React.createClass({
componentDidMount() {
this.inject('https://code.jquery.com/jquery-2.1.4.min.js')
.then(() =>
Promise.all([
Q.all([
this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/shmui.css'),
this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/jquery.shmui.js')
]).then(() => { window.jQuery('.shmui-ascribe').shmui(); }));
@ -99,7 +101,7 @@ let Video = React.createClass({
},
componentDidMount() {
Promise.all([
Q.all([
this.inject('//vjs.zencdn.net/4.12/video-js.css'),
this.inject('//vjs.zencdn.net/4.12/video.js')
]).then(this.ready);

View File

@ -17,7 +17,7 @@ let SlidesContainer = React.createClass({
getInitialState() {
// handle queryParameters
let queryParams = this.getQuery();
let slideNum = 0;
let slideNum = -1;
if(queryParams && 'slide_num' in queryParams) {
slideNum = parseInt(queryParams.slide_num, 10);
@ -26,7 +26,8 @@ let SlidesContainer = React.createClass({
return {
containerWidth: 0,
slideNum: slideNum
slideNum: slideNum,
historyLength: window.history.length
};
},
@ -34,7 +35,12 @@ let SlidesContainer = React.createClass({
// check if slide_num was defined, and if not then default to 0
let queryParams = this.getQuery();
if(!('slide_num' in queryParams)) {
this.replaceWith(this.getPathname(), null, {slide_num: 0});
// we're first requiring all the other possible queryParams and then set
// the specific one we need instead of overwriting them
queryParams.slide_num = 0;
this.replaceWith(this.getPathname(), null, queryParams);
}
// init container width
@ -45,22 +51,73 @@ let SlidesContainer = React.createClass({
window.addEventListener('resize', this.handleContainerResize);
},
componentDidUpdate() {
// check if slide_num was defined, and if not then default to 0
let queryParams = this.getQuery();
this.setSlideNum(queryParams.slide_num);
},
componentWillUnmount() {
window.removeEventListener('resize', this.handleContainerResize);
},
handleContainerResize() {
this.setState({
containerWidth: this.refs.containerWrapper.getDOMNode().offsetWidth
// +30 to get rid of the padding of the container which is 15px + 15px left and right
containerWidth: this.refs.containerWrapper.getDOMNode().offsetWidth + 30
});
},
// We let every one from the outsite set the page number of the slider,
// though only if the slideNum is actually in the range of our children-list.
setSlideNum(slideNum) {
if(slideNum < 0 || slideNum < React.Children.count(this.props.children)) {
this.replaceWith(this.getPathname(), null, {slide_num: slideNum});
// we do not want to overwrite other queryParams
let queryParams = this.getQuery();
// slideNum can in some instances be not a number,
// therefore we have to parse it to one and make sure that its not NaN
slideNum = parseInt(slideNum, 10);
// if slideNum is not a number (even after we parsed it to one) and there has
// never been a transition to another slide (this.state.slideNum ==== -1 indicates that)
// then we want to "replace" (in this case append) the current url with ?slide_num=0
if(isNaN(slideNum) && this.state.slideNum === -1) {
slideNum = 0;
queryParams.slide_num = slideNum;
this.replaceWith(this.getPathname(), null, queryParams);
this.setState({slideNum: slideNum});
return;
// slideNum always represents the future state. So if slideNum and
// this.state.slideNum are equal, there is no sense in redirecting
} else if(slideNum === this.state.slideNum) {
return;
// if slideNum is within the range of slides and none of the previous cases
// where matched, we can actually do transitions
} else if(slideNum >= 0 || slideNum < React.Children.count(this.props.children)) {
if(slideNum !== this.state.slideNum - 1) {
// Bootstrapping the component, getInitialState is called once to save
// the tabs history length.
// In order to know if we already pushed a new state on the history stack or not,
// we're comparing the old history length with the new one and if it didn't change then
// we push a new state on it ONCE (ever).
// Otherwise, we're able to use the browsers history.forward() method
// to keep the stack clean
if(this.state.historyLength === window.history.length) {
queryParams.slide_num = slideNum;
this.transitionTo(this.getPathname(), null, queryParams);
} else {
window.history.forward();
}
}
this.setState({
slideNum: slideNum
});

View File

@ -1,10 +1,12 @@
'use strict';
import React from 'react';
import ProgressBar from 'react-progressbar';
import FileDragAndDropDialog from './file_drag_and_drop_dialog';
import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator';
import { getLangText } from '../../utils/lang_utils';
// Taken from: https://github.com/fedosejev/react-file-drag-and-drop
let FileDragAndDrop = React.createClass({
@ -18,6 +20,7 @@ let FileDragAndDrop = React.createClass({
onDragLeave: React.PropTypes.func,
onDragOver: React.PropTypes.func,
onDragEnd: React.PropTypes.func,
onInactive: React.PropTypes.func,
filesToUpload: React.PropTypes.array,
handleDeleteFile: React.PropTypes.func,
handleCancelFile: React.PropTypes.func,
@ -26,7 +29,15 @@ let FileDragAndDrop = React.createClass({
multiple: React.PropTypes.bool,
dropzoneInactive: React.PropTypes.bool,
areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool
areAssetsEditable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool,
// triggers a FileDragAndDrop-global spinner
hashingProgress: React.PropTypes.number,
// sets the value of this.state.hashingProgress in reactfineuploader
// to -1 which is code for: aborted
handleCancelHashing: React.PropTypes.func
},
handleDragStart(event) {
@ -72,6 +83,15 @@ let FileDragAndDrop = React.createClass({
event.stopPropagation();
let files;
if(this.props.dropzoneInactive) {
// if there is a handle function for doing stuff
// when the dropzone is inactive, then call it
if(this.props.onInactive) {
this.props.onInactive();
}
return;
}
// handle Drag and Drop
if(event.dataTransfer && event.dataTransfer.files.length > 0) {
files = event.dataTransfer.files;
@ -113,10 +133,15 @@ let FileDragAndDrop = React.createClass({
this.props.handleResumeFile(fileId);
},
handleOnClick(event) {
handleOnClick() {
// when multiple is set to false and the user already uploaded a piece,
// do not propagate event
if(this.props.dropzoneInactive) {
// if there is a handle function for doing stuff
// when the dropzone is inactive, then call it
if(this.props.onInactive) {
this.props.onInactive();
}
return;
}
@ -140,40 +165,55 @@ let FileDragAndDrop = React.createClass({
className += this.props.dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone';
className += this.props.className ? ' ' + this.props.className : '';
return (
<div
className={className}
onDragStart={this.handleDragStart}
onDrag={this.handleDrop}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onDragOver={this.handleDragOver}
onDrop={this.handleDrop}
onDragEnd={this.handleDragEnd}>
<FileDragAndDropDialog
multipleFiles={this.props.multiple}
hasFiles={hasFiles}
onClick={this.handleOnClick}/>
<FileDragAndDropPreviewIterator
files={this.props.filesToUpload}
handleDeleteFile={this.handleDeleteFile}
handleCancelFile={this.handleCancelFile}
handlePauseFile={this.handlePauseFile}
handleResumeFile={this.handleResumeFile}
areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={this.props.areAssetsEditable}/>
<input
multiple={this.props.multiple}
ref="fileinput"
type="file"
style={{
display: 'none',
height: 0,
width: 0
}}
onChange={this.handleDrop} />
</div>
);
// if !== -2: triggers a FileDragAndDrop-global spinner
if(this.props.hashingProgress !== -2) {
return (
<div className={className}>
<p>{getLangText('Computing hash(es)... This may take a few minutes.')}</p>
<p>
<span>{Math.ceil(this.props.hashingProgress)}%</span>
<a onClick={this.props.handleCancelHashing}> {getLangText('Cancel hashing')}</a>
</p>
<ProgressBar completed={this.props.hashingProgress} color="#48DACB"/>
</div>
);
} else {
return (
<div
className={className}
onDragStart={this.handleDragStart}
onDrag={this.handleDrop}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onDragOver={this.handleDragOver}
onDrop={this.handleDrop}
onDragEnd={this.handleDragEnd}>
<FileDragAndDropDialog
multipleFiles={this.props.multiple}
hasFiles={hasFiles}
onClick={this.handleOnClick}
enableLocalHashing={this.props.enableLocalHashing}/>
<FileDragAndDropPreviewIterator
files={this.props.filesToUpload}
handleDeleteFile={this.handleDeleteFile}
handleCancelFile={this.handleCancelFile}
handlePauseFile={this.handlePauseFile}
handleResumeFile={this.handleResumeFile}
areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={this.props.areAssetsEditable}/>
<input
multiple={this.props.multiple}
ref="fileinput"
type="file"
style={{
display: 'none',
height: 0,
width: 0
}}
onChange={this.handleDrop} />
</div>
);
}
}
});

View File

@ -1,34 +1,80 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
let FileDragAndDropDialog = React.createClass({
propTypes: {
hasFiles: React.PropTypes.bool,
multipleFiles: React.PropTypes.bool,
onClick: React.PropTypes.func
onClick: React.PropTypes.func,
enableLocalHashing: React.PropTypes.bool
},
mixins: [Router.State],
render() {
const queryParams = this.getQuery();
if(this.props.hasFiles) {
return null;
} else {
if(this.props.multipleFiles) {
if(this.props.enableLocalHashing && !queryParams.method) {
let queryParamsHash = Object.assign({}, queryParams);
queryParamsHash.method = 'hash';
let queryParamsUpload = Object.assign({}, queryParams);
queryParamsUpload.method = 'upload';
return (
<span className="file-drag-and-drop-dialog">Click or drag to add files</span>
);
} else {
return (
<span className="file-drag-and-drop-dialog">
<p>Drag a file here</p>
<p>or</p>
<span
className="btn btn-default"
onClick={this.props.onClick}>
choose a file to upload
</span>
<span className="file-drag-and-drop-dialog present-options">
<p>{getLangText('Would you rather')}</p>
<Link
to={this.getPath()}
query={queryParamsHash}>
<span className="btn btn-default btn-sm">
{getLangText('Hash your work')}
</span>
</Link>
<span> or </span>
<Link
to={this.getPath()}
query={queryParamsUpload}>
<span className="btn btn-default btn-sm">
{getLangText('Upload and hash your work')}
</span>
</Link>
</span>
);
} else {
if(this.props.multipleFiles) {
return (
<span className="file-drag-and-drop-dialog">
{getLangText('Click or drag to add files')}
</span>
);
} else {
let dialog = queryParams.method === 'hash' ? getLangText('choose a file to hash') : getLangText('choose a file to upload');
return (
<span className="file-drag-and-drop-dialog">
<p>{getLangText('Drag a file here')}</p>
<p>{getLangText('or')}</p>
<span
className="btn btn-default"
onClick={this.props.onClick}>
{dialog}
</span>
</span>
);
}
}
}
}

View File

@ -4,7 +4,7 @@ import React from 'react';
import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image';
import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other';
import { getLangText } from '../../utils/lang_utils.js'
import { getLangText } from '../../utils/lang_utils.js';
let FileDragAndDropPreview = React.createClass({

View File

@ -4,7 +4,7 @@ import React from 'react';
import ProgressBar from 'react-progressbar';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js'
import { getLangText } from '../../utils/lang_utils.js';
let FileDragAndDropPreviewImage = React.createClass({
propTypes: {

View File

@ -4,7 +4,7 @@ import React from 'react';
import ProgressBar from 'react-progressbar';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js'
import { getLangText } from '../../utils/lang_utils.js';
let FileDragAndDropPreviewOther = React.createClass({
propTypes: {

View File

@ -1,7 +1,9 @@
'use strict';
import React from 'react/addons';
import Router from 'react-router';
import Raven from 'raven-js';
import Q from 'q';
import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils';
@ -16,6 +18,8 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import AppConstants from '../../constants/application_constants';
import { computeHashOfFile } from '../../utils/file_utils';
var ReactS3FineUploader = React.createClass({
propTypes: {
@ -95,9 +99,23 @@ var ReactS3FineUploader = React.createClass({
isReadyForFormSubmission: React.PropTypes.func,
areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool,
defaultErrorMessage: React.PropTypes.string
defaultErrorMessage: React.PropTypes.string,
onInactive: React.PropTypes.func,
// We encountered some cases where people had difficulties to upload their
// works to ascribe due to a slow internet connection.
// One solution we found in the process of tackling this problem was to hash
// the file in the browser using md5 and then uploading the resulting text document instead
// of the actual file.
// This boolean essentially enables that behavior
enableLocalHashing: React.PropTypes.bool,
// automatically injected by React-Router
query: React.PropTypes.object
},
mixins: [Router.State],
getDefaultProps() {
return {
autoUpload: true,
@ -120,7 +138,10 @@ var ReactS3FineUploader = React.createClass({
sendCredentials: true
},
chunking: {
enabled: true
enabled: true,
concurrent: {
enabled: true
}
},
resume: {
enabled: true
@ -132,7 +153,7 @@ var ReactS3FineUploader = React.createClass({
endpoint: null
},
messages: {
unsupportedBrowser: '<h3>Upload is not functional in IE7 as IE7 has no support for CORS!</h3>'
unsupportedBrowser: '<h3>' + getLangText('Upload is not functional in IE7 as IE7 has no support for CORS!') + '</h3>'
},
formatFileName: function(name){// fix maybe
if (name !== undefined && name.length > 26) {
@ -141,7 +162,7 @@ var ReactS3FineUploader = React.createClass({
return name;
},
multiple: false,
defaultErrorMessage: 'Unexpected error. Please contact us if this happens repeatedly.'
defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.')
};
},
@ -149,14 +170,17 @@ var ReactS3FineUploader = React.createClass({
return {
filesToUpload: [],
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()),
csrfToken: getCookie(AppConstants.csrftoken)
csrfToken: getCookie(AppConstants.csrftoken),
hashingProgress: -2
// -1: aborted
// -2: uninitialized
};
},
// since the csrf header is defined in this component's props,
// everytime the csrf cookie is changed we'll need to reinitalize
// fineuploader and update the actual csrf token
componentWillUpdate() {
// since the csrf header is defined in this component's props,
// everytime the csrf cookie is changed we'll need to reinitalize
// fineuploader and update the actual csrf token
let potentiallyNewCSRFToken = getCookie(AppConstants.csrftoken);
if(this.state.csrfToken !== potentiallyNewCSRFToken) {
this.setState({
@ -207,115 +231,127 @@ var ReactS3FineUploader = React.createClass({
},
requestKey(fileId) {
let defer = new fineUploader.Promise();
let filename = this.state.uploader.getName(fileId);
let uuid = this.state.uploader.getUuid(fileId);
window.fetch(this.props.keyRoutine.url, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': getCookie(AppConstants.csrftoken)
},
credentials: 'include',
body: JSON.stringify({
'filename': filename,
'category': this.props.keyRoutine.fileClass,
'uuid': uuid,
'piece_id': this.props.keyRoutine.pieceId
return Q.Promise((resolve, reject) => {
window.fetch(this.props.keyRoutine.url, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': getCookie(AppConstants.csrftoken)
},
credentials: 'include',
body: JSON.stringify({
'filename': filename,
'category': this.props.keyRoutine.fileClass,
'uuid': uuid,
'piece_id': this.props.keyRoutine.pieceId
})
})
})
.then((res) => {
return res.json();
})
.then((res) =>{
defer.success(res.key);
})
.catch((err) => {
defer.failure(err);
.then((res) => {
return res.json();
})
.then((res) =>{
resolve(res.key);
})
.catch((err) => {
reject(err);
});
});
return defer;
},
createBlob(file) {
let defer = new fineUploader.Promise();
window.fetch(this.props.createBlobRoutine.url, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': getCookie(AppConstants.csrftoken)
},
credentials: 'include',
body: JSON.stringify({
'filename': file.name,
'key': file.key,
'piece_id': this.props.createBlobRoutine.pieceId
return Q.Promise((resolve, reject) => {
window.fetch(this.props.createBlobRoutine.url, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': getCookie(AppConstants.csrftoken)
},
credentials: 'include',
body: JSON.stringify({
'filename': file.name,
'key': file.key,
'piece_id': this.props.createBlobRoutine.pieceId
})
})
})
.then((res) => {
return res.json();
})
.then((res) =>{
if(res.otherdata) {
file.s3Url = res.otherdata.url_safe;
file.s3UrlSafe = res.otherdata.url_safe;
} else if(res.digitalwork) {
file.s3Url = res.digitalwork.url_safe;
file.s3UrlSafe = res.digitalwork.url_safe;
} else {
throw new Error('Could not find a url to download.');
}
defer.success(res.key);
})
.catch((err) => {
defer.failure(err);
console.logGlobal(err);
.then((res) => {
return res.json();
})
.then((res) =>{
if(res.otherdata) {
file.s3Url = res.otherdata.url_safe;
file.s3UrlSafe = res.otherdata.url_safe;
} else if(res.digitalwork) {
file.s3Url = res.digitalwork.url_safe;
file.s3UrlSafe = res.digitalwork.url_safe;
} else {
throw new Error(getLangText('Could not find a url to download.'));
}
resolve(res);
})
.catch((err) => {
reject(err);
console.logGlobal(err);
});
});
return defer;
},
/* FineUploader specific callback function handlers */
onComplete(id) {
let files = this.state.filesToUpload;
// Set the state of the completed file to 'upload successful' in order to
// remove it from the GUI
files[id].status = 'upload successful';
files[id].key = this.state.uploader.getKey(id);
let newState = React.addons.update(this.state, {
filesToUpload: { $set: files }
});
this.setState(newState);
this.createBlob(files[id]);
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey
// are optional, we'll only trigger them when they're actually defined
if(this.props.submitKey) {
this.props.submitKey(files[id].key);
} else {
console.warn('You didn\'t define submitKey in as a prop in react-s3-fine-uploader');
}
this.setState(newState);
// Only after the blob has been created server-side, we can make the form submittable.
this.createBlob(files[id])
.then(() => {
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey
// are optional, we'll only trigger them when they're actually defined
if(this.props.submitKey) {
this.props.submitKey(files[id].key);
} else {
console.warn('You didn\'t define submitKey in as a prop in react-s3-fine-uploader');
}
// for explanation, check comment of if statement above
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
// also, lets check if after the completion of this upload,
// the form is ready for submission or not
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
}
})
.catch((err) => {
console.logGlobal(err);
let notification = new GlobalNotificationModel(err.message, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
});
// for explanation, check comment of if statement above
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
// also, lets check if after the completion of this upload,
// the form is ready for submission or not
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
}
},
onError() {
Raven.captureException('react-fineuploader-error');
onError(id, name, errorReason) {
Raven.captureException(errorReason);
let notification = new GlobalNotificationModel(this.props.defaultErrorMessage, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
@ -323,7 +359,9 @@ var ReactS3FineUploader = React.createClass({
onValidate(data) {
if(data.size > this.props.validation.sizeLimit) {
this.state.uploader.cancelAll();
let notification = new GlobalNotificationModel('Your file is bigger than 10MB', 'danger', 5000);
let fileSizeInMegaBytes = this.props.validation.sizeLimit / 1000000;
let notification = new GlobalNotificationModel(getLangText('Your file is bigger than %d MB', fileSizeInMegaBytes), 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
}
},
@ -331,7 +369,7 @@ var ReactS3FineUploader = React.createClass({
onCancel(id) {
this.removeFileWithIdFromFilesToUpload(id);
let notification = new GlobalNotificationModel('File upload canceled', 'success', 5000);
let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey
@ -448,7 +486,7 @@ var ReactS3FineUploader = React.createClass({
if(this.state.uploader.pauseUpload(fileId)) {
this.setStatusOfFile(fileId, 'paused');
} else {
throw new Error('File upload could not be paused.');
throw new Error(getLangText('File upload could not be paused.'));
}
},
@ -457,7 +495,7 @@ var ReactS3FineUploader = React.createClass({
if(this.state.uploader.continueUpload(fileId)) {
this.setStatusOfFile(fileId, 'uploading');
} else {
throw new Error('File upload could not be resumed.');
throw new Error(getLangText('File upload could not be resumed.'));
}
},
@ -481,10 +519,128 @@ var ReactS3FineUploader = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification);
}
this.state.uploader.addFiles(files);
// As mentioned already in the propTypes declaration, in some instances we need to calculate the
// md5 hash of a file locally and just upload a txt file containing that hash.
//
// In the view this only happens when the user is allowed to do local hashing as well
// as when the correct query parameter is present in the url ('hash' and not 'upload')
let queryParams = this.getQuery();
if(this.props.enableLocalHashing && queryParams && queryParams.method === 'hash') {
let convertedFilePromises = [];
let overallFileSize = 0;
// "files" is not a classical Javascript array but a Javascript FileList, therefore
// we can not use map to convert values
for(let i = 0; i < files.length; i++) {
// for calculating the overall progress of all submitted files
// we'll need to calculate the overall sum of all files' sizes
overallFileSize += files[i].size;
// also, we need to set the files' initial progress value
files[i].progress = 0;
// since the actual computation of a file's hash is an async task ,
// we're using promises to handle that
let hashedFilePromise = computeHashOfFile(files[i]);
convertedFilePromises.push(hashedFilePromise);
}
// To react after the computation of all files, we define the resolvement
// with the all function for iterables and essentially replace all original files
// with their txt representative
Q.all(convertedFilePromises)
.progress(({index, value: {progress, reject}}) => {
// hashing progress has been aborted from outside
// To get out of the executing, we need to call reject from the
// inside of the promise's execution.
// This is why we're passing (along with value) a function that essentially
// just does that (calling reject(err))
//
// In the promises catch method, we're then checking if the interruption
// was due to that error or another generic one.
if(this.state.hashingProgress === -1) {
reject(new Error(getLangText('Hashing canceled')));
}
// update file's progress
files[index].progress = progress;
// calculate weighted average for overall progress of all
// currently hashing files
let overallHashingProgress = 0;
for(let i = 0; i < files.length; i++) {
let filesSliceOfOverall = files[i].size / overallFileSize;
overallHashingProgress += filesSliceOfOverall * files[i].progress;
}
// Multiply by 100, since react-progressbar expects decimal numbers
this.setState({ hashingProgress: overallHashingProgress * 100});
})
.then((convertedFiles) => {
// clear hashing progress, since its done
this.setState({ hashingProgress: -2});
// actually replacing all files with their txt-hash representative
files = convertedFiles;
// routine for adding all the files submitted to fineuploader for actual uploading them
// to the server
this.state.uploader.addFiles(files);
this.synchronizeFileLists(files);
})
.catch((err) => {
// If the error is that hashing has been canceled, we want to display a success
// message instead of a danger message
let typeOfMessage = 'danger';
if(err.message === getLangText('Hashing canceled')) {
typeOfMessage = 'success';
this.setState({ hashingProgress: -2 });
} else {
// if there was a more generic error, we also log it
console.logGlobal(err);
}
let notification = new GlobalNotificationModel(err.message, typeOfMessage, 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
});
// if we're not hashing the files locally, we're just going to hand them over to fineuploader
// to upload them to the server
} else {
this.state.uploader.addFiles(files);
this.synchronizeFileLists(files);
}
},
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.
// 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
// fineuploader is keeping internally.
//
// Unfortunately though fineuploader is not keeping all of a File object's properties after
// submitting them via .addFiles (it deletes the type, key as well as the ObjectUrl (which we need for
// displaying a thumbnail)), we need to readd them manually after each file that gets submitted
// to the dropzone.
// This method is essentially taking care of all these steps.
synchronizeFileLists(files) {
let oldFiles = this.state.filesToUpload;
let oldAndNewFiles = this.state.uploader.getUploads();
// Add fineuploader specific information to new files
for(let i = 0; i < oldAndNewFiles.length; i++) {
for(let j = 0; j < files.length; j++) {
@ -543,6 +699,18 @@ var ReactS3FineUploader = React.createClass({
this.setState(newState);
},
isDropzoneInactive() {
let filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
let queryParams = this.getQuery();
if((this.props.enableLocalHashing && !queryParams.method) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
return true;
} else {
return false;
}
},
render() {
return (
<div>
@ -554,10 +722,14 @@ var ReactS3FineUploader = React.createClass({
handleCancelFile={this.handleCancelFile}
handlePauseFile={this.handlePauseFile}
handleResumeFile={this.handleResumeFile}
handleCancelHashing={this.handleCancelHashing}
multiple={this.props.multiple}
areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={this.props.areAssetsEditable}
dropzoneInactive={!this.props.areAssetsEditable || !this.props.multiple && this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0} />
onInactive={this.props.onInactive}
dropzoneInactive={this.isDropzoneInactive()}
hashingProgress={this.state.hashingProgress}
enableLocalHashing={this.props.enableLocalHashing} />
</div>
);
}

View File

@ -2,13 +2,13 @@
import React from 'react';
import Router from 'react-router';
import Raven from 'raven-js';
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 Nav from 'react-bootstrap/lib/Nav';
import Navbar from 'react-bootstrap/lib/Navbar';
@ -84,19 +84,7 @@ let Header = React.createClass({
this.setState(state);
if(this.state.currentUser && this.state.currentUser.email) {
// bootup intercom if the user is logged in
window.Intercom('boot', {
app_id: 'oboxh5w1',
email: this.state.currentUser.email,
subdomain: window.location.host.split('.')[0],
widget: {
activator: '#IntercomDefaultWidget'
}
});
Raven.setUserContext({
email: this.state.currentUser.email
});
EventActions.profileDidLoad.defer(this.state.currentUser);
}
},

View File

@ -14,7 +14,8 @@ let LoginContainer = React.createClass({
propTypes: {
message: React.PropTypes.string,
redirectOnLoggedIn: React.PropTypes.bool,
redirectOnLoginSuccess: React.PropTypes.bool
redirectOnLoginSuccess: React.PropTypes.bool,
onLogin: React.PropTypes.func
},
getDefaultProps() {
@ -31,7 +32,8 @@ let LoginContainer = React.createClass({
<LoginForm
redirectOnLoggedIn={this.props.redirectOnLoggedIn}
redirectOnLoginSuccess={this.props.redirectOnLoginSuccess}
message={this.props.message} />
message={this.props.message}
onLogin={this.props.onLogin}/>
<div className="ascribe-login-text">
{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>

View File

@ -59,7 +59,7 @@ let RegisterPiece = React.createClass( {
PieceListStore.getState(),
{
selectedLicense: 0,
isFineUploaderEditable: false
isFineUploaderActive: false
});
},
@ -82,14 +82,10 @@ let RegisterPiece = React.createClass( {
onChange(state) {
this.setState(state);
// once the currentUser object from UserStore is defined (eventually the user was transitioned
// to the login form via the slider and successfully logged in), we can direct him back to the
// register_piece slide
if(state.currentUser && state.currentUser.email || this.state.currentUser && this.state.currentUser.email) {
this.refs.slidesContainer.setSlideNum(0);
if(this.state.currentUser && this.state.currentUser.email) {
// we should also make the fineuploader component editable again
this.setState({
isFineUploaderEditable: true
isFineUploaderActive: true
});
}
},
@ -105,7 +101,8 @@ let RegisterPiece = React.createClass( {
this.state.pageSize,
this.state.searchTerm,
this.state.orderBy,
this.state.orderAsc);
this.state.orderAsc
);
this.transitionTo('piece', {pieceId: response.piece.id});
},
@ -160,11 +157,25 @@ let RegisterPiece = React.createClass( {
changeSlide() {
// only transition to the login store, if user is not logged in
// ergo the currentUser object is not properly defined
if(!this.state.currentUser.email) {
if(this.state.currentUser && !this.state.currentUser.email) {
this.refs.slidesContainer.setSlideNum(1);
}
},
// basically redirects to the second slide (index: 1), when the user is not logged in
onLoggedOut() {
this.refs.slidesContainer.setSlideNum(1);
},
onLogin() {
// once the currentUser object from UserStore is defined (eventually the user was transitioned
// to the login form via the slider and successfully logged in), we can direct him back to the
// register_piece slide
if(this.state.currentUser && this.state.currentUser.email) {
window.history.back();
}
},
render() {
return (
<SlidesContainer ref="slidesContainer">
@ -175,8 +186,9 @@ let RegisterPiece = React.createClass( {
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>
<RegisterPieceForm
{...this.props}
isFineUploaderEditable={this.state.isFineUploaderEditable}
handleSuccess={this.handleSuccess}>
isFineUploaderActive={this.state.isFineUploaderActive}
handleSuccess={this.handleSuccess}
onLoggedOut={this.onLoggedOut}>
{this.props.children}
{this.getLicenses()}
{this.getSpecifyEditions()}
@ -188,7 +200,8 @@ let RegisterPiece = React.createClass( {
<LoginContainer
message={getLangText('Please login before ascribing your work%s', '...')}
redirectOnLoggedIn={false}
redirectOnLoginSuccess={false}/>
redirectOnLoginSuccess={false}
onLogin={this.onLogin}/>
</div>
</SlidesContainer>
);

View File

@ -20,6 +20,7 @@ import ReactS3FineUploader from './ascribe_uploader/react_s3_fine_uploader';
import CollapsibleParagraph from './ascribe_collapsible/collapsible_paragraph';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import InputCheckbox from './ascribe_forms/input_checkbox';
import apiUrls from '../constants/api_urls';
import AppConstants from '../constants/application_constants';
@ -65,11 +66,18 @@ let AccountSettings = React.createClass({
handleSuccess(){
UserActions.fetchCurrentUser();
let notification = new GlobalNotificationModel(getLangText('username succesfully updated'), 'success', 5000);
let notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getFormDataProfile(){
return {'email': this.state.currentUser.email};
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
let profile = null;
if (this.state.currentUser.username) {
content = (
<Form
@ -97,6 +105,40 @@ let AccountSettings = React.createClass({
<hr />
</Form>
);
profile = (
<Form
url={apiUrls.users_profile}
handleSuccess={this.handleSuccess}
getFormData={this.getFormDataProfile}>
<Property
name="hash_locally"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox
defaultChecked={this.state.currentUser.profile.hash_locally}>
<span>
{' ' + getLangText('Enable hash option for slow connections. ' +
'Computes and uploads a hash of the work instead.')}
</span>
</InputCheckbox>
</Property>
{/*<Property
name='language'
label={getLangText('Choose your Language')}
editable={true}>
<select id="select-lang" name="language">
<option value="fr">
Fran&ccedil;ais
</option>
<option value="en"
selected="selected">
English
</option>
</select>
</Property>*/}
<hr />
</Form>
);
}
return (
<CollapsibleParagraph
@ -104,6 +146,7 @@ let AccountSettings = React.createClass({
show={true}
defaultExpanded={true}>
{content}
{profile}
{/*<Form
url={AppConstants.serverUrl + 'api/users/set_language/'}>
<Property
@ -189,7 +232,6 @@ let LoanContractSettings = React.createClass({
},
render() {
return (
<CollapsibleParagraph
title="Loan Contract Settings"
@ -276,26 +318,30 @@ let APISettings = React.createClass({
onChange(state) {
this.setState(state);
},
handleCreateSuccess: function(){
handleCreateSuccess() {
ApplicationActions.fetchApplication();
let notification = new GlobalNotificationModel(getLangText('Application successfully created'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
handleTokenRefresh: function(event){
handleTokenRefresh(event) {
let applicationName = event.target.getAttribute('data-id');
ApplicationActions.refreshApplicationToken(applicationName);
let notification = new GlobalNotificationModel(getLangText('Token refreshed'), 'success', 2000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
if (this.state.applications.length > -1) {
content = this.state.applications.map(function(app) {
content = this.state.applications.map(function(app, i) {
return (
<Property
name={app.name}
label={app.name}>
label={app.name}
key={i}>
<div className="row-same-height">
<div className="no-padding col-xs-6 col-sm-10 col-xs-height col-middle">
{'Bearer ' + app.bearer_token.token}

View File

@ -49,6 +49,7 @@ let apiUrls = {
'users_password_reset_request': AppConstants.apiEndpoint + 'users/request_reset_password/',
'users_signup': AppConstants.apiEndpoint + 'users/',
'users_username': AppConstants.apiEndpoint + 'users/username/',
'users_profile': AppConstants.apiEndpoint + 'users/profile/',
'wallet_settings': AppConstants.apiEndpoint + 'users/wallet_settings/',
'whitelabel_settings': AppConstants.apiEndpoint + 'whitelabel/settings/${subdomain}/',
'delete_s3_file': AppConstants.serverUrl + 's3/delete/'

View File

@ -22,7 +22,8 @@ let constants = {
'name': 'Creative Commons France',
'logo': 'https://s3-us-west-2.amazonaws.com/ascribe0/public/creativecommons/cc.logo.sm.png',
'permissions': ['register', 'edit', 'share', 'del_from_collection'],
'type': 'wallet'
'type': 'wallet',
'ga': 'UA-60614729-4'
},
{
'subdomain': 'cc-staging',
@ -36,7 +37,8 @@ let constants = {
'name': 'Sluice Art Fair',
'logo': 'http://sluice.info/images/logo.gif',
'permissions': ['register', 'edit', 'share', 'del_from_collection'],
'type': 'prize'
'type': 'prize',
'ga': 'UA-60614729-5'
},
{
'subdomain': 'sluice-staging',
@ -46,6 +48,10 @@ let constants = {
'type': 'prize'
}
],
'defaultDomain': {
'type': 'default',
'ga': 'UA-60614729-2'
},
// in case of whitelabel customization, we store stuff here
'whitelabel': {},

View File

@ -1,5 +1,7 @@
'use strict';
import Q from 'q';
let mapAttr = {
link: 'href',
script: 'src'
@ -25,7 +27,7 @@ let InjectInHeadMixin = {
},
injectTag(tag, src) {
let promise = new Promise((resolve, reject) => {
return Q.Promise((resolve, reject) => {
if (InjectInHeadMixin.isPresent(tag, src)) {
resolve();
} else {
@ -44,8 +46,6 @@ let InjectInHeadMixin = {
}
}
});
return promise;
},
injectStylesheet(src) {

View File

@ -13,9 +13,9 @@ class CoaStore {
onUpdateCoa(coa) {
this.coa = coa;
}
onErrorCoa(err) {
onFlushCoa() {
this.coa = {};
//this.coa = err
}
}

30
js/third_party/debug.js vendored Normal file
View File

@ -0,0 +1,30 @@
'use strict';
import alt from '../alt';
import EventActions from '../actions/event_actions';
class DebugHandler {
constructor() {
let symbols = [];
for (let k in EventActions) {
if (typeof EventActions[k] === 'symbol') {
symbols.push(EventActions[k]);
}
}
this.bindListeners({
onWhateverEvent: symbols
});
}
onWhateverEvent() {
let args = arguments[0];
let symbol = arguments[1];
console.debug(symbol, args);
}
}
export default alt.createStore(DebugHandler, 'DebugHandler');

27
js/third_party/ga.js vendored Normal file
View File

@ -0,0 +1,27 @@
'use strict';
import alt from '../alt';
import EventActions from '../actions/event_actions';
class GoogleAnalyticsHandler {
constructor() {
this.bindActions(EventActions);
}
onRouteDidChange() {
window.ga('send', 'pageview');
}
onApplicationWillBoot(settings) {
if (settings.ga) {
window.ga('create', settings.ga, 'auto');
console.log('Google Analytics loaded');
} else {
console.log('Cannot load Google Analytics: no tracking code provided');
}
}
}
export default alt.createStore(GoogleAnalyticsHandler, 'GoogleAnalyticsHandler');

34
js/third_party/intercom.js vendored Normal file
View File

@ -0,0 +1,34 @@
'use strict';
import alt from '../alt';
import EventActions from '../actions/event_actions';
class IntercomHandler {
constructor() {
this.bindActions(EventActions);
this.loaded = false;
}
onProfileDidLoad(profile) {
if (this.loaded) {
return;
}
/* eslint-disable */
Intercom('boot', {
/* eslint-enable */
app_id: 'oboxh5w1',
email: profile.email,
subdomain: window.location.host.split('.')[0],
widget: {
activator: '#IntercomDefaultWidget'
}
});
console.log('Intercom loaded');
this.loaded = true;
}
}
export default alt.createStore(IntercomHandler, 'IntercomHandler');

28
js/third_party/raven.js vendored Normal file
View File

@ -0,0 +1,28 @@
'use strict';
import alt from '../alt';
import EventActions from '../actions/event_actions';
import Raven from 'raven-js';
class RavenHandler {
constructor() {
this.bindActions(EventActions);
this.loaded = false;
}
onProfileDidLoad(profile) {
if (this.loaded) {
return;
}
Raven.setUserContext({
email: profile.email
});
console.log('Raven loaded');
this.loaded = true;
}
}
export default alt.createStore(RavenHandler, 'RavenHandler');

View File

@ -8,8 +8,9 @@ export function getSubdomainSettings(subdomain) {
if(settings.length === 1) {
return settings[0];
} else if(settings.length === 0) {
throw new Error('There are no subdomain settings for the subdomain: ' + subdomain);
return appConstants.defaultDomain;
// throw new Error('There are no subdomain settings for the subdomain: ' + subdomain);
} else {
throw new Error('Matched multiple subdomains. Adjust constants file.');
}
}
}

View File

@ -1,5 +1,7 @@
'use strict';
import Q from 'q';
import { sanitize } from './general_utils';
import AppConstants from '../constants/application_constants';
@ -86,7 +88,7 @@ export function getCookie(name) {
*/
export function fetchImageAsBlob(url) {
return new Promise((resolve, reject) => {
return Q.Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);

86
js/utils/file_utils.js Normal file
View File

@ -0,0 +1,86 @@
'use strict';
import Q from 'q';
import SparkMD5 from 'spark-md5';
import { getLangText } from './lang_utils';
/**
* Takes a string, creates a text file and returns the URL
*
* @param {string} text regular javascript string
* @return {string} regular javascript string
*/
function makeTextFile(text, file) {
let textFileBlob = new Blob([text], {type: 'text/plain'});
let textFile = new File([textFileBlob], 'hash-of-' + file.name + '.txt', {
lastModifiedDate: file.lastModifiedDate,
lastModified: file.lastModified,
type: 'text/plain'
});
return textFile;
}
/**
* Takes a file Object, computes the MD5 hash and returns the URL of the textfile with the hash
*
* @param {File} file javascript File object
* @return {string} regular javascript string
*/
export function computeHashOfFile(file) {
return Q.Promise((resolve, reject, notify) => {
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
let chunkSize = 2097152; // Read in chunks of 2MB
let chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
let spark = new SparkMD5.ArrayBuffer();
let fileReader = new FileReader();
let startTime = new Date();
// comment: We should convert this to es6 at some point, however if so please consider that
// an arrow function will get rid of the function's scope...
fileReader.onload = function(e) {
//console.log('read chunk nr', currentChunk + 1, 'of', chunks);
spark.append(e.target.result); // Append array buffer
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
let fileHash = spark.end();
console.info('computed hash %s (took %d s)',
fileHash,
Math.round(((new Date() - startTime) / 1000) % 60)); // Compute hash
let blobTextFile = makeTextFile(fileHash, file);
resolve(blobTextFile);
}
}.bind(this);
fileReader.onerror = function () {
reject(new Error(getLangText('We weren\'t able to hash your file locally. Try to upload it manually or consider contact us.')));
};
function loadNext() {
var start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
// send progress
// Due to the fact that progressHandler and notify are going to be removed in v2
// of Q, the functionality of throwing errors in the progressHandler will not be implemented
// anymore. To still be able to throw an error however, we can just expose the promise's reject
// method to the .progress function to stop the execution immediately.
notify({
progress: start / file.size,
reject
});
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
}

View File

@ -1,5 +1,7 @@
'use strict';
import Q from 'q';
import { argsToQueryParams, getCookie } from '../utils/fetch_api_utils';
import AppConstants from '../constants/application_constants';
@ -22,7 +24,7 @@ class Requests {
throw new Error(response.status + ' - ' + response.statusText + ' - on URL:' + response.url);
}
return new Promise((resolve, reject) => {
return Q.Promise((resolve, reject) => {
response.text()
.then((responseText) => {
// If the responses' body does not contain any data,

View File

@ -49,7 +49,6 @@
"classnames": "^1.2.2",
"compression": "^1.4.4",
"envify": "^3.4.0",
"es6-promise": "^2.1.1",
"eslint": "^0.22.1",
"eslint-plugin-react": "^2.5.0",
"express": "^4.12.4",
@ -69,6 +68,7 @@
"jest-cli": "^0.4.0",
"lodash": "^3.9.3",
"object-assign": "^2.0.0",
"q": "^1.4.1",
"raven-js": "^1.1.19",
"react": "^0.13.2",
"react-bootstrap": "~0.22.6",
@ -79,6 +79,7 @@
"react-textarea-autosize": "^2.2.3",
"reactify": "^1.1.0",
"shmui": "^0.1.0",
"spark-md5": "~1.0.0",
"uglifyjs": "^2.4.10",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",

View File

@ -1,6 +1,11 @@
$ascribe-accordion-list-item-height: 8em;
$ascribe-accordion-list-font: 'Source Sans Pro';
.ascribe-accordion-list {
padding-left: 15px;
padding-right: 15px;
}
.ascribe-accordion-list-item {
background-color: white;
border: 1px solid black;

View File

@ -1,13 +1,12 @@
.ascribe-media-player {
margin-bottom: 1em;
video {
width: 100%;
height: 100%;
}
video,
img {
width: 100%;
width: auto;
height: 100%;
display: block;
margin: 0 auto;
}
.media-other {

View File

@ -2,16 +2,17 @@
overflow-x: hidden;
overflow-y: hidden;
padding-left: 0;
padding-right: 0;
}
.ascribe-sliding-container {
transition: transform 1s cubic-bezier(0.23, 1, 0.32, 1);
padding-left: 0;
padding-right: 0;
}
.ascribe-slide {
position: relative;
min-height: 1px;
padding-left: 15px;
padding-right: 15px;
float:left;
}

View File

@ -10,7 +10,7 @@
cursor: default !important;
padding: 1.5em 1.5em 1.5em 0;
padding: 1.5em 0 1.5em 0;
}
.inactive-dropzone {
@ -19,6 +19,16 @@
outline: 0;
}
.present-options {
> p {
margin-bottom: .75em !important;
}
.btn {
margin: 0 1em 0 1em;
}
}
.file-drag-and-drop .file-drag-and-drop-dialog > p:first-child {
font-size: 1.5em !important;

View File

@ -41,8 +41,8 @@ html {
}
.ascribe-default-app {
max-width: 90%;
padding-top: 70px;
overflow-x: hidden;
}
hr {