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:
commit
16c8df2f8d
@ -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
34
js/actions/coa_actions.js
Normal 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);
|
25
js/actions/license_actions.js
Normal file
25
js/actions/license_actions.js
Normal 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);
|
25
js/actions/whitelabel_actions.js
Normal file
25
js/actions/whitelabel_actions.js
Normal 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);
|
@ -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'}>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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) => {
|
||||
|
@ -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 }
|
||||
|
@ -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"
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
|
||||
{/*<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>
|
||||
|
@ -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 : '';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
101
js/components/coa_verify_container.js
Normal file
101
js/components/coa_verify_container.js
Normal 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;
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -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? Sign up...<br/>
|
||||
Forgot my password? Rescue me...
|
||||
Not an ascribe user? <Link to="signup">Sign up...</Link><br/>
|
||||
Forgot my password? <Link to="password_reset">Rescue me...</Link>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
@ -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;
|
@ -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
|
||||
|
@ -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}/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -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 />
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
19
js/fetchers/coa_fetcher.js
Normal file
19
js/fetchers/coa_fetcher.js
Normal 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;
|
14
js/fetchers/license_fetcher.js
Normal file
14
js/fetchers/license_fetcher.js
Normal 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
17
js/fetchers/s3_fetcher.js
Normal 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;
|
14
js/fetchers/whitelabel_fetcher.js
Normal file
14
js/fetchers/whitelabel_fetcher.js
Normal 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;
|
@ -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
18
js/stores/coa_store.js
Normal 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');
|
18
js/stores/license_store.js
Normal file
18
js/stores/license_store.js
Normal 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');
|
18
js/stores/whitelabel_store.js
Normal file
18
js/stores/whitelabel_store.js
Normal 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');
|
@ -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();
|
||||
});
|
||||
}
|
@ -100,4 +100,4 @@ function _mergeOptions(obj1, obj2){
|
||||
}
|
||||
}
|
||||
return obj3;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
@ -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%;
|
||||
}
|
@ -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
5
sass/ascribe_theme.scss
Normal file
@ -0,0 +1,5 @@
|
||||
/* All bootstrap overwrites should take place in this file */
|
||||
|
||||
.pager li a {
|
||||
color: white;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user