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

Merge branch 'AD-496-add-control-buttons-to-fineupload'

This commit is contained in:
Tim Daubenschütz 2015-07-01 13:09:31 +02:00
commit 16c8df2f8d
57 changed files with 1617 additions and 358 deletions

View File

@ -2,7 +2,7 @@
*This should be a living document. So if you have any ideas for refactoring stuff, then feel free to add them to this document*
- Get rid of all Mixins.
- Get rid of all Mixins. (making good progress there :))
- Make all standalone components independent from things like global utilities (GeneralUtils is maybe used in table for example)
- Check if all polyfills are appropriately initialized and available: Compare to this
- Extract all standalone components to their own folder structure and write application independent tests (+ figure out how to do that in a productive way) (fetch lib especially)
@ -11,3 +11,8 @@
queryParams of the piece_list_store should all be reflected in the url and not a single component each should manipulate the URL bar (refactor pagination, use actions and state)
- Refactor string-templating for api_urls
- Use classNames plugin instead of if-conditional-classes
## React-S3-Fineuploader
- implementation should enable to define all important methods outside
- and: maybe create a utility class for all methods to avoid code duplication
- filesToUpload CRUD methods are dirty

34
js/actions/coa_actions.js Normal file
View File

@ -0,0 +1,34 @@
'use strict';
import alt from '../alt';
import CoaFetcher from '../fetchers/coa_fetcher';
class CoaActions {
constructor() {
this.generateActions(
'updateCoa'
);
}
fetchOne(id) {
CoaFetcher.fetchOne(id)
.then((res) => {
this.actions.updateCoa(res.coa);
})
.catch((err) => {
console.log(err);
});
}
create(edition) {
CoaFetcher.create(edition.bitcoin_id)
.then((res) => {
this.actions.updateCoa(res.coa);
})
.catch((err) => {
console.log(err);
});
}
}
export default alt.createActions(CoaActions);

View File

@ -0,0 +1,25 @@
'use strict';
import alt from '../alt';
import LicenseFetcher from '../fetchers/license_fetcher';
class LicenseActions {
constructor() {
this.generateActions(
'updateLicenses'
);
}
fetchLicense() {
LicenseFetcher.fetch()
.then((res) => {
this.actions.updateLicenses(res.licenses);
})
.catch((err) => {
console.log(err);
});
}
}
export default alt.createActions(LicenseActions);

View File

@ -0,0 +1,25 @@
'use strict';
import alt from '../alt';
import WhitelabelFetcher from '../fetchers/whitelabel_fetcher';
class WhitelabelActions {
constructor() {
this.generateActions(
'updateWhitelabel'
);
}
fetchWhitelabel() {
WhitelabelFetcher.fetch()
.then((res) => {
this.actions.updateWhitelabel(res.whitelabel);
})
.catch((err) => {
console.log(err);
});
}
}
export default alt.createActions(WhitelabelActions);

View File

@ -16,6 +16,13 @@ let AccordionList = React.createClass({
{this.props.children}
</div>
);
} else if(this.props.itemList.length === 0) {
return (
<div>
<p className="text-center">You don't have any works yet...</p>
<p className="text-center">To register one, click <a href="register_piece">here</a>!</p>
</div>
);
} else {
return (
<div className={this.props.className + ' ascribe-accordion-list-loading'}>

View File

@ -16,10 +16,10 @@ let AccordionListItem = React.createClass({
<div className="row">
<div className={this.props.className}>
<div className="wrapper">
<div className="col-xs-4 col-sm-4 col-md-4 col-lg-4 thumbnail-wrapper">
<div className="col-xs-5 col-sm-5 col-md-4 col-lg-4 thumbnail-wrapper">
<img src={this.props.content.thumbnail} />
</div>
<div className="col-xs-8 col-sm-8 col-md-8 col-lg-8">
<div className="col-xs-7 col-sm-7 col-md-7 col-lg-7 col-md-offset-1 col-lg-offset-1">
<h1>{this.props.content.title}</h1>
<h3>{getLangText('by %s', this.props.content.artist_name)}</h3>
<h3>{this.props.content.date_created.split('-')[0]}</h3>

View File

@ -24,6 +24,7 @@ let AccordionListItemTable = React.createClass({
return (
<div className={this.props.className}>
<Table
responsive
className="ascribe-table"
columnList={this.props.columnList}
itemList={this.props.itemList}

View File

@ -4,7 +4,6 @@ import React from 'react';
import EditionListStore from '../../stores/edition_list_store';
import EditionListActions from '../../actions/edition_list_actions';
import PieceListActions from '../../actions/piece_list_actions';
import AccordionListItemTable from './accordion_list_item_table';
import AccordionListItemTableToggle from './accordion_list_item_table_toggle';
@ -122,7 +121,7 @@ let AccordionListItemTableEditions = React.createClass({
'Edition',
TableItemText,
1,
true,
false,
transition
),
new ColumnModel(
@ -131,11 +130,12 @@ let AccordionListItemTableEditions = React.createClass({
'content': item.bitcoin_id
}; },
'bitcoin_id',
getLangText('Bitcoin Address'),
getLangText('ID'),
TableItemText,
5,
true,
transition
false,
transition,
'hidden-xs visible-sm visible-md visible-lg'
),
new ColumnModel(
(item) => {

View File

@ -19,7 +19,8 @@ let AclButton = React.createClass({
availableAcls: React.PropTypes.array.isRequired,
editions: React.PropTypes.array.isRequired,
currentUser: React.PropTypes.object,
handleSuccess: React.PropTypes.func.isRequired
handleSuccess: React.PropTypes.func.isRequired,
className: React.PropTypes.string
},
actionProperties(){
@ -74,9 +75,9 @@ let AclButton = React.createClass({
return (
<ModalWrapper
button={
<div className={shouldDisplay ? 'btn btn-default btn-sm' : 'hidden'}>
<button className={shouldDisplay ? 'btn btn-default btn-sm ' : 'hidden'}>
{this.props.action.toUpperCase()}
</div>
</button>
}
handleSuccess={ aclProps.handleSuccess }
title={ aclProps.title }

View File

@ -41,7 +41,7 @@ let AclButtonList = React.createClass({
action="transfer"
editions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} />
handleSuccess={this.props.handleSuccess}/>
<AclButton
availableAcls={this.props.availableAcls}
action="consign"

View File

@ -130,15 +130,17 @@ let Form = React.createClass({
},
renderChildren() {
return ReactAddons.Children.map(this.props.children, (child) => {
return ReactAddons.addons.cloneWithProps(child, {
handleChange: this.handleChangeChild,
ref: child.props.name
});
if (child) {
return ReactAddons.addons.cloneWithProps(child, {
handleChange: this.handleChangeChild,
ref: child.props.name
});
}
});
},
render() {
return (
<form
<form
role="form"
className="ascribe-form"
onSubmit={this.submit}

View File

@ -16,13 +16,14 @@ let Property = React.createClass({
React.PropTypes.string,
React.PropTypes.element
]),
footer: React.PropTypes.element,
handleChange: React.PropTypes.func
},
getDefaultProps() {
return {
editable: true,
hidden: false
hidden: false,
};
},
@ -55,6 +56,9 @@ let Property = React.createClass({
handleChange(event) {
this.props.handleChange(event);
if ('onChange' in this.props) {
this.props.onChange(event);
}
this.setState({value: event.target.value});
},
handleFocus() {
@ -120,6 +124,13 @@ let Property = React.createClass({
{this.props.tooltip}
</Tooltip>);
}
let footer = null;
if (this.props.footer){
footer = (
<div className="ascribe-property-footer">
{this.props.footer}
</div>);
}
return (
<div
className={'ascribe-settings-wrapper ' + this.getClassName()}
@ -132,6 +143,7 @@ let Property = React.createClass({
{this.state.errors}
<span>{ this.props.label}</span>
{this.renderChildren()}
{footer}
</div>
</OverlayTrigger>
</div>

View File

@ -97,12 +97,12 @@ let PieceListBulkModal = React.createClass({
</div>
</div>
<p></p>
<div className="row">
<div className="row-fluid">
<AclButtonList
availableAcls={availableAcls}
editions={selectedEditions}
handleSuccess={this.handleSuccess}
className="text-center"/>
className="text-center ascribe-button-list collapse-group"/>
</div>
</div>
</div>

View File

@ -1,13 +1,12 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Input from 'react-bootstrap/lib/Input';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import Button from 'react-bootstrap/lib/Button';
let Link = Router.Link;
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
let PieceListToolbar = React.createClass({
@ -29,15 +28,19 @@ let PieceListToolbar = React.createClass({
<div className="row">
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<div className="row">
<div className="col-xs-12 col-md-12 col-md-5 col-lg-4 col-sm-offset-1 col-md-offset-2 col-lg-offset-2 clear-paddings">
<div className="form-inline">
<Input type='text' ref="search" placeholder="Search..." onChange={this.searchFor} addonAfter={searchIcon} />
&nbsp;&nbsp;
{/*<PieceListToolbarFilterWidgetFilter />*/}
<Link to="register_piece">
<Button>+ Artwork</Button>
</Link>
</div>
<div className="col-xs-12 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2 clear-paddings">
<Input wrapperClassName='wrapper'>
<Row>
<Col xs={7} sm={4}>
<Input type='text' ref="search" placeholder="Search..." onChange={this.searchFor} addonAfter={searchIcon} />
</Col>
<Col xs={5} sm={5}>
<ButtonLink to="register_piece">
+ Artwork
</ButtonLink>
</Col>
</Row>
</Input>
</div>
</div>
</div>

View File

@ -2,7 +2,7 @@
export class ColumnModel {
// ToDo: Add validation for all passed-in parameters
constructor(transformFn, columnName, displayName, displayType, rowWidth, canBeOrdered, transition) {
constructor(transformFn, columnName, displayName, displayType, rowWidth, canBeOrdered, transition, className) {
this.transformFn = transformFn;
this.columnName = columnName;
this.displayName = displayName;
@ -10,6 +10,7 @@ export class ColumnModel {
this.rowWidth = rowWidth;
this.canBeOrdered = canBeOrdered;
this.transition = transition;
this.className = className ? className : '';
}
}

View File

@ -23,17 +23,18 @@ let TableHeader = React.createClass({
return (
<thead>
<tr>
{this.props.columnList.map((val, i) => {
{this.props.columnList.map((column, i) => {
let columnClasses = this.calcColumnClasses(this.props.columnList, i, 12);
let columnName = this.props.columnList[i].columnName;
let canBeOrdered = this.props.columnList[i].canBeOrdered;
let columnName = column.columnName;
let canBeOrdered = column.canBeOrdered;
return (
<TableHeaderItem
className={column.className}
key={i}
columnClasses={columnClasses}
displayName={val.displayName}
displayName={column.displayName}
columnName={columnName}
canBeOrdered={canBeOrdered}
orderAsc={this.props.orderAsc}

View File

@ -16,7 +16,8 @@ let TableHeaderItem = React.createClass({
canBeOrdered: React.PropTypes.bool,
changeOrder: React.PropTypes.func,
orderAsc: React.PropTypes.bool,
orderBy: React.PropTypes.string
orderBy: React.PropTypes.string,
className: React.PropTypes.string
},
changeOrder() {
@ -28,7 +29,7 @@ let TableHeaderItem = React.createClass({
if(this.props.columnName === this.props.orderBy) {
return (
<th
className={'ascribe-table-header-column'}
className={'ascribe-table-header-column ' + this.props.className}
onClick={this.changeOrder}>
<span>{this.props.displayName} <TableHeaderItemCarret orderAsc={this.props.orderAsc} /></span>
</th>
@ -36,7 +37,7 @@ let TableHeaderItem = React.createClass({
} else {
return (
<th
className={'ascribe-table-header-column'}
className={'ascribe-table-header-column ' + this.props.className}
onClick={this.changeOrder}>
<span>{this.props.displayName}</span>
</th>
@ -44,7 +45,7 @@ let TableHeaderItem = React.createClass({
}
} else {
return (
<th className={'ascribe-table-header-column'}>
<th className={'ascribe-table-header-column ' + this.props.className}>
<span>
{this.props.displayName}
</span>

View File

@ -41,7 +41,7 @@ let TableItemWrapper = React.createClass({
* programmatically
*/
return (
<td key={i}>
<td key={i} className={column.className}>
<Link
className={'ascribe-table-item-column'}
onClick={column.transition.callback}

View File

@ -5,8 +5,9 @@ import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterato
// Taken from: https://github.com/fedosejev/react-file-drag-and-drop
var FileDragAndDrop = React.createClass({
let FileDragAndDrop = React.createClass({
propTypes: {
className: React.PropTypes.string,
onDragStart: React.PropTypes.func,
onDrop: React.PropTypes.func.isRequired,
onDrag: React.PropTypes.func,
@ -17,8 +18,13 @@ var FileDragAndDrop = React.createClass({
onDragEnd: React.PropTypes.func,
filesToUpload: React.PropTypes.array,
handleDeleteFile: React.PropTypes.func,
handleCancelFile: React.PropTypes.func,
handlePauseFile: React.PropTypes.func,
handleResumeFile: React.PropTypes.func,
multiple: React.PropTypes.bool,
dropzoneInactive: React.PropTypes.bool
dropzoneInactive: React.PropTypes.bool,
areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool
},
handleDragStart(event) {
@ -59,7 +65,6 @@ var FileDragAndDrop = React.createClass({
}
},
handleDrop(event) {
event.preventDefault();
event.stopPropagation();
@ -85,6 +90,27 @@ var FileDragAndDrop = React.createClass({
this.props.handleDeleteFile(fileId);
},
handleCancelFile(fileId) {
// input's value is not change the second time someone
// inputs the same file again, therefore we need to reset its value
this.refs.fileinput.getDOMNode().value = '';
this.props.handleCancelFile(fileId);
},
handlePauseFile(fileId) {
// input's value is not change the second time someone
// inputs the same file again, therefore we need to reset its value
this.refs.fileinput.getDOMNode().value = '';
this.props.handlePauseFile(fileId);
},
handleResumeFile(fileId) {
// input's value is not change the second time someone
// inputs the same file again, therefore we need to reset its value
this.refs.fileinput.getDOMNode().value = '';
this.props.handleResumeFile(fileId);
},
handleOnClick() {
// when multiple is set to false and the user already uploaded a piece,
// do not propagate event
@ -92,17 +118,24 @@ var FileDragAndDrop = React.createClass({
return;
}
// Simulate click on hidden file input
var event = document.createEvent('HTMLEvents');
event.initEvent('click', false, true);
this.refs.fileinput.getDOMNode().dispatchEvent(event);
// Firefox only recognizes the simulated mouse click if bubbles is set to true,
// but since Google Chrome propagates the event much further than needed, we
// need to stop propagation as soon as the event is created
var evt = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
evt.stopPropagation();
this.refs.fileinput.getDOMNode().dispatchEvent(evt);
},
render: function () {
console.log(this.props.dropzoneInactive);
let hasFiles = this.props.filesToUpload.length > 0;
let className = hasFiles ? 'file-drag-and-drop has-files ' : 'file-drag-and-drop ';
// has files only is true if there are files that do not have the status deleted or canceled
let hasFiles = this.props.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0;
let className = hasFiles ? 'has-files ' : '';
className += this.props.dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone';
className += this.props.className ? ' ' + this.props.className : '';
return (
<div
@ -115,10 +148,15 @@ var FileDragAndDrop = React.createClass({
onDragOver={this.handleDragOver}
onDrop={this.handleDrop}
onDragEnd={this.handleDragEnd}>
{hasFiles ? null : this.props.multiple ? <span>Click or drag to add files</span> : <span>Click or drag to add a file</span>}
{hasFiles ? null : this.props.multiple ? <span className="file-drag-and-drop-dialog">Click or drag to add files</span> : <span className="file-drag-and-drop-dialog">Click or drag to add a file</span>}
<FileDragAndDropPreviewIterator
files={this.props.filesToUpload}
handleDeleteFile={this.handleDeleteFile}/>
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"

View File

@ -1,3 +1,5 @@
'use strict';
import React from 'react';
import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image';
@ -6,43 +8,78 @@ import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other';
let FileDragAndDropPreview = React.createClass({
propsTypes: {
propTypes: {
file: React.PropTypes.shape({
url: React.PropTypes.string,
type: React.PropTypes.string
}).isRequired,
handleDeleteFile: React.PropTypes.func
handleDeleteFile: React.PropTypes.func,
handleCancelFile: React.PropTypes.func,
handlePauseFile: React.PropTypes.func,
handleResumeFile: React.PropTypes.func,
areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool
},
handleDeleteFile(event) {
event.preventDefault();
event.stopPropagation();
toggleUploadProcess() {
if(this.props.file.status === 'uploading') {
this.props.handlePauseFile(this.props.file.id);
} else if(this.props.file.status === 'paused') {
this.props.handleResumeFile(this.props.file.id);
}
},
handleDeleteFile() {
// handleDeleteFile is optional, so if its not submitted,
// don't run it
if(this.props.handleDeleteFile) {
// On the other hand, if the files progress is not yet at a 100%,
// just run fineuploader.cancel
if(this.props.handleDeleteFile && this.props.file.progress === 100) {
this.props.handleDeleteFile(this.props.file.id);
} else if(this.props.handleCancelFile && this.props.file.progress !== 100) {
this.props.handleCancelFile(this.props.file.id);
}
},
handleDownloadFile() {
if(this.props.file.s3Url) {
open(this.props.file.s3Url);
}
},
render() {
let previewElement;
let removeBtn;
// Decide wether an image or a placeholder picture should be displayed
// Decide whether an image or a placeholder picture should be displayed
if(this.props.file.type.split('/')[0] === 'image') {
previewElement = (<FileDragAndDropPreviewImage
progress={this.props.file.progress}
url={this.props.file.url}/>);
previewElement = (<FileDragAndDropPreviewImage
onClick={this.handleDeleteFile}
progress={this.props.file.progress}
url={this.props.file.url}
toggleUploadProcess={this.toggleUploadProcess}
areAssetsDownloadable={this.props.areAssetsDownloadable}
downloadUrl={this.props.file.s3UrlSafe}/>);
} else {
previewElement = (<FileDragAndDropPreviewOther
progress={this.props.file.progress}
type={this.props.file.type.split('/')[1]}/>);
previewElement = (<FileDragAndDropPreviewOther
onClick={this.handleDeleteFile}
progress={this.props.file.progress}
type={this.props.file.type.split('/')[1]}
toggleUploadProcess={this.toggleUploadProcess}
areAssetsDownloadable={this.props.areAssetsDownloadable}
downloadUrl={this.props.file.s3UrlSafe}/>);
}
if(this.props.areAssetsEditable) {
removeBtn = (<div className="delete-file">
<span className="glyphicon glyphicon-remove text-center" aria-hidden="true" title="Remove file" onClick={this.handleDeleteFile}/>
</div>);
}
return (
<div
className="file-drag-and-drop-position"
onClick={this.handleDeleteFile}>
<div
className="file-drag-and-drop-position">
{removeBtn}
{previewElement}
</div>
);

View File

@ -1,10 +1,34 @@
'use strict';
import React from 'react';
import ProgressBar from 'react-progressbar';
import AppConstants from '../../constants/application_constants';
let FileDragAndDropPreviewImage = React.createClass({
propTypes: {
progress: React.PropTypes.number,
url: React.PropTypes.string
url: React.PropTypes.string,
toggleUploadProcess: React.PropTypes.func,
downloadUrl: React.PropTypes.string,
areAssetsDownloadable: React.PropTypes.bool
},
getInitialState() {
return {
paused: true
};
},
toggleUploadProcess(e) {
e.preventDefault();
e.stopPropagation();
this.setState({
paused: !this.state.paused
});
this.props.toggleUploadProcess();
},
render() {
@ -13,11 +37,30 @@ let FileDragAndDropPreviewImage = React.createClass({
backgroundSize: 'cover'
};
let actionSymbol;
if(this.props.progress > 0 && this.props.progress < 99 && this.state.paused) {
actionSymbol = <span className="glyphicon glyphicon-pause action-file" aria-hidden="true" title="Pause upload" onClick={this.toggleUploadProcess}/>;
} else if(this.props.progress > 0 && this.props.progress < 99 && !this.state.paused) {
actionSymbol = <span className="glyphicon glyphicon-play action-file" aria-hidden="true" title="Resume uploading" onClick={this.toggleUploadProcess}/>;
} else if(this.props.progress === 100) {
// only if assets are actually downloadable, there should be a download icon if the process is already at
// 100%. If not, no actionSymbol should be displayed
if(this.props.areAssetsDownloadable) {
actionSymbol = <a href={this.props.downloadUrl} target="_blank" className="glyphicon glyphicon-download action-file" aria-hidden="true" title="Download file"/>;
}
} else {
actionSymbol = <img height={35} className="action-file" src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
}
return (
<div
className="file-drag-and-drop-preview-image"
style={imageStyle}>
<ProgressBar completed={this.props.progress} color="black"/>
{actionSymbol}
</div>
);
}

View File

@ -1,3 +1,5 @@
'use strict';
import React from 'react';
import FileDragAndDropPreview from './file_drag_and_drop_preview';
@ -5,7 +7,12 @@ import FileDragAndDropPreview from './file_drag_and_drop_preview';
let FileDragAndDropPreviewIterator = React.createClass({
propTypes: {
files: React.PropTypes.array,
handleDeleteFile: React.PropTypes.func
handleDeleteFile: React.PropTypes.func,
handleCancelFile: React.PropTypes.func,
handlePauseFile: React.PropTypes.func,
handleResumeFile: React.PropTypes.func,
areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool
},
render() {
@ -13,12 +20,21 @@ let FileDragAndDropPreviewIterator = React.createClass({
return (
<div>
{this.props.files.map((file, i) => {
return (
<FileDragAndDropPreview
key={i}
file={file}
handleDeleteFile={this.props.handleDeleteFile}/>
);
if(file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1) {
return (
<FileDragAndDropPreview
key={i}
file={file}
handleDeleteFile={this.props.handleDeleteFile}
handleCancelFile={this.props.handleCancelFile}
handlePauseFile={this.props.handlePauseFile}
handleResumeFile={this.props.handleResumeFile}
areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={this.props.areAssetsEditable}/>
);
} else {
return null;
}
})}
</div>
);

View File

@ -1,19 +1,63 @@
'use strict';
import React from 'react';
import ProgressBar from 'react-progressbar';
import AppConstants from '../../constants/application_constants';
let FileDragAndDropPreviewOther = React.createClass({
propTypes: {
type: React.PropTypes.string,
progress: React.PropTypes.number
progress: React.PropTypes.number,
areAssetsDownloadable: React.PropTypes.bool,
toggleUploadProcess: React.PropTypes.func,
downloadUrl: React.PropTypes.string
},
getInitialState() {
return {
paused: true
};
},
toggleUploadProcess(e) {
e.preventDefault();
e.stopPropagation();
this.setState({
paused: !this.state.paused
});
this.props.toggleUploadProcess();
},
render() {
return(
<div
let actionSymbol;
if(this.props.progress > 0 && this.props.progress < 99 && this.state.paused) {
actionSymbol = <span className="glyphicon glyphicon-pause action-file" aria-hidden="true" title="Pause upload" onClick={this.toggleUploadProcess}/>;
} else if(this.props.progress > 0 && this.props.progress < 99 && !this.state.paused) {
actionSymbol = <span className="glyphicon glyphicon-play action-file" aria-hidden="true" title="Resume uploading" onClick={this.toggleUploadProcess}/>;
} else if(this.props.progress === 100) {
// only if assets are actually downloadable, there should be a download icon if the process is already at
// 100%. If not, no actionSymbol should be displayed
if(this.props.areAssetsDownloadable) {
actionSymbol = <a href={this.props.downloadUrl} target="_blank" className="glyphicon glyphicon-download action-file" aria-hidden="true" title="Download file"/>;
}
} else {
actionSymbol = <img height={35} src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
}
return (
<div
className="file-drag-and-drop-preview">
<ProgressBar completed={this.props.progress} color="black"/>
<div className="file-drag-and-drop-preview-table-wrapper">
<div className="file-drag-and-drop-preview-other">
{actionSymbol}
<span>{'.' + this.props.type}</span>
</div>
</div>

View File

@ -6,6 +6,10 @@ import promise from 'es6-promise';
promise.polyfill();
import fetch from 'isomorphic-fetch';
import AppConstants from '../../constants/application_constants';
import { getCookie } from '../../utils/fetch_api_utils';
import S3Fetcher from '../../fetchers/s3_fetcher';
import fineUploader from 'fineUploader';
import FileDragAndDrop from './file_drag_and_drop';
@ -18,12 +22,14 @@ var ReactS3FineUploader = React.createClass({
propTypes: {
keyRoutine: React.PropTypes.shape({
url: React.PropTypes.string,
fileClass: React.PropTypes.string
fileClass: React.PropTypes.string,
bitcoinId: React.PropTypes.string
}),
createBlobRoutine: React.PropTypes.shape({
url: React.PropTypes.string
url: React.PropTypes.string,
bitcoinId: React.PropTypes.string
}),
handleChange: React.PropTypes.func,
submitKey: React.PropTypes.func,
autoUpload: React.PropTypes.bool,
debug: React.PropTypes.bool,
objectProperties: React.PropTypes.shape({
@ -59,7 +65,8 @@ var ReactS3FineUploader = React.createClass({
deleteFile: React.PropTypes.shape({
enabled: React.PropTypes.bool,
method: React.PropTypes.string,
endpoint: React.PropTypes.string
endpoint: React.PropTypes.string,
customHeaders: React.PropTypes.object
}),
session: React.PropTypes.shape({
endpoint: React.PropTypes.bool
@ -75,7 +82,71 @@ var ReactS3FineUploader = React.createClass({
multiple: React.PropTypes.bool,
retry: React.PropTypes.shape({
enableAuto: React.PropTypes.bool
})
}),
setIsUploadReady: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool
},
getDefaultProps() {
return {
autoUpload: true,
debug: false,
objectProperties: {
acl: 'public-read',
bucket: 'ascribe0'
},
request: {
endpoint: 'https://ascribe0.s3.amazonaws.com',
accessKey: 'AKIAIVCZJ33WSCBQ3QDA'
},
uploadSuccess: {
params: {
isBrowserPreviewCapable: fineUploader.supportedFeatures.imagePreviews
}
},
signature: {
endpoint: AppConstants.serverUrl + 's3/signature/',
customHeaders: {
'X-CSRFToken': getCookie('csrftoken')
}
},
deleteFile: {
enabled: true,
method: 'DELETE',
endpoint: AppConstants.serverUrl + 's3/delete',
customHeaders: {
'X-CSRFToken': getCookie('csrftoken')
}
},
cors: {
expected: true,
sendCredentials: true
},
chunking: {
enabled: true
},
resume: {
enabled: true
},
retry: {
enableAuto: false
},
session: {
endpoint: null
},
messages: {
unsupportedBrowser: '<h3>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) {
name = name.slice(0, 15) + '...' + name.slice(-15);
}
return name;
},
multiple: false
};
},
getInitialState() {
@ -109,13 +180,14 @@ var ReactS3FineUploader = React.createClass({
callbacks: {
onSubmit: this.onSubmit,
onComplete: this.onComplete,
onCancel: this.onCancel,
onDelete: this.onDelete,
onSessionRequestComplete: this.onSessionRequestComplete,
onProgress: this.onProgress,
onRetry: this.onRetry,
onAutoRetry: this.onAutoRetry,
onManualRetry: this.onManualRetry,
onDeleteComplete: this.onDeleteComplete
onDeleteComplete: this.onDeleteComplete,
onSessionRequestComplete: this.onSessionRequestComplete
}
};
},
@ -140,7 +212,8 @@ var ReactS3FineUploader = React.createClass({
credentials: 'include',
body: JSON.stringify({
'filename': filename,
'file_class': 'digitalwork'
'file_class': this.props.keyRoutine.fileClass,
'bitcoin_id': this.props.keyRoutine.bitcoinId
})
})
.then((res) => {
@ -172,8 +245,16 @@ var ReactS3FineUploader = React.createClass({
});
this.setState(newState);
this.createBlob(files[id]);
this.props.handleChange();
console.log('completed ' + files[id].name);
this.props.submitKey(files[id].key);
// also, lets check if after the completion of this upload,
// the form is ready for submission or not
if(this.props.isReadyForFormSubmission && this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
},
createBlob(file) {
@ -188,13 +269,23 @@ var ReactS3FineUploader = React.createClass({
credentials: 'include',
body: JSON.stringify({
'filename': file.name,
'key': file.key
'key': file.key,
'bitcoin_id': this.props.createBlobRoutine.bitcoinId
})
})
.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) => {
@ -219,33 +310,22 @@ var ReactS3FineUploader = React.createClass({
console.log('delete');
},
onCancel() {
console.log('cancel');
},
onCancel(id) {
this.removeFileWithIdFromFilesToUpload(id);
onSessionRequestComplete() {
console.log('sessionrequestcomplete');
},
let notification = new GlobalNotificationModel('File upload canceled', 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
onDeleteComplete(id, xhr, isError) {
if(isError) {
// also, sync files from state with the ones from fineuploader
let filesToUpload = JSON.parse(JSON.stringify(this.state.filesToUpload));
// splice because I can
filesToUpload.splice(id, 1);
// set state
this.setState({
filesToUpload: React.addons.update(this.state.filesToUpload, {$set: filesToUpload})
});
if(this.props.isReadyForFormSubmission && this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
console.log(id);
// TODO: add global notification
this.props.setIsUploadReady(false);
}
},
onProgress(id, name, uploadedBytes, totalBytes) {
var newState = React.addons.update(this.state, {
let newState = React.addons.update(this.state, {
filesToUpload: { [id]: {
progress: { $set: (uploadedBytes / totalBytes) * 100} }
}
@ -253,19 +333,106 @@ var ReactS3FineUploader = React.createClass({
this.setState(newState);
},
onSessionRequestComplete(response, success) {
if(success) {
// fetch blobs for images
response = response.map((file) => {
file.url = file.s3UrlSafe;
file.status = 'online';
file.progress = 100;
return file;
});
// add file to filesToUpload
let updatedFilesToUpload = this.state.filesToUpload.concat(response);
// refresh all files ids,
updatedFilesToUpload = updatedFilesToUpload.map((file, i) => {
file.id = i;
return file;
});
let newState = React.addons.update(this.state, {filesToUpload: {$set: updatedFilesToUpload}});
this.setState(newState);
} else {
// server has to respond with 204
//let notification = new GlobalNotificationModel('Could not load attached files (Further data)', 'danger', 10000);
//GlobalNotificationActions.appendGlobalNotification(notification);
//
//throw new Error('The session request failed', response);
}
},
onDeleteComplete(id, xhr, isError) {
if(isError) {
let notification = new GlobalNotificationModel('Couldn\'t delete file', 'danger', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
} else {
this.removeFileWithIdFromFilesToUpload(id);
let notification = new GlobalNotificationModel('File deleted', 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
}
if(this.props.isReadyForFormSubmission && this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
},
handleDeleteFile(fileId) {
// delete file from server
this.state.uploader.deleteFile(fileId);
// this is being continues in onDeleteFile, as
// fineuploaders deleteFile does not return a correct callback or
// promise
// In some instances (when the file was already uploaded and is just displayed to the user)
// fineuploader does not register an id on the file (we do, don't be confused by this!).
// Since you can only delete a file by its id, we have to implement this method ourselves
//
// So, if an id is not present, we delete the file manually
// To check which files are already uploaded from previous sessions we check their status.
// If they are, it is "online"
if(this.state.filesToUpload[fileId].status !== 'online') {
// delete file from server
this.state.uploader.deleteFile(fileId);
// this is being continues in onDeleteFile, as
// fineuploaders deleteFile does not return a correct callback or
// promise
} else {
let fileToDelete = this.state.filesToUpload[fileId];
fileToDelete.status = 'deleted';
S3Fetcher
.deleteFile(fileToDelete.s3Key, fileToDelete.s3Bucket)
.then(() => this.onDeleteComplete(fileToDelete.id, null, false))
.catch(() => this.onDeleteComplete(fileToDelete.id, null, true));
}
},
handleCancelFile(fileId) {
this.state.uploader.cancel(fileId);
},
handlePauseFile(fileId) {
if(this.state.uploader.pauseUpload(fileId)) {
this.setStatusOfFile(fileId, 'paused');
} else {
throw new Error('File upload could not be paused.');
}
},
handleResumeFile(fileId) {
if(this.state.uploader.continueUpload(fileId)) {
this.setStatusOfFile(fileId, 'uploading');
} else {
throw new Error('File upload could not be resumed.');
}
},
handleUploadFile(files) {
// If multiple set and user already uploaded its work,
// cancel upload
if(!this.props.multiple && this.state.filesToUpload.length > 0) {
if(!this.props.multiple && this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled').length > 0) {
return;
}
@ -304,24 +471,62 @@ var ReactS3FineUploader = React.createClass({
oldAndNewFiles[i].progress = oldFiles[j].progress;
oldAndNewFiles[i].type = oldFiles[j].type;
oldAndNewFiles[i].url = oldFiles[j].url;
oldAndNewFiles[i].key = oldFiles[j].key;
}
}
}
// set the new file array
let newState = React.addons.update(this.state, {
filesToUpload: { $set: oldAndNewFiles }
});
this.setState(newState);
},
removeFileWithIdFromFilesToUpload(fileId) {
// also, sync files from state with the ones from fineuploader
let filesToUpload = JSON.parse(JSON.stringify(this.state.filesToUpload));
// splice because I can
filesToUpload.splice(fileId, 1);
// set state
let newState = React.addons.update(this.state, {
filesToUpload: { $set: filesToUpload }
});
this.setState(newState);
},
setStatusOfFile(fileId, status) {
// also, sync files from state with the ones from fineuploader
let filesToUpload = JSON.parse(JSON.stringify(this.state.filesToUpload));
// splice because I can
filesToUpload[fileId].status = status;
// set state
let newState = React.addons.update(this.state, {
filesToUpload: { $set: filesToUpload }
});
this.setState(newState);
},
render() {
return (
<FileDragAndDrop
onDrop={this.handleUploadFile}
filesToUpload={this.state.filesToUpload}
handleDeleteFile={this.handleDeleteFile}
multiple={this.props.multiple}
dropzoneInactive={!this.props.multiple && this.state.filesToUpload.length > 0} />
<div>
<FileDragAndDrop
className="file-drag-and-drop"
onDrop={this.handleUploadFile}
filesToUpload={this.state.filesToUpload}
handleDeleteFile={this.handleDeleteFile}
handleCancelFile={this.handleCancelFile}
handlePauseFile={this.handlePauseFile}
handleResumeFile={this.handleResumeFile}
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} />
</div>
);
}

View File

@ -0,0 +1,101 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import InputTextAreaToggable from './ascribe_forms/input_textarea_toggable';
import apiUrls from '../constants/api_urls';
let CoaVerifyContainer = React.createClass({
mixins: [Router.Navigation],
render() {
return (
<div className="ascribe-login-wrapper">
<br/>
<div className="ascribe-login-text ascribe-login-header">
Verify your Certificate of Authenticity
</div>
<CoaVerifyForm />
<br />
<br />
ascribe is using the following public key for verification:
<br />
<pre>
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDddadqY31kKPFYk8PQA8BWSTbm
gaGf9KEYBALp2nWAJcwq80qBzGF+gfi0Z+yb4ooeKHl27GnuxZYValE1Z5ZujfeJ
TgO4li59ZMYiah8oXZp/OysrBwCvWw0PtWd8/D9Nc4PqyOz5gzEh6kFah5VsuAke
Znu2w7KmeLZ85SmwEQIDAQAB
-----END PUBLIC KEY-----
</pre>
</div>
);
}
});
let CoaVerifyForm = React.createClass({
mixins: [Router.Navigation],
handleSuccess(response){
let notification = null;
if (response.verdict){
notification = new GlobalNotificationModel('Certificate of Authenticity successfully verified', 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
}
},
render() {
return (
<div>
<Form
url={apiUrls.coa_verify}
handleSuccess={this.handleSuccess}
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
Verify your Certificate of Authenticity
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<Property
name='message'
label="Message">
<input
type="text"
placeholder="Copy paste the message on the bottom of your Certificate of Authenticity"
autoComplete="on"
name="username"
required/>
</Property>
<Property
name='signature'
label="Signature">
<InputTextAreaToggable
rows={3}
editable={true}
placeholder="Copy paste the signature on the bottom of your Certificate of Authenticity"
required/>
</Property>
<hr />
</Form>
</div>
);
}
});
export default CoaVerifyContainer;

View File

@ -1,6 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
@ -9,6 +10,8 @@ import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import UserActions from '../actions/user_actions';
import UserStore from '../stores/user_store';
import CoaActions from '../actions/coa_actions';
import CoaStore from '../stores/coa_store';
import MediaPlayer from './ascribe_media/media_player';
@ -24,12 +27,18 @@ import RequestActionForm from './ascribe_forms/form_request_action';
import EditionActions from '../actions/edition_actions';
import AclButtonList from './ascribe_buttons/acl_button_list';
import fineUploader from 'fineUploader';
import ReactS3FineUploader from './ascribe_uploader/react_s3_fine_uploader';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import requests from '../utils/requests';
import apiUrls from '../constants/api_urls';
import AppConstants from '../constants/application_constants';
import { getCookie } from '../utils/fetch_api_utils';
let Link = Router.Link;
/**
* This is the component that implements display-specific functionality
*/
@ -56,6 +65,8 @@ let Edition = React.createClass({
this.setState(state);
},
render() {
let thumbnail = this.props.edition.thumbnail;
let mimetype = this.props.edition.digital_work.mime;
@ -93,6 +104,7 @@ let Edition = React.createClass({
<Col md={6} className="ascribe-edition-details">
<EditionHeader edition={this.props.edition}/>
<EditionSummary
currentUser={this.state.currentUser}
edition={this.props.edition} />
<CollapsibleParagraph
@ -109,13 +121,22 @@ let Edition = React.createClass({
</CollapsibleParagraph>
<CollapsibleParagraph
title="Further Details (all editions)"
show={this.props.edition.acl.indexOf('edit') > -1 || Object.keys(this.props.edition.extra_data).length > 0}>
title="Further Details"
show={this.props.edition.acl.indexOf('edit') > -1
|| Object.keys(this.props.edition.extra_data).length > 0
|| this.props.edition.other_data !== null}>
<EditionFurtherDetails
handleSuccess={this.props.loadEdition}
edition={this.props.edition}/>
</CollapsibleParagraph>
<CollapsibleParagraph
title="Certificate of Authenticity"
show={this.props.edition.acl.indexOf('coa') > -1}>
<CoaDetails
edition={this.props.edition}/>
</CollapsibleParagraph>
<CollapsibleParagraph
title="Provenance/Ownership History"
show={this.props.edition.ownership_history && this.props.edition.ownership_history.length > 0}>
@ -191,6 +212,9 @@ let EditionSummary = React.createClass({
edition: React.PropTypes.object
},
getTransferWithdrawData(){
return {'bitcoin_id': this.props.edition.bitcoin_id};
},
handleSuccess(){
EditionActions.fetchOne(this.props.edition.id);
},
@ -202,7 +226,24 @@ let EditionSummary = React.createClass({
render() {
let status = null;
if (this.props.edition.status.length > 0){
status = <EditionDetailProperty label="STATUS" value={ this.props.edition.status.join().replace(/_/, ' ') } />;
let statusStr = this.props.edition.status.join().replace(/_/, ' ');
status = <EditionDetailProperty label="STATUS" value={ statusStr }/>;
if (this.props.edition.pending_new_owner && this.props.edition.acl.indexOf('withdraw_transfer') > -1){
status = (
<Form
url={apiUrls.ownership_transfers_withdraw}
getFormData={this.getTransferWithdrawData}
handleSuccess={this.showNotification}>
<EditionDetailProperty label="STATUS" value={ statusStr }>
<button
type="submit"
className="pull-right btn btn-default btn-sm">
WITHDRAW
</button>
</EditionDetailProperty>
</Form>
);
}
}
let actions = null;
if (this.props.edition.request_action && this.props.edition.request_action.length > 0){
@ -216,7 +257,7 @@ let EditionSummary = React.createClass({
<Row>
<Col md={12}>
<AclButtonList
className="pull-left"
className="text-center ascribe-button-list"
availableAcls={this.props.edition.acl}
editions={[this.props.edition]}
handleSuccess={this.handleSuccess} />
@ -262,6 +303,18 @@ let EditionDetailProperty = React.createClass({
},
render() {
let value = this.props.value;
if (this.props.children){
value = (
<div className="row-same-height">
<div className="col-xs-6 col-xs-height col-bottom no-padding">
{ this.props.value }
</div>
<div className="col-xs-6 col-xs-height">
{ this.props.children }
</div>
</div>);
}
return (
<div className="row ascribe-detail-property">
<div className="row-same-height">
@ -269,7 +322,7 @@ let EditionDetailProperty = React.createClass({
<div>{ this.props.label + this.props.separator}</div>
</div>
<div className={this.props.valueClassName + ' col-xs-height col-bottom'}>
<div>{ this.props.value }</div>
{value}
</div>
</div>
</div>
@ -284,19 +337,20 @@ let EditionDetailHistoryIterator = React.createClass({
render() {
return (
<div>
<Form>
{this.props.history.map((historicalEvent, i) => {
return (
<EditionDetailProperty
key={i}
label={historicalEvent[0]}
value={historicalEvent[1]}
labelClassName="col-xs-4 col-sm-4 col-md-4 col-lg-4"
valueClassName="col-xs-8 col-sm-8 col-md-8 col-lg-8"
separator="" />
<Property
name={i}
key={i}
label={ historicalEvent[0] }
editable={false}>
<pre className="ascribe-pre">{ historicalEvent[1] }</pre>
</Property>
);
})}
</div>
<hr />
</Form>
);
}
});
@ -386,14 +440,43 @@ let EditionFurtherDetails = React.createClass({
edition: React.PropTypes.object,
handleSuccess: React.PropTypes.func
},
getInitialState() {
return {
loading: false
};
},
showNotification(){
this.props.handleSuccess();
let notification = new GlobalNotificationModel('Details updated', 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
submitKey(key){
this.setState({
otherDataKey: key
});
},
setIsUploadReady(isReady) {
this.setState({
isUploadReady: isReady
});
},
isReadyForFormSubmission(files) {
files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled');
if(files.length > 0 && files[0].status === 'upload successful') {
return true;
} else {
return false;
}
},
render() {
let editable = this.props.edition.acl.indexOf('edit') > -1;
return (
<Row>
<Col md={12} className="ascribe-edition-personal-note">
@ -415,10 +498,127 @@ let EditionFurtherDetails = React.createClass({
handleSuccess={this.showNotification}
editable={editable}
edition={this.props.edition} />
<FileUploader
submitKey={this.submitKey}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={this.isReadyForFormSubmission}
editable={editable}
edition={this.props.edition}/>
</Col>
</Row>
);
}
});
let FileUploader = React.createClass({
propTypes: {
edition: React.PropTypes.object,
setIsUploadReady: React.PropTypes.func,
submitKey: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
editable: React.PropTypes.bool
},
render() {
// Essentially there a three cases important to the fileuploader
//
// 1. there is no other_data => do not show the fileuploader at all
// 2. there is other_data, but user has no edit rights => show fileuploader but without action buttons
// 3. both other_data and editable are defined or true => show fileuploade with all action buttons
if (!this.props.editable && !this.props.edition.other_data){
return null;
}
return (
<Form>
<Property
label="Additional files">
<ReactS3FineUploader
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'otherdata',
bitcoinId: this.props.edition.bitcoin_id
}}
createBlobRoutine={{
url: apiUrls.blob_otherdatas,
bitcoinId: this.props.edition.bitcoin_id
}}
validation={{
itemLimit: 100000,
sizeLimit: '10000000'
}}
submitKey={this.props.submitKey}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
session={{
endpoint: AppConstants.serverUrl + 'api/blob/otherdatas/fineuploader_session/',
customHeaders: {
'X-CSRFToken': getCookie('csrftoken')
},
params: {
'pk': this.props.edition.other_data ? this.props.edition.other_data.id : null
}
}}
areAssetsDownloadable={true}
areAssetsEditable={this.props.editable}/>
</Property>
<hr />
</Form>
);
}
});
let CoaDetails = React.createClass({
propTypes: {
edition: React.PropTypes.object
},
getInitialState() {
return CoaStore.getState();
},
componentDidMount() {
CoaStore.listen(this.onChange);
if (this.props.edition.coa) {
CoaActions.fetchOne(this.props.edition.coa);
}
else{
console.log('create coa');
CoaActions.create(this.props.edition);
}
},
componentWillUnmount() {
CoaStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
if (this.state.coa.url_safe) {
return (
<div>
<p className="text-center ascribe-button-list">
<button className="btn btn-default btn-xs" href={this.state.coa.url_safe} target="_blank">
Download <Glyphicon glyph="cloud-download"/>
</button>
<Link to="coa_verify">
<button className="btn btn-default btn-xs">
Verify <Glyphicon glyph="check"/>
</button>
</Link>
</p>
</div>
);
}
return (
<div className="text-center">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
</div>);
}
});
export default Edition;

View File

@ -6,6 +6,9 @@ import Router from 'react-router';
import UserActions from '../actions/user_actions';
import UserStore from '../stores/user_store';
import WhitelabelActions from '../actions/whitelabel_actions';
import WhitelabelStore from '../stores/whitelabel_store';
import Alt from '../alt';
import Nav from 'react-bootstrap/lib/Nav';
@ -17,6 +20,7 @@ import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink';
import NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
let Link = Router.Link;
@ -25,22 +29,50 @@ let Header = React.createClass({
mixins: [Router.Navigation],
getInitialState() {
return UserStore.getState();
return mergeOptions(WhitelabelStore.getState(), UserStore.getState());
},
componentDidMount() {
UserActions.fetchCurrentUser();
UserStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
WhitelabelStore.listen(this.onChange);
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
},
handleLogout(){
UserActions.logoutCurrentUser();
Alt.flush();
this.transitionTo('login');
},
getLogo(){
let logo = (
<span>
<span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span>
</span>);
if (this.state.whitelabel.logo){
logo = <img className="img-brand" src={this.state.whitelabel.logo} />;
}
return logo;
},
getPoweredBy(){
if (this.state.whitelabel.logo) {
return (
<div className="row no-margin ascribe-subheader">
<a className="pull-right" href="https://www.ascribe.io/" target="_blank">
<span id="powered">powered by </span>
<span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span>
</a>
</div>);
}
return null;
},
onChange(state) {
this.setState(state);
},
@ -64,20 +96,24 @@ let Header = React.createClass({
account = <NavItemLink to="login">LOGIN</NavItemLink>;
signup = <NavItemLink to="signup">SIGNUP</NavItemLink>;
}
let brand = (<Link className="navbar-brand" to="pieces" path="/?page=1">
<span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span>
</Link>);
return (
<Navbar brand={brand} toggleNavKey={0}>
<CollapsibleNav eventKey={0}>
<Nav navbar right>
{account}
{signup}
</Nav>
</CollapsibleNav>
</Navbar>
return (
<div>
<Navbar
brand={
<Link className="navbar-brand" to="pieces" path="/?page=1">
{this.getLogo()}
</Link>}
toggleNavKey={0}>
<CollapsibleNav eventKey={0}>
<Nav navbar right>
{account}
{signup}
</Nav>
</CollapsibleNav>
</Navbar>
{this.getPoweredBy()}
</div>
);
}
});

View File

@ -12,7 +12,9 @@ import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import apiUrls from '../constants/api_urls';
import AppConstants from '../constants/application_constants';
let Link = Router.Link;
let LoginContainer = React.createClass({
mixins: [Router.Navigation],
@ -45,7 +47,6 @@ let LoginContainer = React.createClass({
<div className="ascribe-login-text ascribe-login-header">
Log in to ascribe...
</div>
<LoginForm />
</div>
);
@ -65,7 +66,7 @@ let LoginForm = React.createClass({
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 = '/collection';
window.location = AppConstants.baseUrl + 'collection';
},
render() {
@ -107,8 +108,8 @@ let LoginForm = React.createClass({
</Property>
<hr />
<div className="ascribe-login-text">
Not an ascribe user&#63; Sign up...<br/>
Forgot my password&#63; Rescue me...
Not an ascribe user&#63; <Link to="signup">Sign up...</Link><br/>
Forgot my password&#63; <Link to="password_reset">Rescue me...</Link>
</div>
</Form>
);

View File

@ -3,28 +3,159 @@
import React from 'react';
import Router from 'react-router';
import PasswordResetForm from './ascribe_forms/form_password_reset';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import apiUrls from '../constants/api_urls';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
let PasswordResetContainer = React.createClass({
mixins: [Router.Navigation],
getInitialState() {
return {isRequested: false};
},
handleRequestSuccess(email){
this.setState({isRequested: email});
},
render() {
if (this.props.query.email && this.props.query.token) {
return (
<div>
<div className="ascribe-login-text ascribe-login-header">
Reset the password for {this.props.query.email}
</div>
<PasswordResetForm
email={this.props.query.email}
token={this.props.query.token}/>
</div>
);
}
else {
if (this.state.isRequested === false) {
return (
<div>
<div className="ascribe-login-text ascribe-login-header">
Reset your ascribe password
</div>
<PasswordRequestResetForm
handleRequestSuccess={this.handleRequestSuccess}/>
</div>
);
}
else if (this.state.isRequested) {
return (
<div>
<div className="ascribe-login-text ascribe-login-header">
An email has been sent to "{this.state.isRequested}"
</div>
</div>
);
}
else {
return <span />;
}
}
}
});
handleSuccess(){
let PasswordRequestResetForm = React.createClass({
handleSuccess() {
let notificationText = 'Request succesfully sent, check your email';
let notification = new GlobalNotificationModel(notificationText, 'success', 50000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.props.handleRequestSuccess(this.refs.form.refs.email.state.value);
},
render() {
return (
<Form
ref="form"
url={apiUrls.users_password_reset_request}
handleSuccess={this.handleSuccess}
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
Reset your password
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<Property
name='email'
label="Email">
<input
type="email"
placeholder="Enter your email and we'll send a link"
name="email"
required/>
</Property>
<hr />
</Form>
);
}
});
let PasswordResetForm = React.createClass({
mixins: [Router.Navigation],
getFormData(){
let data = {};
for (let ref in this.refs.form.refs){
data[this.refs.form.refs[ref].props.name] = this.refs.form.refs[ref].state.value;
}
data.email = this.props.email;
data.token = this.props.token;
return data;
},
handleSuccess() {
this.transitionTo('pieces');
let notification = new GlobalNotificationModel('password succesfully updated', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
return (
<PasswordResetForm
email={this.props.query.email}
token={this.props.query.token}
<Form
ref="form"
url={apiUrls.users_password_reset}
handleSuccess={this.handleSuccess}
/>
);
}
getFormData={this.getFormData}
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
Reset your password
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<Property
name='password'
label="Password">
<input
type="password"
placeholder="Enter a new password"
name="password"
required/>
</Property>
<Property
name='password_confirm'
label="Confirm password">
<input
type="password"
placeholder="Enter your password once again"
name="password"
required/>
</Property>
<hr />
</Form>
);
}
});
export default PasswordResetContainer;

View File

@ -65,7 +65,6 @@ let PieceList = React.createClass({
let currentPage = parseInt(this.props.query.page, 10) || 1;
let totalPages = Math.ceil(this.state.pieceListCount / this.state.pageSize);
let loadingElement = (<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />);
return (
<div>
<PieceListToolbar

View File

@ -3,10 +3,12 @@
import React from 'react';
import AppConstants from '../constants/application_constants';
import fineUploader from 'fineUploader';
import Router from 'react-router';
import LicenseActions from '../actions/license_actions';
import LicenseStore from '../stores/license_store';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
@ -19,12 +21,34 @@ import ReactS3FineUploader from './ascribe_uploader/react_s3_fine_uploader';
import DatePicker from 'react-datepicker/dist/react-datepicker';
import { mergeOptions } from '../utils/general_utils';
let RegisterPiece = React.createClass( {
mixins: [Router.Navigation],
getInitialState(){
return {digital_work_key: null};
return mergeOptions(
LicenseStore.getState(),
{
digitalWorkKey: null,
uploadStatus: false,
selectedLicense: 0
});
},
componentDidMount() {
LicenseActions.fetchLicense();
LicenseStore.listen(this.onChange);
},
componentWillUnmount() {
LicenseStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
handleSuccess(){
let notification = new GlobalNotificationModel('Login successsful', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
@ -36,42 +60,90 @@ let RegisterPiece = React.createClass( {
for (let ref in this.refs.form.refs){
data[this.refs.form.refs[ref].props.name] = this.refs.form.refs[ref].state.value;
}
data.digital_work_key = this.state.digital_work_key;
data.digital_work_key = this.state.digitalWorkKey;
return data;
},
handleChange(){
this.setState({digital_work_key: this.refs.uploader.refs.fineuploader.state.filesToUpload[0].key});
submitKey(key){
this.setState({
digitalWorkKey: key
});
},
setIsUploadReady(isReady) {
this.setState({
isUploadReady: isReady
});
},
isReadyForFormSubmission(files) {
files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled');
if (files.length > 0 && files[0].status === 'upload successful') {
return true;
} else {
return false;
}
},
onLicenseChange(event){
console.log(this.state.licenses[event.target.selectedIndex].url);
this.setState({selectedLicense: event.target.selectedIndex});
},
getLicenses() {
if (this.state.licenses && this.state.licenses.length > 0) {
return (
<Property
name='license'
label="Copyright license..."
onChange={this.onLicenseChange}
footer={
<a className="pull-right" href={this.state.licenses[this.state.selectedLicense].url} target="_blank">
Learn more about this license
</a>}>
<select name="license">
{this.state.licenses.map((license, i) => {
return (
<option
name={i}
key={i}
value={ license.code }>
{ license.code.toUpperCase() }: { license.name }
</option>
);
})}
</select>
</Property>);
}
return null;
},
render() {
let buttons = null;
if (this.refs.uploader && this.refs.uploader.refs.fineuploader.state.filesToUpload[0].status === 'upload successful'){
buttons = (
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
Register your artwork
</button>);
}
return (
<div className="row ascribe-row">
<div className="col-md-5">
<FileUploader
ref='uploader'
handleChange={this.handleChange}/>
<br />
</div>
<div className="col-md-7">
<div className="col-md-12">
<h3 style={{'marginTop': 0}}>Lock down title</h3>
<Form
ref='form'
url={apiUrls.pieces_list}
getFormData={this.getFormData}
handleSuccess={this.handleSuccess}
buttons={buttons}
buttons={<button
type="submit"
className="btn ascribe-btn ascribe-btn-login"
disabled={!this.state.isUploadReady}>
Register your artwork
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</button>
}>
<Property
label="Files to upload">
<FileUploader
submitKey={this.submitKey}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={this.isReadyForFormSubmission}/>
</Property>
<Property
name='artist_name'
label="Artist Name">
@ -106,6 +178,7 @@ let RegisterPiece = React.createClass( {
min={1}
required/>
</Property>
{this.getLicenses()}
<hr />
</Form>
</div>
@ -115,11 +188,16 @@ let RegisterPiece = React.createClass( {
});
let FileUploader = React.createClass( {
let FileUploader = React.createClass({
propTypes: {
setIsUploadReady: React.PropTypes.func,
submitKey: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func
},
render() {
return (
<ReactS3FineUploader
ref='fineuploader'
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'digitalwork'
@ -127,59 +205,15 @@ let FileUploader = React.createClass( {
createBlobRoutine={{
url: apiUrls.blob_digitalworks
}}
handleChange={this.props.handleChange}
autoUpload={true}
debug={false}
objectProperties={{
acl: 'public-read',
bucket: 'ascribe0'
}}
request={{
endpoint: 'https://ascribe0.s3.amazonaws.com',
accessKey: 'AKIAIVCZJ33WSCBQ3QDA'
}}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/'
}}
uploadSuccess={{
params: {
isBrowserPreviewCapable: fineUploader.supportedFeatures.imagePreviews
}
}}
cors={{
expected: true
}}
chunking={{
enabled: true
}}
resume={{
enabled: true
}}
retry={{
enableAuto: false
}}
deleteFile={{
enabled: true,
method: 'DELETE',
endpoint: AppConstants.serverUrl + 's3/delete'
}}
submitKey={this.props.submitKey}
validation={{
itemLimit: 100000,
sizeLimit: '25000000000'
}}
session={{
endpoint: null
}}
messages={{
unsupportedBrowser: '<h3>Upload is not functional in IE7 as IE7 has no support for CORS!</h3>'
}}
formatFileName={(name) => {// fix maybe
if (name !== undefined && name.length > 26) {
name = name.slice(0, 15) + '...' + name.slice(-15);
}
return name;
}}
multiple={false}/>
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
areAssetsDownloadable={false}
areAssetsEditable={true}/>
);
}
});

View File

@ -137,7 +137,7 @@ let SignupForm = React.createClass({
name='promo_code'
label="Promocode">
<input
type="password"
type="text"
placeholder="Enter a promocode here (Optional)"/>
</Property>
<hr />

View File

@ -6,10 +6,15 @@ let apiUrls = {
'applications': AppConstants.apiEndpoint + 'applications/',
'application_token_refresh': AppConstants.apiEndpoint + 'applications/refresh_token/',
'blob_digitalworks': AppConstants.apiEndpoint + 'blob/digitalworks/',
'blob_otherdatas': AppConstants.apiEndpoint + 'blob/otherdatas/',
'coa': AppConstants.apiEndpoint + 'coa/${id}/',
'coa_create': AppConstants.apiEndpoint + 'coa/',
'coa_verify': AppConstants.apiEndpoint + 'coa/verify_coa/',
'edition': AppConstants.apiEndpoint + 'editions/${bitcoin_id}/',
'edition_delete': AppConstants.apiEndpoint + 'editions/${edition_id}/',
'edition_remove_from_collection': AppConstants.apiEndpoint + 'ownership/shares/${edition_id}/',
'editions_list': AppConstants.apiEndpoint + 'pieces/${piece_id}/editions/',
'licenses': AppConstants.apiEndpoint + 'ownership/licenses/',
'note_notes': AppConstants.apiEndpoint + 'note/notes/',
'note_edition': AppConstants.apiEndpoint + 'note/edition_notes/',
'ownership_consigns': AppConstants.apiEndpoint + 'ownership/consigns/',
@ -20,6 +25,7 @@ let apiUrls = {
'ownership_loans_deny': AppConstants.apiEndpoint + 'ownership/loans/deny/',
'ownership_shares': AppConstants.apiEndpoint + 'ownership/shares/',
'ownership_transfers': AppConstants.apiEndpoint + 'ownership/transfers/',
'ownership_transfers_withdraw': AppConstants.apiEndpoint + 'ownership/transfers/withdraw/',
'ownership_unconsigns': AppConstants.apiEndpoint + 'ownership/unconsigns/',
'ownership_unconsigns_deny': AppConstants.apiEndpoint + 'ownership/unconsigns/deny/',
'ownership_unconsigns_request': AppConstants.apiEndpoint + 'ownership/unconsigns/request/',
@ -33,7 +39,9 @@ let apiUrls = {
'users_password_reset_request': AppConstants.apiEndpoint + 'users/request_reset_password/',
'users_signup': AppConstants.apiEndpoint + 'users/',
'users_username': AppConstants.apiEndpoint + 'users/username/',
'wallet_settings': AppConstants.apiEndpoint + 'users/wallet_settings/'
'wallet_settings': AppConstants.apiEndpoint + 'users/wallet_settings/',
'whitelabel_settings': AppConstants.apiEndpoint + 'whitelabel/settings/${subdomain}/',
'delete_s3_file': AppConstants.serverUrl + 's3/delete/'
};
export default apiUrls;

View File

@ -2,7 +2,7 @@
const languages = {
'en-US': {
'Bitcoin Address': 'Bitcoin Address',
'ID': 'ID',
'Actions': 'Actions',
'Hide': 'Hide',
'Show the edition': 'Show the edition',
@ -16,7 +16,7 @@ const languages = {
'Next': 'Next'
},
'de': {
'Bitcoin Address': 'Bitcoin Adresse',
'ID': 'ID',
'Actions': 'Aktionen',
'Hide': 'Verstecke',
'Show the edition': 'Zeige die Edition',

View File

@ -0,0 +1,19 @@
'use strict';
import requests from '../utils/requests';
let CoaFetcher = {
/**
* Fetch one user from the API.
* If no arg is supplied, load the current user
*/
fetchOne(id) {
return requests.get('coa', {'id': id});
},
create(bitcoinId) {
console.log(bitcoinId);
return requests.post('coa_create', {body: {'bitcoin_id': bitcoinId}});
}
};
export default CoaFetcher;

View File

@ -0,0 +1,14 @@
'use strict';
import requests from '../utils/requests';
let LicenseFetcher = {
/**
* Fetch the available licenses from the API (might be bound to the subdomain e.g. cc.ascribe.io).
*/
fetch() {
return requests.get('licenses', {'subdomain': window.location.host.split('.')[0]});
}
};
export default LicenseFetcher;

17
js/fetchers/s3_fetcher.js Normal file
View File

@ -0,0 +1,17 @@
'use strict';
import requests from '../utils/requests';
let S3Fetcher = {
/**
* Fetch the registered applications of a user from the API.
*/
deleteFile(key, bucket) {
return requests.delete('delete_s3_file', {
key,
bucket
});
}
};
export default S3Fetcher;

View File

@ -0,0 +1,14 @@
'use strict';
import requests from '../utils/requests';
let WhitelabelFetcher = {
/**
* Fetch the custom whitelabel data from the API.
*/
fetch() {
return requests.get('whitelabel_settings', {'subdomain': window.location.host.split('.')[0]});
}
};
export default WhitelabelFetcher;

View File

@ -12,6 +12,8 @@ import SignupContainer from './components/signup_container';
import PasswordResetContainer from './components/password_reset_container';
import SettingsContainer from './components/settings_container';
import CoaVerifyContainer from './components/coa_verify_container';
import AppConstants from './constants/application_constants';
import RegisterPiece from './components/register_piece';
@ -28,6 +30,7 @@ let routes = (
<Route name="password_reset" path="password_reset" handler={PasswordResetContainer} />
<Route name="register_piece" path="register_piece" handler={RegisterPiece} />
<Route name="settings" path="settings" handler={SettingsContainer} />
<Route name="coa_verify" path="verify" handler={CoaVerifyContainer} />
<Redirect from={baseUrl} to="login" />
<Redirect from={baseUrl + '/'} to="login" />

18
js/stores/coa_store.js Normal file
View File

@ -0,0 +1,18 @@
'use strict';
import alt from '../alt';
import CoaActions from '../actions/coa_actions';
class CoaStore {
constructor() {
this.coa = {};
this.bindActions(CoaActions);
}
onUpdateCoa(coa) {
this.coa = coa;
}
}
export default alt.createStore(CoaStore, 'CoaStore');

View File

@ -0,0 +1,18 @@
'use strict';
import alt from '../alt';
import LicenseActions from '../actions/license_actions';
class LicenseStore {
constructor() {
this.licenses = {};
this.bindActions(LicenseActions);
}
onUpdateLicenses(licenses) {
this.licenses = licenses;
}
}
export default alt.createStore(LicenseStore, 'LicenseStore');

View File

@ -0,0 +1,18 @@
'use strict';
import alt from '../alt';
import WhitelabelActions from '../actions/whitelabel_actions';
class WhitelabelStore {
constructor() {
this.whitelabel = {};
this.bindActions(WhitelabelActions);
}
onUpdateWhitelabel(whitelabel) {
this.whitelabel = whitelabel;
}
}
export default alt.createStore(WhitelabelStore, 'WhitelabelStore');

View File

@ -69,4 +69,39 @@ export function getCookie(name) {
if (parts.length === 2) {
return parts.pop().split(';').shift();
}
}
/*
Given a url for an image, this method fetches it and returns a promise that resolves to
a blob object.
It can be used to create a 64base encoded data url.
Taken from: http://jsfiddle.net/jan_miksovsky/yy7zs/
*/
export function fetchImageAsBlob(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
// Ask for the result as an ArrayBuffer.
xhr.responseType = 'arraybuffer';
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status >= 400) {
reject(xhr.statusText);
}
};
xhr.onload = function() {
// Obtain a blob: URL for the image data.
let arrayBufferView = new Uint8Array(this.response);
let blob = new Blob([arrayBufferView], {type: 'image/jpeg'});
resolve(blob);
};
xhr.send();
});
}

View File

@ -100,4 +100,4 @@ function _mergeOptions(obj1, obj2){
}
}
return obj3;
}
}

View File

@ -27,6 +27,17 @@ class Requests {
return response.text();
}
customJSONparse(responseText) {
// If the responses' body does not contain any data,
// fetch will resolve responseText to the string 'None'.
// If this is the case, we can not try to parse it as JSON.
if(responseText !== 'None') {
return JSON.parse(responseText);
} else {
return {};
}
}
handleFatalError(err) {
this.fatalErrorHandler(err);
throw new ServerError(err);
@ -36,6 +47,7 @@ class Requests {
if (!json.success) {
let error = new APIError();
error.json = json;
console.error(new Error('The \'success\' property is missing in the server\'s response.'));
throw error;
}
return json;
@ -83,7 +95,7 @@ class Requests {
merged.method = verb;
return fetch(url, merged)
.then(this.unpackResponse)
.then(JSON.parse)
.then(this.customJSONparse)
.catch(this.handleFatalError.bind(this))
.then(this.handleAPIError);
}

View File

@ -35,7 +35,6 @@
"devDependencies": {
"babel-eslint": "^3.1.11",
"babel-jest": "^5.2.0",
"browserify-shim": "^3.8.9",
"jest-cli": "^0.4.0"
},
"dependencies": {
@ -44,6 +43,7 @@
"bootstrap-sass": "^3.3.4",
"browser-sync": "^2.7.5",
"browserify": "^9.0.8",
"browserify-shim": "^3.8.9",
"classnames": "^1.2.2",
"compression": "^1.4.4",
"envify": "^3.4.0",

View File

@ -21,12 +21,19 @@ $ascribe-accordion-list-font: 'Source Sans Pro';
height:100%;
// ToDo: Include media queries for thumbnail
.thumbnail-wrapper {
margin-left:0;
padding-left:0;
width: 110px;
height: 110px;
padding:0;
img {
display:block;
height: $ascribe-accordion-list-item-height;
max-width: 100%;
max-height: 100%;
}
&::before {
content: ' ';
display: inline-block;
vertical-align: middle; /* vertical alignment of the inline element */
height: 100%;
}
}
h1 {
margin-top: .3em;

View File

@ -28,4 +28,20 @@
width:100%;
margin-top: 1em;
}
.coa-file-wrapper{
display: table;
height: 200px;
overflow: hidden;
margin: 0 auto;
width: 100%;
padding: 1em;
}
.coa-file {
display: table-cell;
vertical-align: middle;
border: 1px solid #CCC;
background-color: #F8F8F8;
}

View File

@ -1,8 +1,9 @@
.ascribe-piece-list-bulk-modal {
position: fixed;
top:0;
width:1170px;
height:6em;
left: 3%;
width:94%;
background-color: #FAFAFA;
border-left: 0.1em solid #E0E0E0;
@ -12,6 +13,15 @@
border-bottom-right-radius: 5px;
border-bottom: 0.2em solid #E0E0E0;
z-index:1000;
padding-bottom: 1em;
}
@media(min-width:1174px){
.ascribe-piece-list-bulk-modal {
left: auto;
max-width: 1174px;
}
}
.piece-list-bulk-modal-clear-all {

View File

@ -1,64 +0,0 @@
.file-drag-and-drop {
display: table-cell;
outline: 1px dashed #616161;
cursor: pointer;
vertical-align: middle;
text-align: center;
height:208px;
width: 672px;
background-color: #FAFAFA;
transition: .1s linear background-color;
}
.file-drag-and-drop:hover {
background-color: rgba(72, 218, 203, 0.2);
}
.file-drag-and-drop > span {
font-size: 1.5em;
}
.has-files {
text-align: left;
padding: 3em 0 0 0;
}
.file-drag-and-drop-position {
display: inline-block;
margin: 0 0 3em 3em;
float:left;
}
.file-drag-and-drop-preview-table-wrapper {
display: table;
height:94px;
width:104px;
}
.file-drag-and-drop-preview {
overflow:hidden;
cursor: default;
background-color: #EEEEEE;
border: 1px solid #616161;
}
.file-drag-and-drop-preview-image {
display: table;
height:104px;
width:104px;
overflow:hidden;
border: 1px solid #616161;
}
.file-drag-and-drop-preview-other {
display: table-cell;
text-align: center;
vertical-align: middle;
}
.file-drag-and-drop-preview-other span {
font-size: 1.1em;
display: block;
margin-top: -10px;
}

View File

@ -68,12 +68,14 @@
padding-top: 1em;
padding-left: 1.5em;
padding-right: 1.5em;
cursor:pointer;
input, div, span, pre, textarea, select {
input, div, span:not(.glyphicon), pre, textarea, select {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
span {
font-weight: normal;
font-size: 0.9em;
@ -81,8 +83,8 @@
}
div {
margin-top: 10px;
div {
/* margin-top: 10px; */
div:not(.file-drag-and-drop div) {
padding-left: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: normal;
@ -92,6 +94,10 @@
}
}
.progressbar-container, .progressbar-progress {
margin-top: 0 !important;
}
input, pre, textarea, select {
font-weight: 400;
font-size: 1.1em;
@ -121,3 +127,9 @@
padding: 0;
}
}
.ascribe-property-footer{
font-size: 0.8em;
margin-top: 10px;
width: 100%;
}

View File

@ -5,7 +5,8 @@
}
.ascribe-textarea-editable:hover {
border: 1px solid #AAA;
//border: 1px solid #AAA;;
border: none;
}
.ascribe-pre{

5
sass/ascribe_theme.scss Normal file
View File

@ -0,0 +1,5 @@
/* All bootstrap overwrites should take place in this file */
.pager li a {
color: white;
}

View File

@ -1,13 +1,15 @@
.file-drag-and-drop {
display: table-cell;
display: block;
outline: 1px dashed #616161;
cursor: pointer;
vertical-align: middle;
text-align: center;
height:208px;
width: 672px;
height: auto;
background-color: #FAFAFA;
transition: .1s linear background-color;
overflow: auto;
margin-top: 1em;
padding: 3em;
}
.inactive-dropzone {
@ -22,25 +24,54 @@
background-color: rgba(72, 218, 203, 0.2);
}
.file-drag-and-drop > span {
font-size: 1.5em;
.file-drag-and-drop .file-drag-and-drop-dialog {
font-size: 1.25em !important;
margin-top: 1em;
&::before {
content: ' ';
display: inline-block;
vertical-align: middle; /* vertical alignment of the inline element */
height: 100%;
}
}
.has-files {
text-align: left;
padding: 3em 0 0 0;
padding: 4% 0 0 0;
}
.file-drag-and-drop-position {
position: relative;
display: inline-block;
margin: 0 0 3em 3em;
margin: 0 0 4% 4%;
float:left;
.delete-file {
display: block;
background-color: black;
width: 20px;
height: 20px;
position: absolute;
right: -7px;
top: -7px;
border-radius: 1em;
text-align: center;
cursor: pointer;
span {
color: white;
}
}
}
.file-drag-and-drop-preview-table-wrapper {
display: table;
height:94px;
width:104px;
height:64px;
width:74px;
}
.file-drag-and-drop-preview {
@ -52,10 +83,45 @@
.file-drag-and-drop-preview-image {
display: table;
height:104px;
width:104px;
height:74px;
width:74px;
overflow:hidden;
border: 1px solid #616161;
text-align: center;
}
.file-drag-and-drop-preview-image .action-file {
font-size: 2.5em;
margin-top: .3em;
color: white;
text-shadow: -2px 0 black, 0 2px black, 2px 0 black, 0 -2px black;
cursor: pointer;
&:link, &:visited, &:hover, &:active {
text-decoration: none;
}
&:hover {
color: #d9534f;
}
}
.file-drag-and-drop-preview-other .action-file {
position: relative;
top: .3em;
margin-top: 0;
font-size: 2.5em;
color: white;
text-shadow: -2px 0 black, 0 2px black, 2px 0 black, 0 -2px black;
cursor: pointer;
&:link, &:visited, &:hover, &:active {
text-decoration: none;
}
&:hover {
color: #d9534f;
}
}
.file-drag-and-drop-preview-other {
@ -65,8 +131,7 @@
}
.file-drag-and-drop-preview-other span {
font-size: 1.1em;
.file-drag-and-drop-preview-other span:not(:first-child) {
display: block;
margin-top: -10px;
margin-top: .5em;
}

View File

@ -1,3 +1,6 @@
$ascribe-color: rgba(2, 182, 163, 0.5);
$ascribe-color-dark: rgba(2, 182, 163, 0.8);
$ascribe-color-full: rgba(2, 182, 163, 1);
$ascribe-color-full: rgba(2, 182, 163, 1);
$ascribe-brand-danger: #FC535F;
$ascribe-brand-warning: #FFC354;

View File

@ -3,10 +3,11 @@
$BASE_URL: '<%= BASE_URL %>';
@import 'variables';
@import 'ascribe_variables';
@import 'variables';
@import '../node_modules/bootstrap-sass/assets/stylesheets/bootstrap';
@import '../node_modules/react-datepicker/dist/react-datepicker';
@import 'ascribe_theme';
@import './ascribe-fonts/style';
@import './ascribe-fonts/ascribe-fonts';
@import 'ascribe_login';
@ -22,7 +23,6 @@ $BASE_URL: '<%= BASE_URL %>';
@import 'ascribe_piece_register';
@import 'offset_right';
@import 'ascribe_settings';
@import 'ascribe_react_s3_fineuploader';
body {
background-color: #FDFDFD;
@ -33,6 +33,12 @@ body {
display: none;
}
.no-margin{
margin: 0;
}
.no-padding{
padding: 0;
}
.navbar-default {
border: none;
border-left:0;
@ -55,6 +61,20 @@ body {
color: $ascribe-color;
}
.img-brand{
height: 25px;
}
.ascribe-subheader{
padding-bottom: 10px;
margin-top: -10px;
a {
cursor: pointer;
font-size: 0.8em;
color: #222;
}
}
.tooltip-inner{
max-width: 300px;
padding: 3px 8px;
@ -222,3 +242,8 @@ body {
.col-bottom {
vertical-align: bottom;
}
.ascribe-button-list button {
margin-right: 1px;
margin-top: 1px;
}

View File

@ -18,8 +18,8 @@ $gray-lighter: lighten($gray-base, 93.5%) !default; // #eee
$brand-primary: darken(#428bca, 6.5%) !default; // #337ab7
$brand-success: #5cb85c !default;
$brand-info: #5bc0de !default;
$brand-warning: #f0ad4e !default;
$brand-danger: #d9534f !default;
$brand-warning: $ascribe-brand-warning !default;
$brand-danger: $ascribe-brand-danger !default;
//== Scaffolding
@ -107,9 +107,9 @@ $padding-xs-horizontal: 5px !default;
$line-height-large: 1.3333333 !default; // extra decimals for Win 8.1 Chrome
$line-height-small: 1.5 !default;
$border-radius-base: 4px !default;
$border-radius-large: 6px !default;
$border-radius-small: 3px !default;
$border-radius-base: 0 !default;
$border-radius-large: 0 !default;
$border-radius-small: 0 !default;
//** Global color for active items (e.g., navs or dropdowns).
$component-active-color: #fff !default;
@ -149,9 +149,9 @@ $table-border-color: #ddd !default;
$btn-font-weight: normal !default;
$btn-default-color: #333 !default;
$btn-default-bg: #fff !default;
$btn-default-border: #ccc !default;
$btn-default-color: white !default;
$btn-default-bg: $ascribe-color-full !default;
$btn-default-border: $ascribe-color-full !default;
$btn-primary-color: #fff !default;
$btn-primary-bg: $brand-primary !default;
@ -171,7 +171,7 @@ $btn-warning-border: darken($btn-warning-bg, 5%) !default;
$btn-danger-color: #fff !default;
$btn-danger-bg: $brand-danger !default;
$btn-danger-border: darken($btn-danger-bg, 5%) !default;
$btn-danger-border: $brand-danger !default;
$btn-link-disabled-color: $gray-light !default;
@ -186,7 +186,7 @@ $input-bg: #fff !default;
$input-bg-disabled: $gray-lighter !default;
//** Text color for `<input>`s
$input-color: $gray !default;
$input-color: white !default;
//** `<input>` border color
$input-border: #ccc !default;
@ -219,9 +219,9 @@ $legend-color: $gray-dark !default;
$legend-border-color: #e5e5e5 !default;
//** Background color for textual input addons
$input-group-addon-bg: $gray-lighter !default;
$input-group-addon-bg: $ascribe-color-full !default;
//** Border color for textual input addons
$input-group-addon-border-color: $input-border !default;
$input-group-addon-border-color: $ascribe-color-full !default;
//** Disabled cursor for form controls and buttons.
$cursor-disabled: not-allowed !default;
@ -468,16 +468,16 @@ $pagination-disabled-border: #ddd !default;
//
//##
$pager-bg: $pagination-bg !default;
$pager-border: $pagination-border !default;
$pager-border-radius: 15px !default;
$pager-bg: $ascribe-color-full !default;
$pager-border: $ascribe-color-full !default;
$pager-border-radius: 0 !default;
$pager-hover-bg: $pagination-hover-bg !default;
$pager-hover-bg: darken($ascribe-color-full, 10%) !default;
$pager-active-bg: $pagination-active-bg !default;
$pager-active-color: $pagination-active-color !default;
$pager-active-bg: $ascribe-color-full !default;
$pager-active-color: $ascribe-color-full !default;
$pager-disabled-color: $pagination-disabled-color !default;
$pager-disabled-color: lighten($ascribe-color-full, 10%) !default;
//== Jumbotron