1
0
mirror of https://github.com/ascribe/onion.git synced 2024-11-15 01:25:17 +01:00

Merge branch 'AD-435-hash-locally-for-when-a-artist-do'

Conflicts:
	js/components/ascribe_forms/form_register_piece.js
	js/components/ascribe_slides_container/slides_container.js
	js/components/ascribe_uploader/file_drag_and_drop.js
	js/components/ascribe_uploader/react_s3_fine_uploader.js
This commit is contained in:
Tim Daubenschütz 2015-07-29 09:35:14 +02:00
commit 76cf9fd74d
28 changed files with 683 additions and 385 deletions

View File

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

View File

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

View File

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

View File

@ -91,7 +91,7 @@ let Form = React.createClass({
} }
} }
else { else {
console.logGlobal(err); console.logGlobal(err, false, this.getFormData());
this.setState({errors: [getLangText('Something went wrong, please try again later')]}); this.setState({errors: [getLangText('Something went wrong, please try again later')]});
} }
this.setState({submitted: false}); this.setState({submitted: false});

View File

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

View File

@ -2,18 +2,21 @@
import React from 'react'; 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 Form from './form';
import Property from './property'; import Property from './property';
import FormPropertyHeader from './form_property_header'; import FormPropertyHeader from './form_property_header';
import apiUrls from '../../constants/api_urls';
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader'; 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 { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
let RegisterPieceForm = React.createClass({ let RegisterPieceForm = React.createClass({
@ -34,10 +37,26 @@ let RegisterPieceForm = React.createClass({
}, },
getInitialState(){ getInitialState(){
return { return mergeOptions(
{
digitalWorkKey: null, digitalWorkKey: null,
isUploadReady: false isUploadReady: false
}; },
UserStore.getState()
);
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
}, },
getFormData(){ getFormData(){
@ -68,6 +87,9 @@ let RegisterPieceForm = React.createClass({
}, },
render() { render() {
let currentUser = this.state.currentUser;
let enableLocalHashing = currentUser && currentUser.profile ? currentUser.profile.hash_locally : false;
return ( return (
<Form <Form
className="ascribe-form-bordered" className="ascribe-form-bordered"
@ -96,7 +118,9 @@ let RegisterPieceForm = React.createClass({
setIsUploadReady={this.setIsUploadReady} setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={this.isReadyForFormSubmission} isReadyForFormSubmission={this.isReadyForFormSubmission}
isFineUploaderActive={this.props.isFineUploaderActive} isFineUploaderActive={this.props.isFineUploaderActive}
onLoggedOut={this.props.onLoggedOut}/> onLoggedOut={this.props.onLoggedOut}
editable={this.props.isFineUploaderEditable}
enableLocalHashing={enableLocalHashing}/>
</Property> </Property>
<Property <Property
name='artist_name' name='artist_name'
@ -135,11 +159,14 @@ let FileUploader = React.createClass({
submitKey: React.PropTypes.func, submitKey: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func, isReadyForFormSubmission: React.PropTypes.func,
onClick: React.PropTypes.func, onClick: React.PropTypes.func,
// isFineUploaderActive is used to lock react fine uploader in case // isFineUploaderActive is used to lock react fine uploader in case
// a user is actually not logged in already to prevent him from droping files // a user is actually not logged in already to prevent him from droping files
// before login in // before login in
isFineUploaderActive: React.PropTypes.bool, isFineUploaderActive: React.PropTypes.bool,
onLoggedOut: React.PropTypes.func onLoggedOut: React.PropTypes.func,
editable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool
}, },
render() { render() {
@ -176,7 +203,8 @@ let FileUploader = React.createClass({
'X-CSRFToken': getCookie(AppConstants.csrftoken) 'X-CSRFToken': getCookie(AppConstants.csrftoken)
} }
}} }}
onInactive={this.props.onLoggedOut}/> 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') + '.'); 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() { render() {
let tooltipPassword = getLangText('Your password must be at least 10 characters') + '.\n ' + 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 ' + getLangText('This password is securing your digital property like a bank account') + '.\n ' +
@ -76,7 +74,6 @@ let SignupForm = React.createClass({
ref='form' ref='form'
url={apiUrls.users_signup} url={apiUrls.users_signup}
handleSuccess={this.handleSuccess} handleSuccess={this.handleSuccess}
getFormData={this.getFormData}
buttons={ buttons={
<button type="submit" className="btn ascribe-btn ascribe-btn-login"> <button type="submit" className="btn ascribe-btn ascribe-btn-login">
{this.props.submitMessage} {this.props.submitMessage}

View File

@ -2,35 +2,73 @@
import React from 'react'; 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({ let InputCheckbox = React.createClass({
propTypes: { 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([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
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 { return {
show: false defaultChecked: false
}; };
}, },
handleFocus(event) { // Setting value to null in initialState is essentially since we're deriving a certain state from
this.refs.checkbox.getDOMNode().checked = !this.refs.checkbox.getDOMNode().checked; // value === null as can be seen in componentWillReceiveProps.
getInitialState() {
return {
value: null
};
},
// This is calling property.js's method handleChange which componentWillReceiveProps(nextProps) {
// 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();
this.setState({ // Developer's are used to define defaultValues for inputs via defaultValue, but since this is a
show: this.refs.checkbox.getDOMNode().checked, // input of type checkbox we warn the dev to not do that.
value: this.refs.checkbox.getDOMNode().checked 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
}
}); });
}, },
@ -38,9 +76,13 @@ let InputCheckbox = React.createClass({
render() { render() {
return ( return (
<span <span
onClick={this.handleFocus} onClick={this.onChange}>
onFocus={this.handleFocus}> <input
<input type="checkbox" ref="checkbox" required="required"/> type="checkbox"
ref="checkbox"
onChange={this.onChange}
checked={this.state.value}
defaultChecked={this.props.defaultChecked}/>
<span className="checkbox"> <span className="checkbox">
{this.props.children} {this.props.children}
</span> </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() { getInitialState() {
return { 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, initialValue: null,
value: null, value: null,
isFocused: false, isFocused: false,
@ -47,19 +50,29 @@ let Property = React.createClass({
}, },
componentWillReceiveProps() { componentWillReceiveProps() {
let childInput = this.refs.input;
// In order to set this.state.value from another component // In order to set this.state.value from another component
// the state of value should only be set if its not undefined and // the state of value should only be set if its not undefined and
// actually references something // actually references something
if(typeof this.refs.input.getDOMNode().value !== 'undefined') { if(typeof childInput.getDOMNode().value !== 'undefined') {
this.setState({ 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) { if(!this.state.initialValue) {
this.setState({ this.setState({
initialValue: this.refs.input.getDOMNode().defaultValue initialValue: childInput.defaultValue
}); });
} }
}, },
@ -158,7 +171,6 @@ let Property = React.createClass({
renderChildren() { renderChildren() {
return ReactAddons.Children.map(this.props.children, (child) => { return ReactAddons.Children.map(this.props.children, (child) => {
return ReactAddons.addons.cloneWithProps(child, { return ReactAddons.addons.cloneWithProps(child, {
value: this.state.value,
onChange: this.handleChange, onChange: this.handleChange,
onFocus: this.handleFocus, onFocus: this.handleFocus,
onBlur: this.handleBlur, onBlur: this.handleBlur,

View File

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

View File

@ -32,6 +32,17 @@ let SlidesContainer = React.createClass({
}, },
componentDidMount() { componentDidMount() {
// check if slide_num was defined, and if not then default to 0
let queryParams = this.getQuery();
if(!('slide_num' in queryParams)) {
// 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 // init container width
this.handleContainerResize(); this.handleContainerResize();
@ -60,6 +71,9 @@ let SlidesContainer = React.createClass({
// though only if the slideNum is actually in the range of our children-list. // though only if the slideNum is actually in the range of our children-list.
setSlideNum(slideNum) { setSlideNum(slideNum) {
// we do not want to overwrite other queryParams
let queryParams = this.getQuery();
// slideNum can in some instances be not a number, // slideNum can in some instances be not a number,
// therefore we have to parse it to one and make sure that its not NaN // therefore we have to parse it to one and make sure that its not NaN
slideNum = parseInt(slideNum, 10); slideNum = parseInt(slideNum, 10);
@ -70,7 +84,9 @@ let SlidesContainer = React.createClass({
if(isNaN(slideNum) && this.state.slideNum === -1) { if(isNaN(slideNum) && this.state.slideNum === -1) {
slideNum = 0; slideNum = 0;
this.replaceWith(this.getPathname(), null, {slide_num: slideNum}); queryParams.slide_num = slideNum;
this.replaceWith(this.getPathname(), null, queryParams);
this.setState({slideNum: slideNum}); this.setState({slideNum: slideNum});
return; return;
@ -92,7 +108,10 @@ let SlidesContainer = React.createClass({
// Otherwise, we're able to use the browsers history.forward() method // Otherwise, we're able to use the browsers history.forward() method
// to keep the stack clean // to keep the stack clean
if(this.state.historyLength === window.history.length) { if(this.state.historyLength === window.history.length) {
this.transitionTo(this.getPathname(), null, {slide_num: slideNum});
queryParams.slide_num = slideNum;
this.transitionTo(this.getPathname(), null, queryParams);
} else { } else {
window.history.forward(); window.history.forward();
} }

View File

@ -1,10 +1,12 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import ProgressBar from 'react-progressbar';
import FileDragAndDropDialog from './file_drag_and_drop_dialog'; import FileDragAndDropDialog from './file_drag_and_drop_dialog';
import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator'; 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 // Taken from: https://github.com/fedosejev/react-file-drag-and-drop
let FileDragAndDrop = React.createClass({ let FileDragAndDrop = React.createClass({
@ -27,7 +29,15 @@ let FileDragAndDrop = React.createClass({
multiple: React.PropTypes.bool, multiple: React.PropTypes.bool,
dropzoneInactive: React.PropTypes.bool, dropzoneInactive: React.PropTypes.bool,
areAssetsDownloadable: 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) { handleDragStart(event) {
@ -155,6 +165,19 @@ let FileDragAndDrop = React.createClass({
className += this.props.dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone'; className += this.props.dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone';
className += this.props.className ? ' ' + this.props.className : ''; className += this.props.className ? ' ' + this.props.className : '';
// 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 ( return (
<div <div
className={className} className={className}
@ -168,7 +191,8 @@ let FileDragAndDrop = React.createClass({
<FileDragAndDropDialog <FileDragAndDropDialog
multipleFiles={this.props.multiple} multipleFiles={this.props.multiple}
hasFiles={hasFiles} hasFiles={hasFiles}
onClick={this.handleOnClick}/> onClick={this.handleOnClick}
enableLocalHashing={this.props.enableLocalHashing}/>
<FileDragAndDropPreviewIterator <FileDragAndDropPreviewIterator
files={this.props.filesToUpload} files={this.props.filesToUpload}
handleDeleteFile={this.handleDeleteFile} handleDeleteFile={this.handleDeleteFile}
@ -190,6 +214,7 @@ let FileDragAndDrop = React.createClass({
</div> </div>
); );
} }
}
}); });
export default FileDragAndDrop; export default FileDragAndDrop;

View File

@ -1,37 +1,83 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import Router from 'react-router';
import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
let FileDragAndDropDialog = React.createClass({ let FileDragAndDropDialog = React.createClass({
propTypes: { propTypes: {
hasFiles: React.PropTypes.bool, hasFiles: React.PropTypes.bool,
multipleFiles: React.PropTypes.bool, multipleFiles: React.PropTypes.bool,
onClick: React.PropTypes.func onClick: React.PropTypes.func,
enableLocalHashing: React.PropTypes.bool
}, },
mixins: [Router.State],
render() { render() {
const queryParams = this.getQuery();
if(this.props.hasFiles) { if(this.props.hasFiles) {
return null; return null;
} else { } 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 ( return (
<span className="file-drag-and-drop-dialog">Click or drag to add files</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 { } else {
if(this.props.multipleFiles) {
return ( return (
<span className="file-drag-and-drop-dialog"> <span className="file-drag-and-drop-dialog">
<p>Drag a file here</p> {getLangText('Click or drag to add files')}
<p>or</p> </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 <span
className="btn btn-default" className="btn btn-default"
onClick={this.props.onClick}> onClick={this.props.onClick}>
choose a file to upload {dialog}
</span> </span>
</span> </span>
); );
} }
} }
} }
}
}); });
export default FileDragAndDropDialog; export default FileDragAndDropDialog;

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
'use strict'; 'use strict';
import React from 'react/addons'; import React from 'react/addons';
import Router from 'react-router';
import Raven from 'raven-js'; import Raven from 'raven-js';
import Q from 'q';
import { getCookie } from '../../utils/fetch_api_utils'; import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
@ -16,6 +18,8 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { computeHashOfFile } from '../../utils/file_utils';
var ReactS3FineUploader = React.createClass({ var ReactS3FineUploader = React.createClass({
propTypes: { propTypes: {
@ -96,9 +100,22 @@ var ReactS3FineUploader = React.createClass({
areAssetsDownloadable: React.PropTypes.bool, areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool, areAssetsEditable: React.PropTypes.bool,
defaultErrorMessage: React.PropTypes.string, defaultErrorMessage: React.PropTypes.string,
onInactive: React.PropTypes.func 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() { getDefaultProps() {
return { return {
autoUpload: true, autoUpload: true,
@ -136,7 +153,7 @@ var ReactS3FineUploader = React.createClass({
endpoint: null endpoint: null
}, },
messages: { 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 formatFileName: function(name){// fix maybe
if (name !== undefined && name.length > 26) { if (name !== undefined && name.length > 26) {
@ -145,7 +162,7 @@ var ReactS3FineUploader = React.createClass({
return name; return name;
}, },
multiple: false, multiple: false,
defaultErrorMessage: 'Unexpected error. Please contact us if this happens repeatedly.' defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.')
}; };
}, },
@ -153,14 +170,17 @@ var ReactS3FineUploader = React.createClass({
return { return {
filesToUpload: [], filesToUpload: [],
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()), uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()),
csrfToken: getCookie(AppConstants.csrftoken) csrfToken: getCookie(AppConstants.csrftoken),
hashingProgress: -2
// -1: aborted
// -2: uninitialized
}; };
}, },
componentWillUpdate() {
// since the csrf header is defined in this component's props, // since the csrf header is defined in this component's props,
// everytime the csrf cookie is changed we'll need to reinitalize // everytime the csrf cookie is changed we'll need to reinitalize
// fineuploader and update the actual csrf token // fineuploader and update the actual csrf token
componentWillUpdate() {
let potentiallyNewCSRFToken = getCookie(AppConstants.csrftoken); let potentiallyNewCSRFToken = getCookie(AppConstants.csrftoken);
if(this.state.csrfToken !== potentiallyNewCSRFToken) { if(this.state.csrfToken !== potentiallyNewCSRFToken) {
this.setState({ this.setState({
@ -211,10 +231,10 @@ var ReactS3FineUploader = React.createClass({
}, },
requestKey(fileId) { requestKey(fileId) {
let defer = new fineUploader.Promise();
let filename = this.state.uploader.getName(fileId); let filename = this.state.uploader.getName(fileId);
let uuid = this.state.uploader.getUuid(fileId); let uuid = this.state.uploader.getUuid(fileId);
return Q.Promise((resolve, reject) => {
window.fetch(this.props.keyRoutine.url, { window.fetch(this.props.keyRoutine.url, {
method: 'post', method: 'post',
headers: { headers: {
@ -234,18 +254,16 @@ var ReactS3FineUploader = React.createClass({
return res.json(); return res.json();
}) })
.then((res) =>{ .then((res) =>{
defer.success(res.key); resolve(res.key);
}) })
.catch((err) => { .catch((err) => {
defer.failure(err); reject(err);
});
}); });
return defer;
}, },
createBlob(file) { createBlob(file) {
let defer = new fineUploader.Promise(); return Q.Promise((resolve, reject) => {
window.fetch(this.props.createBlobRoutine.url, { window.fetch(this.props.createBlobRoutine.url, {
method: 'post', method: 'post',
headers: { headers: {
@ -271,30 +289,36 @@ var ReactS3FineUploader = React.createClass({
file.s3Url = res.digitalwork.url_safe; file.s3Url = res.digitalwork.url_safe;
file.s3UrlSafe = res.digitalwork.url_safe; file.s3UrlSafe = res.digitalwork.url_safe;
} else { } else {
throw new Error('Could not find a url to download.'); throw new Error(getLangText('Could not find a url to download.'));
} }
defer.success(res.key); resolve(res);
}) })
.catch((err) => { .catch((err) => {
defer.failure(err); reject(err);
console.logGlobal(err); console.logGlobal(err);
}); });
return defer; });
}, },
/* FineUploader specific callback function handlers */ /* FineUploader specific callback function handlers */
onComplete(id) { onComplete(id) {
let files = this.state.filesToUpload; 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].status = 'upload successful';
files[id].key = this.state.uploader.getKey(id); files[id].key = this.state.uploader.getKey(id);
let newState = React.addons.update(this.state, { let newState = React.addons.update(this.state, {
filesToUpload: { $set: files } filesToUpload: { $set: files }
}); });
this.setState(newState);
this.createBlob(files[id]);
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 // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey
// are optional, we'll only trigger them when they're actually defined // are optional, we'll only trigger them when they're actually defined
if(this.props.submitKey) { if(this.props.submitKey) {
@ -316,10 +340,18 @@ var ReactS3FineUploader = React.createClass({
} else { } else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader'); 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);
});
}, },
onError() { onError(id, name, errorReason) {
Raven.captureException('react-fineuploader-error'); Raven.captureException(errorReason);
let notification = new GlobalNotificationModel(this.props.defaultErrorMessage, 'danger', 5000); let notification = new GlobalNotificationModel(this.props.defaultErrorMessage, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
@ -327,7 +359,9 @@ var ReactS3FineUploader = React.createClass({
onValidate(data) { onValidate(data) {
if(data.size > this.props.validation.sizeLimit) { if(data.size > this.props.validation.sizeLimit) {
this.state.uploader.cancelAll(); 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); GlobalNotificationActions.appendGlobalNotification(notification);
} }
}, },
@ -335,7 +369,7 @@ var ReactS3FineUploader = React.createClass({
onCancel(id) { onCancel(id) {
this.removeFileWithIdFromFilesToUpload(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); GlobalNotificationActions.appendGlobalNotification(notification);
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey
@ -452,7 +486,7 @@ var ReactS3FineUploader = React.createClass({
if(this.state.uploader.pauseUpload(fileId)) { if(this.state.uploader.pauseUpload(fileId)) {
this.setStatusOfFile(fileId, 'paused'); this.setStatusOfFile(fileId, 'paused');
} else { } else {
throw new Error('File upload could not be paused.'); throw new Error(getLangText('File upload could not be paused.'));
} }
}, },
@ -461,7 +495,7 @@ var ReactS3FineUploader = React.createClass({
if(this.state.uploader.continueUpload(fileId)) { if(this.state.uploader.continueUpload(fileId)) {
this.setStatusOfFile(fileId, 'uploading'); this.setStatusOfFile(fileId, 'uploading');
} else { } else {
throw new Error('File upload could not be resumed.'); throw new Error(getLangText('File upload could not be resumed.'));
} }
}, },
@ -485,7 +519,126 @@ var ReactS3FineUploader = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
} }
// 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.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 oldFiles = this.state.filesToUpload;
let oldAndNewFiles = this.state.uploader.getUploads(); let oldAndNewFiles = this.state.uploader.getUploads();
// Add fineuploader specific information to new files // Add fineuploader specific information to new files
@ -546,6 +699,17 @@ var ReactS3FineUploader = React.createClass({
this.setState(newState); 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() { render() {
return ( return (
@ -558,11 +722,14 @@ var ReactS3FineUploader = React.createClass({
handleCancelFile={this.handleCancelFile} handleCancelFile={this.handleCancelFile}
handlePauseFile={this.handlePauseFile} handlePauseFile={this.handlePauseFile}
handleResumeFile={this.handleResumeFile} handleResumeFile={this.handleResumeFile}
handleCancelHashing={this.handleCancelHashing}
multiple={this.props.multiple} multiple={this.props.multiple}
areAssetsDownloadable={this.props.areAssetsDownloadable} areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={this.props.areAssetsEditable} 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}
onInactive={this.props.onInactive}/> dropzoneInactive={this.isDropzoneInactive()}
hashingProgress={this.state.hashingProgress}
enableLocalHashing={this.props.enableLocalHashing} />
</div> </div>
); );
} }

View File

@ -20,6 +20,7 @@ import ReactS3FineUploader from './ascribe_uploader/react_s3_fine_uploader';
import CollapsibleParagraph from './ascribe_collapsible/collapsible_paragraph'; import CollapsibleParagraph from './ascribe_collapsible/collapsible_paragraph';
import Form from './ascribe_forms/form'; import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property'; import Property from './ascribe_forms/property';
import InputCheckbox from './ascribe_forms/input_checkbox';
import apiUrls from '../constants/api_urls'; import apiUrls from '../constants/api_urls';
import AppConstants from '../constants/application_constants'; import AppConstants from '../constants/application_constants';
@ -65,11 +66,18 @@ let AccountSettings = React.createClass({
handleSuccess(){ handleSuccess(){
UserActions.fetchCurrentUser(); 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); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
getFormDataProfile(){
return {'email': this.state.currentUser.email};
},
render() { render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />; let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
let profile = null;
if (this.state.currentUser.username) { if (this.state.currentUser.username) {
content = ( content = (
<Form <Form
@ -97,6 +105,40 @@ let AccountSettings = React.createClass({
<hr /> <hr />
</Form> </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 ( return (
<CollapsibleParagraph <CollapsibleParagraph
@ -104,6 +146,7 @@ let AccountSettings = React.createClass({
show={true} show={true}
defaultExpanded={true}> defaultExpanded={true}>
{content} {content}
{profile}
{/*<Form {/*<Form
url={AppConstants.serverUrl + 'api/users/set_language/'}> url={AppConstants.serverUrl + 'api/users/set_language/'}>
<Property <Property
@ -189,7 +232,6 @@ let LoanContractSettings = React.createClass({
}, },
render() { render() {
return ( return (
<CollapsibleParagraph <CollapsibleParagraph
title="Loan Contract Settings" title="Loan Contract Settings"
@ -276,26 +318,30 @@ let APISettings = React.createClass({
onChange(state) { onChange(state) {
this.setState(state); this.setState(state);
}, },
handleCreateSuccess: function(){
handleCreateSuccess() {
ApplicationActions.fetchApplication(); ApplicationActions.fetchApplication();
let notification = new GlobalNotificationModel(getLangText('Application successfully created'), 'success', 5000); let notification = new GlobalNotificationModel(getLangText('Application successfully created'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
handleTokenRefresh: function(event){ handleTokenRefresh(event) {
let applicationName = event.target.getAttribute('data-id'); let applicationName = event.target.getAttribute('data-id');
ApplicationActions.refreshApplicationToken(applicationName); ApplicationActions.refreshApplicationToken(applicationName);
let notification = new GlobalNotificationModel(getLangText('Token refreshed'), 'success', 2000); let notification = new GlobalNotificationModel(getLangText('Token refreshed'), 'success', 2000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
render() { render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />; let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
if (this.state.applications.length > -1) { if (this.state.applications.length > -1) {
content = this.state.applications.map(function(app) { content = this.state.applications.map(function(app, i) {
return ( return (
<Property <Property
name={app.name} name={app.name}
label={app.name}> label={app.name}
key={i}>
<div className="row-same-height"> <div className="row-same-height">
<div className="no-padding col-xs-6 col-sm-10 col-xs-height col-middle"> <div className="no-padding col-xs-6 col-sm-10 col-xs-height col-middle">
{'Bearer ' + app.bearer_token.token} {'Bearer ' + app.bearer_token.token}

View File

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

View File

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

View File

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

View File

@ -1,16 +1,25 @@
'use strict'; 'use strict';
import Q from 'q';
import SparkMD5 from 'spark-md5'; import SparkMD5 from 'spark-md5';
import { getLangText } from './lang_utils';
/** /**
* Takes a string, creates a text file and returns the URL * Takes a string, creates a text file and returns the URL
* *
* @param {string} text regular javascript string * @param {string} text regular javascript string
* @return {string} regular javascript string * @return {string} regular javascript string
*/ */
export function makeTextFile(text) { function makeTextFile(text, file) {
let data = new Blob([text], {type: 'text/plain'}); let textFileBlob = new Blob([text], {type: 'text/plain'});
return window.URL.createObjectURL(data); let textFile = new File([textFileBlob], 'hash-of-' + file.name + '.txt', {
lastModifiedDate: file.lastModifiedDate,
lastModified: file.lastModified,
type: 'text/plain'
});
return textFile;
} }
/** /**
@ -20,14 +29,18 @@ export function makeTextFile(text) {
* @return {string} regular javascript string * @return {string} regular javascript string
*/ */
export function computeHashOfFile(file) { export function computeHashOfFile(file) {
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice, return Q.Promise((resolve, reject, notify) => {
chunkSize = 2097152, // Read in chunks of 2MB let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
chunks = Math.ceil(file.size / chunkSize), let chunkSize = 2097152; // Read in chunks of 2MB
currentChunk = 0, let chunks = Math.ceil(file.size / chunkSize);
spark = new SparkMD5.ArrayBuffer(), let currentChunk = 0;
fileReader = new FileReader(); let spark = new SparkMD5.ArrayBuffer();
let fileReader = new FileReader();
let startTime = new Date(); 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) { fileReader.onload = function(e) {
//console.log('read chunk nr', currentChunk + 1, 'of', chunks); //console.log('read chunk nr', currentChunk + 1, 'of', chunks);
spark.append(e.target.result); // Append array buffer spark.append(e.target.result); // Append array buffer
@ -37,25 +50,37 @@ export function computeHashOfFile(file) {
loadNext(); loadNext();
} else { } else {
let fileHash = spark.end(); let fileHash = spark.end();
console.info('computed hash %s (took %d s)', console.info('computed hash %s (took %d s)',
fileHash, fileHash,
Math.round(((new Date() - startTime) / 1000) % 60)); // Compute hash Math.round(((new Date() - startTime) / 1000) % 60)); // Compute hash
let hashFile = this.makeTextFile(fileHash);
console.info('hash: ', hashFile); let blobTextFile = makeTextFile(fileHash, file);
return hashFile; resolve(blobTextFile);
} }
}.bind(this); }.bind(this);
fileReader.onerror = function () { fileReader.onerror = function () {
console.warn('oops, something went wrong.'); reject(new Error(getLangText('We weren\'t able to hash your file locally. Try to upload it manually or consider contact us.')));
}; };
function loadNext() { function loadNext() {
var start = currentChunk * chunkSize, var start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + 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)); fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
} }
loadNext(); loadNext();
});
} }

View File

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

View File

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

View File

@ -10,7 +10,7 @@
cursor: default !important; cursor: default !important;
padding: 1.5em 1.5em 1.5em 0; padding: 1.5em 0 1.5em 0;
} }
.inactive-dropzone { .inactive-dropzone {
@ -19,6 +19,16 @@
outline: 0; 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 { .file-drag-and-drop .file-drag-and-drop-dialog > p:first-child {
font-size: 1.5em !important; font-size: 1.5em !important;