1
0
mirror of https://github.com/ascribe/onion.git synced 2024-06-25 18:56:28 +02:00

started using favicon setter package browser icons there, ikono icon will change

This commit is contained in:
Cevo 2015-10-05 11:09:45 +02:00
commit f033a878d0
155 changed files with 6133 additions and 2399 deletions

224
.scss-lint.yml Normal file
View File

@ -0,0 +1,224 @@
linters:
BangFormat:
enabled: true
space_before_bang: true
space_after_bang: false
BemDepth:
enabled: false
max_elements: 1
BorderZero:
enabled: true
convention: zero # or `none`
ColorKeyword:
enabled: true
ColorVariable:
enabled: true
Comment:
enabled: true
DebugStatement:
enabled: true
DeclarationOrder:
enabled: true
DisableLinterReason:
enabled: false
DuplicateProperty:
enabled: true
ElsePlacement:
enabled: true
style: same_line # or 'new_line'
EmptyLineBetweenBlocks:
enabled: true
ignore_single_line_blocks: true
EmptyRule:
enabled: true
ExtendDirective:
enabled: false
FinalNewline:
enabled: false
present: true
HexLength:
enabled: true
style: short # or 'long'
HexNotation:
enabled: true
style: lowercase # or 'uppercase'
HexValidation:
enabled: true
IdSelector:
enabled: true
ImportantRule:
enabled: true
ImportPath:
enabled: true
leading_underscore: false
filename_extension: false
Indentation:
enabled: true
allow_non_nested_indentation: false
character: space # or 'tab'
width: 4
LeadingZero:
enabled: true
style: exclude_zero # or 'include_zero'
MergeableSelector:
enabled: true
force_nesting: true
NameFormat:
enabled: true
allow_leading_underscore: true
convention: hyphenated_lowercase # or 'camel_case', or 'snake_case', or a regex pattern
NestingDepth:
enabled: true
max_depth: 3
ignore_parent_selectors: false
PlaceholderInExtend:
enabled: true
PropertyCount:
enabled: false
include_nested: false
max_properties: 10
PropertySortOrder:
enabled: false
ignore_unspecified: false
min_properties: 2
separate_groups: false
PropertySpelling:
enabled: true
extra_properties: []
PropertyUnits:
enabled: true
global: [
'ch', 'em', 'ex', 'rem', # Font-relative lengths
'cm', 'in', 'mm', 'pc', 'pt', 'px', 'q', # Absolute lengths
'vh', 'vw', 'vmin', 'vmax', # Viewport-percentage lengths
'deg', 'grad', 'rad', 'turn', # Angle
'ms', 's', # Duration
'Hz', 'kHz', # Frequency
'dpi', 'dpcm', 'dppx', # Resolution
'%'] # Other
properties: {}
QualifyingElement:
enabled: true
allow_element_with_attribute: false
allow_element_with_class: false
allow_element_with_id: false
SelectorDepth:
enabled: true
max_depth: 3
SelectorFormat:
enabled: true
convention: hyphenated_lowercase # or 'strict_BEM', or 'hyphenated_BEM', or 'snake_case', or 'camel_case', or a regex pattern
Shorthand:
enabled: true
allowed_shorthands: [1, 2, 3]
SingleLinePerProperty:
enabled: true
allow_single_line_rule_sets: true
SingleLinePerSelector:
enabled: true
SpaceAfterComma:
enabled: true
SpaceAfterPropertyColon:
enabled: true
style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned'
SpaceAfterPropertyName:
enabled: true
SpaceAfterVariableName:
enabled: true
SpaceAroundOperator:
enabled: true
style: one_space # or 'no_space'
SpaceBeforeBrace:
enabled: true
style: space # or 'new_line'
allow_single_line_padding: false
SpaceBetweenParens:
enabled: true
spaces: 0
StringQuotes:
enabled: true
style: single_quotes # or double_quotes
TrailingSemicolon:
enabled: true
TrailingWhitespace:
enabled: true
TrailingZero:
enabled: false
TransitionAll:
enabled: false
UnnecessaryMantissa:
enabled: true
UnnecessaryParentReference:
enabled: true
UrlFormat:
enabled: true
UrlQuotes:
enabled: true
VariableForProperty:
enabled: false
properties: []
VendorPrefix:
enabled: false
identifier_list: base
additional_identifiers: []
excluded_identifiers: []
ZeroUnit:
enabled: true
Compass::*:
enabled: false

View File

@ -32,8 +32,8 @@ Additionally, to work on the white labeling functionality, you need to edit your
```
Code Conventions
================
JavaScript Code Conventions
===========================
For this project, we're using:
* 4 Spaces
@ -42,6 +42,15 @@ For this project, we're using:
* We don't use camel case for file naming but in everything Javascript related
* We use `let` instead of `var`: [SA Post](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword)
SCSS Code Conventions
=====================
Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor.
Some interesting links:
* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom)
Testing
===============
We're using Facebook's jest to do testing as it integrates nicely with react.js as well.
@ -127,4 +136,4 @@ Moar stuff
- [24ways.org: JavaScript Modules the ES6 Way](http://24ways.org/2014/javascript-modules-the-es6-way/)
- [Babel: Learn ES6](https://babeljs.io/docs/learn-es6/)
- [egghead's awesome reactjs and flux tutorials](https://egghead.io/)
- [Crockford's genious Javascript: The Good Parts (Tim has a copy)](http://www.amazon.de/JavaScript-Parts-Working-Shallow-Grain/dp/0596517742)
- [Crockford's genious Javascript: The Good Parts (Tim has a copy)](http://www.amazon.de/JavaScript-Parts-Working-Shallow-Grain/dp/0596517742)

View File

@ -0,0 +1,113 @@
'use strict';
import alt from '../alt';
import Q from 'q';
import OwnershipFetcher from '../fetchers/ownership_fetcher';
import ContractListActions from './contract_list_actions';
class ContractAgreementListActions {
constructor() {
this.generateActions(
'updateContractAgreementList',
'flushContractAgreementList'
);
}
fetchContractAgreementList(issuer, accepted, pending) {
this.actions.updateContractAgreementList(null);
return Q.Promise((resolve, reject) => {
OwnershipFetcher.fetchContractAgreementList(issuer, accepted, pending)
.then((contractAgreementList) => {
if (contractAgreementList.count > 0) {
this.actions.updateContractAgreementList(contractAgreementList.results);
resolve(contractAgreementList.results);
}
else{
resolve(null);
}
})
.catch((err) => {
console.logGlobal(err);
reject(err);
});
}
);
}
fetchAvailableContractAgreementList(issuer, createContractAgreement) {
return Q.Promise((resolve, reject) => {
OwnershipFetcher.fetchContractAgreementList(issuer, true, null)
.then((acceptedContractAgreementList) => {
// if there is at least an accepted contract agreement, we're going to
// use it
if(acceptedContractAgreementList.count > 0) {
this.actions.updateContractAgreementList(acceptedContractAgreementList.results);
} else {
// otherwise, we're looking for contract agreements that are still pending
//
// Normally nesting promises, but for this conditional one, it makes sense to not
// overcomplicate the method
OwnershipFetcher.fetchContractAgreementList(issuer, null, true)
.then((pendingContractAgreementList) => {
if(pendingContractAgreementList.count > 0) {
this.actions.updateContractAgreementList(pendingContractAgreementList.results);
} else {
// if there was neither a pending nor an active contractAgreement
// found and createContractAgreement is set to true, we create a
// new contract agreement
if(createContractAgreement) {
this.actions.createContractAgreementFromPublicContract(issuer);
}
}
})
.catch((err) => {
console.logGlobal(err);
reject(err);
});
}
})
.catch((err) => {
console.logGlobal(err);
reject(err);
});
}
);
}
createContractAgreementFromPublicContract(issuer) {
ContractListActions.fetchContractList(null, null, issuer)
.then((publicContract) => {
// create an agreement with the public contract if there is one
if (publicContract && publicContract.length > 0) {
return this.actions.createContractAgreement(null, publicContract[0]);
}
else {
/*
contractAgreementList in the store is already set to null;
*/
}
}).then((publicContracAgreement) => {
if (publicContracAgreement) {
this.actions.updateContractAgreementList([publicContracAgreement]);
}
}).catch((err) => {
console.logGlobal(err);
});
}
createContractAgreement(issuer, contract){
return Q.Promise((resolve, reject) => {
OwnershipFetcher.createContractAgreement(issuer, contract).then(
(contractAgreement) => {
resolve(contractAgreement);
}
).catch((err) => {
console.logGlobal(err);
reject(err);
});
});
}
}
export default alt.createActions(ContractAgreementListActions);

View File

@ -0,0 +1,58 @@
'use strict';
import alt from '../alt';
import OwnershipFetcher from '../fetchers/ownership_fetcher';
import Q from 'q';
class ContractListActions {
constructor() {
this.generateActions(
'updateContractList',
'flushContractList'
);
}
fetchContractList(isActive, isPublic, issuer) {
return Q.Promise((resolve, reject) => {
OwnershipFetcher.fetchContractList(isActive, isPublic, issuer)
.then((contracts) => {
this.actions.updateContractList(contracts.results);
resolve(contracts.results);
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateContractList([]);
reject(err);
});
});
}
changeContract(contract){
return Q.Promise((resolve, reject) => {
OwnershipFetcher.changeContract(contract)
.then((res) => {
resolve(res);
})
.catch((err)=> {
console.logGlobal(err);
reject(err);
});
});
}
removeContract(contractId){
return Q.Promise( (resolve, reject) => {
OwnershipFetcher.deleteContract(contractId)
.then((res) => {
resolve(res);
})
.catch( (err) => {
console.logGlobal(err);
reject(err);
});
});
}
}
export default alt.createActions(ContractListActions);

View File

@ -1,48 +0,0 @@
'use strict';
import alt from '../alt';
import OwnershipFetcher from '../fetchers/ownership_fetcher';
class LoanContractActions {
constructor() {
this.generateActions(
'updateLoanContract',
'flushLoanContract'
);
}
fetchLoanContract(email) {
if(email.match(/.+\@.+\..+/)) {
OwnershipFetcher.fetchLoanContract(email)
.then((contracts) => {
if (contracts && contracts.length > 0) {
this.actions.updateLoanContract({
contractKey: contracts[0].s3Key,
contractUrl: contracts[0].s3Url,
contractEmail: email
});
}
else {
this.actions.updateLoanContract({
contractKey: null,
contractUrl: null,
contractEmail: null
});
}
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateLoanContract({
contractKey: null,
contractUrl: null,
contractEmail: null
});
});
} else {
/* No email was entered - Ignore and keep going*/
}
}
}
export default alt.createActions(LoanContractActions);

View File

@ -1,27 +0,0 @@
'use strict';
import alt from '../alt';
import OwnershipFetcher from '../fetchers/ownership_fetcher';
class LoanContractListActions {
constructor() {
this.generateActions(
'updateLoanContractList',
'flushLoanContractList'
);
}
fetchLoanContractList() {
OwnershipFetcher.fetchLoanContractList()
.then((contracts) => {
this.actions.updateLoanContractList(contracts);
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateLoanContractList([]);
});
}
}
export default alt.createActions(LoanContractListActions);

View File

@ -0,0 +1,68 @@
'use strict';
import alt from '../alt';
import Q from 'q';
import NotificationFetcher from '../fetchers/notification_fetcher';
class NotificationActions {
constructor() {
this.generateActions(
'updatePieceListNotifications',
'updateEditionListNotifications',
'updateEditionNotifications',
'updatePieceNotifications',
'updateContractAgreementListNotifications'
);
}
fetchPieceListNotifications() {
NotificationFetcher
.fetchPieceListNotifications()
.then((res) => {
this.actions.updatePieceListNotifications(res);
})
.catch((err) => console.logGlobal(err));
}
fetchPieceNotifications(pieceId) {
NotificationFetcher
.fetchPieceNotifications(pieceId)
.then((res) => {
this.actions.updatePieceNotifications(res);
})
.catch((err) => console.logGlobal(err));
}
fetchEditionListNotifications() {
NotificationFetcher
.fetchEditionListNotifications()
.then((res) => {
this.actions.updateEditionListNotifications(res);
})
.catch((err) => console.logGlobal(err));
}
fetchEditionNotifications(editionId) {
NotificationFetcher
.fetchEditionNotifications(editionId)
.then((res) => {
this.actions.updateEditionNotifications(res);
})
.catch((err) => console.logGlobal(err));
}
fetchContractAgreementListNotifications() {
return Q.Promise((resolve, reject) => {
NotificationFetcher
.fetchContractAgreementListNotifications()
.then((res) => {
this.actions.updateContractAgreementListNotifications(res);
resolve(res);
})
.catch((err) => console.logGlobal(err));
});
}
}
export default alt.createActions(NotificationActions);

View File

@ -57,7 +57,7 @@ class PieceListActions {
PieceListFetcher
.fetchRequestActions()
.then((res) => {
this.actions.updatePieceListRequestActions(res.piece_ids);
this.actions.updatePieceListRequestActions(res);
})
.catch((err) => console.logGlobal(err));
}

View File

@ -26,6 +26,7 @@ import EventActions from './actions/event_actions';
import GoogleAnalyticsHandler from './third_party/ga';
import RavenHandler from './third_party/raven';
import IntercomHandler from './third_party/intercom';
import NotificationsHandler from './third_party/notifications';
/* eslint-enable */
initLogging();
@ -71,8 +72,10 @@ class AppGateway {
subdomain = settings.subdomain;
}
window.document.body.classList.add('client--' + subdomain);
EventActions.applicationWillBoot(settings);
Router.run(getRoutes(type, subdomain), Router.HistoryLocation, (App) => {
window.appRouter = Router.run(getRoutes(type, subdomain), Router.HistoryLocation, (App) => {
React.render(
<App />,
document.getElementById('main')

View File

@ -20,21 +20,28 @@ let AclProxy = React.createClass({
show: React.PropTypes.bool
},
render() {
if(this.props.show) {
getChildren() {
if (React.Children.count(this.props.children) > 1){
/*
This might ruin styles for header items in the navbar etc
*/
return (
<span>
{this.props.children}
</span>
);
}
/* can only do this when there is only 1 child, but will preserve styles */
return this.props.children;
},
render() {
if(this.props.show) {
return this.getChildren();
} else {
if(this.props.aclObject) {
if(this.props.aclObject[this.props.aclName]) {
return (
<span>
{this.props.children}
</span>
);
return this.getChildren();
} else {
/* if(typeof this.props.aclObject[this.props.aclName] === 'undefined') {
console.warn('The aclName you\'re filtering for was not present (or undefined) in the aclObject.');

View File

@ -21,7 +21,7 @@ let AccordionList = React.createClass({
);
} else if(this.props.count === 0) {
return (
<div>
<div className="ascribe-accordion-list-placeholder">
<p className="text-center">{getLangText('We could not find any works related to you...')}</p>
<p className="text-center">{getLangText('To register one, click')} <a href="register_piece">{getLangText('here')}</a>!</p>
</div>

View File

@ -6,7 +6,6 @@ import classNames from 'classnames';
import EditionListActions from '../../actions/edition_list_actions';
import EditionListStore from '../../stores/edition_list_store';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import Button from 'react-bootstrap/lib/Button';
@ -16,6 +15,7 @@ import CreateEditionsButton from '../ascribe_buttons/create_editions_button';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
let AccordionListItemEditionWidget = React.createClass({
propTypes: {
className: React.PropTypes.string,

View File

@ -160,7 +160,7 @@ let AccordionListItemTableEditions = React.createClass({
let content = item.acl;
return {
'content': content,
'requestAction': item.request_action
'notifications': item.notifications
}; },
'acl',
getLangText('Actions'),

View File

@ -61,12 +61,12 @@ let AccordionListItemWallet = React.createClass({
},
getGlyphicon(){
if (this.props.content.requestAction && this.props.content.requestAction.length > 0) {
if ((this.props.content.notifications && this.props.content.notifications.length > 0)){
return (
<OverlayTrigger
delay={500}
placement="left"
overlay={<Tooltip>{getLangText('You have actions pending in one of your editions')}</Tooltip>}>
overlay={<Tooltip>{getLangText('You have actions pending')}</Tooltip>}>
<Glyphicon glyph='bell' color="green"/>
</OverlayTrigger>);
}

View File

@ -162,21 +162,24 @@ let AclButton = React.createClass({
},
render() {
let shouldDisplay = this.props.availableAcls[this.props.action];
let aclProps = this.actionProperties();
let buttonClassName = this.props.buttonAcceptClassName ? this.props.buttonAcceptClassName : '';
return (
<ModalWrapper
trigger={
<button className={shouldDisplay ? 'btn btn-default btn-sm ' + buttonClassName : 'hidden'}>
{this.sanitizeAction()}
</button>
}
handleSuccess={aclProps.handleSuccess}
title={aclProps.title}>
{aclProps.form}
</ModalWrapper>
);
if (this.props.availableAcls){
let shouldDisplay = this.props.availableAcls[this.props.action];
let aclProps = this.actionProperties();
let buttonClassName = this.props.buttonAcceptClassName ? this.props.buttonAcceptClassName : '';
return (
<ModalWrapper
trigger={
<button className={shouldDisplay ? 'btn btn-default btn-sm ' + buttonClassName : 'hidden'}>
{this.sanitizeAction()}
</button>
}
handleSuccess={aclProps.handleSuccess}
title={aclProps.title}>
{aclProps.form}
</ModalWrapper>
);
}
return null;
}
});

View File

@ -17,7 +17,7 @@ const CollapsibleParagraph = React.createClass({
getDefaultProps() {
return {
show: false
show: true
};
},
@ -38,14 +38,14 @@ const CollapsibleParagraph = React.createClass({
if(this.props.show) {
return (
<div className="ascribe-detail-header">
<div className="ascribe-edition-collapsible-wrapper">
<div className="ascribe-collapsible-wrapper">
<div onClick={this.handleToggle}>
<span>{text} {this.props.title}</span>
</div>
<Panel
collapsible
expanded={this.state.expanded}
className="ascribe-edition-collapsible-content">
className="ascribe-collapsible-content">
{this.props.children}
</Panel>
</div>

View File

@ -25,7 +25,7 @@ import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph
import Form from './../ascribe_forms/form';
import Property from './../ascribe_forms/property';
import EditionDetailProperty from './detail_property';
import LicenseDetail from './license_detail';
import EditionFurtherDetails from './further_details';
import ListRequestActions from './../ascribe_forms/list_form_request_actions';
@ -88,10 +88,8 @@ let Edition = React.createClass({
},
handleDeleteSuccess(response) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
this.refreshCollection();
EditionListActions.refreshEditionList({pieceId: this.props.edition.parent});
EditionListActions.closeAllEditionLists();
EditionListActions.clearAllEditionSelections();
@ -101,6 +99,12 @@ let Edition = React.createClass({
this.transitionTo('pieces');
},
refreshCollection() {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
EditionListActions.refreshEditionList({pieceId: this.props.edition.parent});
},
render() {
return (
<Row>
@ -118,6 +122,7 @@ let Edition = React.createClass({
</div>
<EditionSummary
handleSuccess={this.props.loadEdition}
refreshCollection={this.refreshCollection}
currentUser={this.state.currentUser}
edition={this.props.edition}
handleDeleteSuccess={this.handleDeleteSuccess}/>
@ -152,8 +157,9 @@ let Edition = React.createClass({
<CollapsibleParagraph
title="Notes"
show={(this.state.currentUser.username && true || false) ||
(this.props.edition.acl.acl_edit || this.props.edition.public_note)}>
show={!!(this.state.currentUser.username
|| this.props.edition.acl.acl_edit
|| this.props.edition.public_note)}>
<Note
id={() => {return {'bitcoin_id': this.props.edition.bitcoin_id}; }}
label={getLangText('Personal note (private)')}
@ -205,14 +211,22 @@ let EditionSummary = React.createClass({
edition: React.PropTypes.object,
handleSuccess: React.PropTypes.func,
currentUser: React.PropTypes.object,
handleDeleteSuccess: React.PropTypes.func
handleDeleteSuccess: React.PropTypes.func,
refreshCollection: React.PropTypes.func
},
getTransferWithdrawData(){
return {'bitcoin_id': this.props.edition.bitcoin_id};
},
handleSuccess() {
this.props.refreshCollection();
this.props.handleSuccess();
},
showNotification(response){
this.props.handleSuccess();
if (response){
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
@ -234,13 +248,15 @@ let EditionSummary = React.createClass({
getActions(){
let actions = null;
if (this.props.edition.request_action && this.props.edition.request_action.length > 0){
if (this.props.edition &&
this.props.edition.notifications &&
this.props.edition.notifications.length > 0){
actions = (
<ListRequestActions
pieceOrEditions={[this.props.edition]}
currentUser={this.props.currentUser}
handleSuccess={this.showNotification}
requestActions={this.props.edition.request_action}/>);
notifications={this.props.edition.notifications}/>);
}
else {
@ -275,7 +291,7 @@ let EditionSummary = React.createClass({
className="text-center ascribe-button-list"
availableAcls={this.props.edition.acl}
editions={[this.props.edition]}
handleSuccess={this.props.handleSuccess}>
handleSuccess={this.handleSuccess}>
{withdrawButton}
<DeleteButton
handleSuccess={this.props.handleDeleteSuccess}
@ -300,12 +316,12 @@ let EditionSummary = React.createClass({
<EditionDetailProperty
label={getLangText('OWNER')}
value={ this.props.edition.owner } />
<LicenseDetail license={this.props.edition.license_type}/>
{this.getStatus()}
{this.getActions()}
<hr/>
</div>
);
}
});

View File

@ -9,6 +9,8 @@ import Edition from './edition';
import AppConstants from '../../constants/application_constants';
/**
* This is the component that implements resource/data specific functionality
*/
@ -34,6 +36,15 @@ let EditionContainer = React.createClass({
EditionActions.fetchOne(this.props.params.editionId);
},
// This is done to update the container when the user clicks on the prev or next
// button to update the URL parameter (and therefore to switch pieces)
componentWillReceiveProps(nextProps) {
if(this.props.params.editionId !== nextProps.params.editionId) {
EditionActions.updateEdition({});
EditionActions.fetchOne(nextProps.params.editionId);
}
},
componentWillUnmount() {
// Every time we're leaving the edition detail page,
// just reset the edition that is saved in the edition store

View File

@ -15,7 +15,7 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import FurtherDetailsFileuploader from './further_details_fileuploader';
import { isReadyForFormSubmission } from '../ascribe_uploader/react_s3_fine_uploader_utils';
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
let FurtherDetails = React.createClass({
propTypes: {
@ -38,9 +38,9 @@ let FurtherDetails = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification);
},
submitKey(key){
submitFile(file){
this.setState({
otherDataKey: key
otherDataKey: file.key
});
},
@ -78,9 +78,9 @@ let FurtherDetails = React.createClass({
extraData={this.props.extraData} />
<Form>
<FurtherDetailsFileuploader
submitKey={this.submitKey}
submitFile={this.submitFile}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={isReadyForFormSubmission}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
editable={this.props.editable}
overrideForm={true}
pieceId={this.props.pieceId}

View File

@ -17,7 +17,7 @@ let FurtherDetailsFileuploader = React.createClass({
pieceId: React.PropTypes.number,
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
setIsUploadReady: React.PropTypes.func,
submitKey: React.PropTypes.func,
submitFile: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
editable: React.PropTypes.bool,
multiple: React.PropTypes.bool
@ -55,11 +55,8 @@ let FurtherDetailsFileuploader = React.createClass({
url: ApiUrls.blob_otherdatas,
pieceId: this.props.pieceId
}}
validation={{
itemLimit: 100000,
sizeLimit: '50000000'
}}
submitKey={this.props.submitKey}
validation={AppConstants.fineUploader.validation.additionalData}
submitFile={this.props.submitFile}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
session={{

View File

@ -0,0 +1,31 @@
'use strict';
import React from 'react';
import DetailProperty from './detail_property';
/**
* This is the component that implements display-specific functionality
*/
let LicenseDetail = React.createClass({
propTypes: {
license: React.PropTypes.object
},
render () {
if (this.props.license.code === 'default') {
return null;
}
return (
<DetailProperty
label="LICENSE"
value={
<a href={this.props.license.url} target="_blank">
{ this.props.license.code.toUpperCase() + ': ' + this.props.license.name}
</a>
}
/>
);
}
});
export default LicenseDetail;

View File

@ -39,19 +39,18 @@ let Note = React.createClass({
},
render() {
if (!!this.props.currentUser.username && this.props.show) {
if ((!!this.props.currentUser.username && this.props.editable || !this.props.editable ) && this.props.show) {
return (
<Form
url={this.props.url}
getFormData={this.props.id}
handleSuccess={this.showNotification}>
handleSuccess={this.showNotification}
disabled={!this.props.editable}>
<Property
name='note'
label={this.props.label}
editable={this.props.editable}>
label={this.props.label}>
<InputTextAreaToggable
rows={1}
editable={this.props.editable}
defaultValue={this.props.defaultValue}
placeholder={this.props.placeholder}/>
</Property>
@ -63,4 +62,4 @@ let Note = React.createClass({
}
});
export default Note
export default Note;

View File

@ -19,6 +19,7 @@ import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph
import FurtherDetails from './further_details';
import DetailProperty from './detail_property';
import LicenseDetail from './license_detail';
import HistoryIterator from './history_iterator';
import AclButtonList from './../ascribe_buttons/acl_button_list';
@ -172,17 +173,16 @@ let PieceContainer = React.createClass({
return {'id': this.state.piece.id};
},
getActions(){
getActions() {
if (this.state.piece &&
this.state.piece.request_action &&
this.state.piece.request_action.length > 0) {
this.state.piece.notifications &&
this.state.piece.notifications.length > 0) {
return (
<ListRequestActions
pieceOrEditions={this.state.piece}
currentUser={this.state.currentUser}
handleSuccess={this.loadPiece}
requestActions={this.state.piece.request_action}/>
);
notifications={this.state.piece.notifications}/>);
}
else {
return (
@ -225,6 +225,7 @@ let PieceContainer = React.createClass({
<div className="ascribe-detail-header">
<DetailProperty label={getLangText('REGISTREE')} value={ this.state.piece.user_registered } />
<DetailProperty label={getLangText('ID')} value={ this.state.piece.bitcoin_id } ellipsis={true} />
<LicenseDetail license={this.state.piece.license_type} />
</div>
}
buttons={this.getActions()}>
@ -238,12 +239,11 @@ let PieceContainer = React.createClass({
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('Notes')}
show={(this.state.currentUser.username && true || false) ||
(this.state.piece.public_note)}>
show={!!(this.state.currentUser.username || this.state.piece.public_note)}>
<Note
id={this.getId}
label={getLangText('Personal note (private)')}
defaultValue={this.state.piece.private_note ? this.state.piece.private_note : null}
defaultValue={this.state.piece.private_note || null}
placeholder={getLangText('Enter your comments ...')}
editable={true}
successMessage={getLangText('Private note saved')}

View File

@ -1,141 +0,0 @@
'use strict';
import React from 'react';
import LoanContractListActions from '../../actions/loan_contract_list_actions';
import LoanContractListStore from '../../stores/loan_contract_list_store';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import Form from './form';
import Property from './property';
import PropertyCollapsible from './property_collapsible';
import InputTextAreaToggable from './input_textarea_toggable';
import ApiUrls from '../../constants/api_urls';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
let ContractForm = React.createClass({
propTypes: {
handleSuccess: React.PropTypes.func
},
getInitialState() {
return mergeOptions(
LoanContractListStore.getState(),
{
selectedContract: 0
}
);
},
componentDidMount() {
LoanContractListStore.listen(this.onChange);
LoanContractListActions.fetchLoanContractList();
},
componentWillUnmount() {
LoanContractListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
onContractChange(event){
this.setState({selectedContract: event.target.selectedIndex});
},
handleSubmitSuccess(response) {
let notification = new GlobalNotificationModel(response.notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getContracts() {
if (this.state.contractList && this.state.contractList.length > 0) {
return (
<Property
name='contract'
label={getLangText('Contract Type')}
onChange={this.onContractChange}
footer={
<a
className="pull-right"
href={this.state.contractList[this.state.selectedContract].s3UrlSafe}
target="_blank">
{getLangText('Learn more')}
</a>
}>
<select name="contract">
{this.state.contractList.map((contract, i) => {
return (
<option
name={i}
key={i}
value={ contract.name }>
{ contract.name }
</option>
);
})}
</select>
</Property>);
}
return null;
},
render() {
return (
<Form
className="ascribe-form-bordered ascribe-form-wrapper"
ref='form'
url={ApiUrls.ownership_loans_contract}
handleSuccess={this.props.handleSuccess}
buttons={<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
{getLangText('Send loan request')}
</button>}
spinner={
<span 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" />
</span>
}>
<div className="ascribe-form-header">
<h3>{getLangText('Contract form')}</h3>
</div>
<Property
name='artist_name'
label={getLangText('Artist Name')}>
<input
type="text"
placeholder={getLangText('(e.g. Andy Warhol)')}
required/>
</Property>
<Property
name='artist_email'
label={getLangText('Artist Email')}>
<input
type="email"
placeholder={getLangText('(e.g. andy@warhol.co.uk)')}
required/>
</Property>
{this.getContracts()}
<PropertyCollapsible
name='appendix'
checkboxLabel={getLangText('Add appendix to the contract')}>
<span>{getLangText('Appendix')}</span>
<InputTextAreaToggable
rows={1}
editable={true}
placeholder={getLangText('This will be appended to the contract selected above')}/>
</PropertyCollapsible>
</Form>
);
}
});
export default ContractForm;

View File

@ -40,6 +40,12 @@ let CreateEditionsForm = React.createClass({
url={ApiUrls.editions}
getFormData={this.getFormData}
handleSuccess={this.handleSuccess}
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
{getLangText('Create editions')}
</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" />

View File

@ -100,6 +100,20 @@ let Form = React.createClass({
.catch(this.handleError);
},
put() {
requests
.put(this.props.url, { body: this.getFormData() })
.then(this.handleSuccess)
.catch(this.handleError);
},
patch() {
requests
.patch(this.props.url, { body: this.getFormData() })
.then(this.handleSuccess)
.catch(this.handleError);
},
delete() {
requests
.delete(this.props.url, this.getFormData())
@ -189,7 +203,7 @@ let Form = React.createClass({
}
let buttons = null;
if (this.state.edited){
if (this.state.edited && !this.props.disabled){
buttons = (
<div className="row" style={{margin: 0}}>
<p className="pull-right">

View File

@ -56,10 +56,10 @@ let ConsignForm = React.createClass({
<Property
name='consign_message'
label={getLangText('Personal Message')}
editable={true}>
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>

View File

@ -0,0 +1,149 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import ContractListActions from '../../actions/contract_list_actions';
import ContractListStore from '../../stores/contract_list_store';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import Form from './form';
import Property from './property';
import PropertyCollapsible from './property_collapsible';
import InputTextAreaToggable from './input_textarea_toggable';
import ApiUrls from '../../constants/api_urls';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
let ContractAgreementForm = React.createClass({
propTypes: {
handleSuccess: React.PropTypes.func
},
mixins: [Router.Navigation, Router.State],
getInitialState() {
return mergeOptions(
ContractListStore.getState(),
{
selectedContract: 0
}
);
},
componentDidMount() {
ContractListStore.listen(this.onChange);
ContractListActions.fetchContractList(true, false);
},
componentWillUnmount() {
ContractListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
onContractChange(event){
this.setState({selectedContract: event.target.selectedIndex});
},
handleSubmitSuccess() {
let notification = 'Contract agreement send';
notification = new GlobalNotificationModel(notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
},
getFormData(){
return {'appendix': {'default': this.refs.form.refs.appendix.state.value}};
},
getContracts() {
if (this.state.contractList && this.state.contractList.length > 0) {
let contractList = this.state.contractList;
return (
<Property
name='contract'
label={getLangText('Contract Type')}
onChange={this.onContractChange}>
<select name="contract">
{contractList.map((contract, i) => {
return (
<option
name={i}
key={i}
value={ contract.id }>
{ contract.name }
</option>
);
})}
</select>
</Property>);
}
return null;
},
render() {
if (this.state.contractList && this.state.contractList.length > 0) {
return (
<Form
className="ascribe-form-bordered ascribe-form-wrapper"
ref='form'
url={ApiUrls.ownership_contract_agreements}
getFormData={this.getFormData}
handleSuccess={this.handleSubmitSuccess}
buttons={<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
{getLangText('Send contract')}
</button>}
spinner={
<span 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" />
</span>
}>
<div className="ascribe-form-header">
<h3>{getLangText('Contract form')}</h3>
</div>
<Property
name='signee'
label={getLangText('Artist Email')}>
<input
type="email"
placeholder={getLangText('(e.g. andy@warhol.co.uk)')}
required/>
</Property>
{this.getContracts()}
<PropertyCollapsible
name='appendix'
checkboxLabel={getLangText('Add appendix to the contract')}>
<span>{getLangText('Appendix')}</span>
{/* We're using disabled on a form here as PropertyCollapsible currently
does not support the disabled + overrideForm functionality */}
<InputTextAreaToggable
rows={1}
disabled={false}
placeholder={getLangText('This will be appended to the contract selected above')}/>
</PropertyCollapsible>
</Form>
);
}
return (
<div>
<p className="text-center">
{getLangText('No contracts uploaded yet, please go to the ')}
<a href="settings">{getLangText('settings page')}</a>
{getLangText(' and create them.')}
</p>
</div>
);
}
});
export default ContractAgreementForm;

View File

@ -0,0 +1,80 @@
'use strict';
import React from 'react';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import Form from './form';
import Property from './property';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils';
let CopyrightAssociationForm = React.createClass({
propTypes: {
currentUser: React.PropTypes.object
},
handleSubmitSuccess(){
let notification = getLangText('Copyright association updated');
notification = new GlobalNotificationModel(notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getProfileFormData(){
return {email: this.props.currentUser.email};
},
render() {
let selectedState;
let selectDefaultValue = ' -- ' + getLangText('select an association') + ' -- ';
if (this.props.currentUser && this.props.currentUser.profile
&& this.props.currentUser.profile.copyright_association) {
selectedState = AppConstants.copyrightAssociations.indexOf(this.props.currentUser.profile.copyright_association);
selectedState = selectedState !== -1 ? AppConstants.copyrightAssociations[selectedState] : selectDefaultValue;
}
if (this.props.currentUser && this.props.currentUser.email){
return (
<Form
ref='form'
url={ApiUrls.users_profile}
getFormData={this.getProfileFormData}
handleSuccess={this.handleSubmitSuccess}>
<Property
name="copyright_association"
className="ascribe-settings-property-collapsible-toggle"
label={getLangText('Copyright Association')}
style={{paddingBottom: 0}}>
<select defaultValue={selectedState} name="contract">
<option
name={0}
key={0}
value={selectDefaultValue}>
{selectDefaultValue}
</option>
{AppConstants.copyrightAssociations.map((association, i) => {
return (
<option
name={i + 1}
key={i + 1}
value={association}>
{ association }
</option>
);
})}
</select>
</Property>
<hr />
</Form>
);
}
return null;
}
});
export default CopyrightAssociationForm;

View File

@ -0,0 +1,111 @@
'use strict';
import React from 'react';
import Form from '../ascribe_forms/form';
import Property from '../ascribe_forms/property';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import ContractListActions from '../../actions/contract_list_actions';
import AppConstants from '../../constants/application_constants';
import ApiUrls from '../../constants/api_urls';
import InputFineUploader from './input_fineuploader';
import { getLangText } from '../../utils/lang_utils';
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
let CreateContractForm = React.createClass({
propTypes: {
isPublic: React.PropTypes.bool,
// A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
})
},
getInitialState() {
return {
isUploadReady: false,
contractName: ''
};
},
setIsUploadReady(isReady) {
this.setState({
isUploadReady: isReady
});
},
handleCreateSuccess(response) {
ContractListActions.fetchContractList(true);
let notification = new GlobalNotificationModel(getLangText('Contract %s successfully created', response.name), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.refs.form.reset();
},
submitFileName(fileName) {
this.setState({
contractName: fileName
});
this.refs.form.submit();
},
render() {
return (
<Form
ref='form'
url={ApiUrls.ownership_contract_list}
handleSuccess={this.handleCreateSuccess}>
<Property
name="blob"
label={getLangText('Contract file (*.pdf only, max. 50MB per contract)')}>
<InputFineUploader
submitFileName={this.submitFileName}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'contract'
}}
createBlobRoutine={{
url: ApiUrls.blob_contracts
}}
validation={{
itemLimit: AppConstants.fineUploader.validation.additionalData.itemLimit,
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
allowedExtensions: ['pdf']
}}
areAssetsDownloadable={true}
areAssetsEditable={true}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
fileClassToUpload={this.props.fileClassToUpload}/>
</Property>
<Property
name='name'
label={getLangText('Contract name')}
hidden={true}>
<input
type="text"
value={this.state.contractName}/>
</Property>
<Property
name="is_public"
hidden={true}>
<input
type="checkbox"
value={this.props.isPublic} />
</Property>
</Form>
);
}
});
export default CreateContractForm;

View File

@ -12,11 +12,12 @@ import InputTextAreaToggable from './input_textarea_toggable';
import InputDate from './input_date';
import InputCheckbox from './input_checkbox';
import LoanContractStore from '../../stores/loan_contract_store';
import LoanContractActions from '../../actions/loan_contract_actions';
import ContractAgreementListStore from '../../stores/contract_agreement_list_store';
import ContractAgreementListActions from '../../actions/contract_agreement_list_actions';
import AppConstants from '../../constants/application_constants';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
@ -34,6 +35,7 @@ let LoanForm = React.createClass({
url: React.PropTypes.string,
id: React.PropTypes.object,
message: React.PropTypes.string,
createPublicContractAgreement: React.PropTypes.bool,
handleSuccess: React.PropTypes.func
},
@ -43,62 +45,117 @@ let LoanForm = React.createClass({
showPersonalMessage: true,
showEndDate: true,
showStartDate: true,
showPassword: true
showPassword: true,
createPublicContractAgreement: true
};
},
getInitialState() {
return LoanContractStore.getState();
return ContractAgreementListStore.getState();
},
componentDidMount() {
LoanContractStore.listen(this.onChange);
LoanContractActions.flushLoanContract.defer();
ContractAgreementListStore.listen(this.onChange);
this.getContractAgreementsOrCreatePublic(this.props.email);
},
/**
* This method needs to be in form_loan as some whitelabel pages (Cyland) load
* the loanee's email async!
*
* SO LEAVE IT IN!
*/
componentWillReceiveProps(nextProps) {
if(nextProps && nextProps.email && this.props.email !== nextProps.email) {
this.getContractAgreementsOrCreatePublic(nextProps.email);
}
},
componentWillUnmount() {
LoanContractStore.unlisten(this.onChange);
ContractAgreementListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
getContractAgreementsOrCreatePublic(email){
ContractAgreementListActions.flushContractAgreementList.defer();
if (email) {
// fetch the available contractagreements (pending/accepted)
ContractAgreementListActions.fetchAvailableContractAgreementList(email, true);
}
},
getFormData(){
return this.props.id;
return mergeOptions(
this.props.id,
this.getContractAgreementId()
);
},
handleOnChange(event) {
// event.target.value is the submitted email of the loanee
if(event && event.target && event.target.value && event.target.value.match(/.*@.*/)) {
LoanContractActions.fetchLoanContract(event.target.value);
if(event && event.target && event.target.value && event.target.value.match(/.*@.*\..*/)) {
this.getContractAgreementsOrCreatePublic(event.target.value);
} else {
LoanContractActions.flushLoanContract();
ContractAgreementListActions.flushContractAgreementList();
}
},
getContractAgreementId() {
if (this.state.contractAgreementList && this.state.contractAgreementList.length > 0) {
return {'contract_agreement_id': this.state.contractAgreementList[0].id};
}
return {};
},
getContractCheckbox() {
if(this.state.contractKey && this.state.contractUrl) {
if(this.state.contractAgreementList && this.state.contractAgreementList.length > 0) {
// we need to define a key on the InputCheckboxes as otherwise
// react is not rerendering them on a store switch and is keeping
// the default value of the component (which is in that case true)
return (
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox
key="terms_explicitly"
defaultChecked={false}>
<span>
{getLangText('I agree to the')}&nbsp;
<a href={this.state.contractUrl} target="_blank">
{getLangText('terms of')} {this.state.contractEmail}
</a>
</span>
</InputCheckbox>
</Property>
);
let contractAgreement = this.state.contractAgreementList[0];
let contract = contractAgreement.contract;
if(contractAgreement.datetime_accepted) {
return (
<Property
name="terms"
label={getLangText('Loan Contract')}
hidden={false}
className="notification-contract-pdf">
<embed
className="loan-form"
src={contract.blob.url_safe}
alt="pdf"
pluginspage="http://www.adobe.com/products/acrobat/readstep2.html"/>
{/* We still need to send the server information that we're accepting */}
<InputCheckbox
style={{'display': 'none'}}
key="terms_implicitly"
defaultChecked={true} />
</Property>
);
} else {
return (
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox
key="terms_explicitly"
defaultChecked={false}>
<span>
{getLangText('I agree to the')}&nbsp;
<a href={contract.blob.url_safe} target="_blank">
{getLangText('terms of ')} {contract.issuer}
</a>
</span>
</InputCheckbox>
</Property>
);
}
} else {
return (
<Property
@ -113,6 +170,22 @@ let LoanForm = React.createClass({
}
},
getAppendix() {
if(this.state.contractAgreementList && this.state.contractAgreementList.length > 0) {
let appendix = this.state.contractAgreementList[0].appendix;
if (appendix && appendix.default) {
return (
<Property
name='appendix'
label={getLangText('Appendix')}>
<pre className="ascribe-pre">{appendix.default}</pre>
</Property>
);
}
}
return null;
},
getButtons() {
if(this.props.loanHeading) {
return (
@ -157,8 +230,8 @@ let LoanForm = React.createClass({
<Property
name='loanee'
label={getLangText('Loanee Email')}
onChange={this.handleOnChange}
editable={!this.props.email}
onChange={this.handleOnChange}
overrideForm={!!this.props.email}>
<input
value={this.props.email}
@ -200,14 +273,16 @@ let LoanForm = React.createClass({
name='loan_message'
label={getLangText('Personal Message')}
editable={true}
overrideForm={true}
hidden={!this.props.showPersonalMessage}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required={this.props.showPersonalMessage ? 'required' : ''}/>
</Property>
{this.getContractCheckbox()}
{this.getAppendix()}
<Property
name='password'
label={getLangText('Password')}
@ -217,7 +292,6 @@ let LoanForm = React.createClass({
placeholder={getLangText('Enter your password')}
required={this.props.showPassword ? 'required' : ''}/>
</Property>
{this.getContractCheckbox()}
{this.props.children}
</Form>
);

View File

@ -18,7 +18,7 @@ let LoanRequestAnswerForm = React.createClass({
url: React.PropTypes.string,
id: React.PropTypes.object,
message: React.PropTypes.string,
handleSuccess: React.PropTypes.func.required
handleSuccess: React.PropTypes.func.isRequired
},
getDefaultProps() {

View File

@ -27,7 +27,7 @@ let LoginForm = React.createClass({
onLogin: React.PropTypes.func
},
mixins: [Router.Navigation],
mixins: [Router.Navigation, Router.State],
getDefaultProps() {
return {
@ -95,6 +95,7 @@ let LoginForm = React.createClass({
},
render() {
let email = this.getQuery().email || null;
return (
<Form
className="ascribe-form-bordered"
@ -122,7 +123,8 @@ let LoginForm = React.createClass({
<input
type="email"
placeholder={getLangText('Enter your email')}
name="username"
name="email"
defaultValue={email}
required/>
</Property>
<Property

View File

@ -41,14 +41,13 @@ let PieceExtraDataForm = React.createClass({
ref='form'
url={url}
handleSuccess={this.props.handleSuccess}
getFormData={this.getFormData}>
getFormData={this.getFormData}
disabled={!this.props.editable}>
<Property
name={this.props.name}
label={this.props.title}
editable={this.props.editable}>
label={this.props.title}>
<InputTextAreaToggable
rows={1}
editable={this.props.editable}
defaultValue={defaultValue}
placeholder={getLangText('Fill in%s', ' ') + this.props.title}
required="required"/>

View File

@ -10,10 +10,11 @@ import Property from './property';
import InputFineUploader from './input_fineuploader';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
import { isReadyForFormSubmission } from '../ascribe_uploader/react_s3_fine_uploader_utils';
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
let RegisterPieceForm = React.createClass({
@ -99,11 +100,19 @@ let RegisterPieceForm = React.createClass({
name="digital_work_key"
ignoreFocus={true}>
<InputFineUploader
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'digitalwork'
}}
createBlobRoutine={{
url: ApiUrls.blob_digitalworks
}}
validation={AppConstants.fineUploader.validation.registerWork}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={isReadyForFormSubmission}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
isFineUploaderActive={this.props.isFineUploaderActive}
onLoggedOut={this.props.onLoggedOut}
editable={this.props.isFineUploaderEditable}
disabled={!this.props.isFineUploaderEditable}
enableLocalHashing={enableLocalHashing}/>
</Property>
<Property

View File

@ -6,6 +6,8 @@ import AclButton from './../ascribe_buttons/acl_button';
import ActionPanel from '../ascribe_panel/action_panel';
import Form from './form';
import NotificationActions from '../../actions/notification_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
@ -20,8 +22,7 @@ let RequestActionForm = React.createClass({
React.PropTypes.object,
React.PropTypes.array
]).isRequired,
requestAction: React.PropTypes.string,
requestUser: React.PropTypes.string,
notifications: React.PropTypes.object,
currentUser: React.PropTypes.object,
handleSuccess: React.PropTypes.func
},
@ -33,19 +34,19 @@ let RequestActionForm = React.createClass({
getUrls() {
let urls = {};
if (this.props.requestAction === 'consign'){
if (this.props.notifications.action === 'consign'){
urls.accept = ApiUrls.ownership_consigns_confirm;
urls.deny = ApiUrls.ownership_consigns_deny;
} else if (this.props.requestAction === 'unconsign'){
} else if (this.props.notifications.action === 'unconsign'){
urls.accept = ApiUrls.ownership_unconsigns;
urls.deny = ApiUrls.ownership_unconsigns_deny;
} else if (this.props.requestAction === 'loan' && !this.isPiece()){
} else if (this.props.notifications.action === 'loan' && !this.isPiece()){
urls.accept = ApiUrls.ownership_loans_confirm;
urls.deny = ApiUrls.ownership_loans_deny;
} else if (this.props.requestAction === 'loan' && this.isPiece()){
} else if (this.props.notifications.action === 'loan' && this.isPiece()){
urls.accept = ApiUrls.ownership_loans_pieces_confirm;
urls.deny = ApiUrls.ownership_loans_pieces_deny;
} else if (this.props.requestAction === 'loan_request' && this.isPiece()){
} else if (this.props.notifications.action === 'loan_request' && this.isPiece()){
urls.accept = ApiUrls.ownership_loans_pieces_request_confirm;
urls.deny = ApiUrls.ownership_loans_pieces_request_deny;
}
@ -68,30 +69,36 @@ let RequestActionForm = React.createClass({
return () => {
let message = getLangText('You have successfully') + ' ' + option + ' the ' + action + ' request ' + getLangText('from') + ' ' + owner;
let notification = new GlobalNotificationModel(message, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
let notifications = new GlobalNotificationModel(message, 'success');
GlobalNotificationActions.appendGlobalNotification(notifications);
this.handleSuccess();
if(this.props.handleSuccess) {
this.props.handleSuccess();
}
};
},
getContent() {
let pieceOrEditionStr = this.isPiece() ? getLangText('this work%s', '.') : getLangText('this edition%s', '.');
let message = this.props.requestUser + ' ' + getLangText('requests you') + ' ' + this.props.requestAction + ' ' + pieceOrEditionStr;
if (this.props.requestAction === 'loan_request'){
message = this.props.requestUser + ' ' + getLangText('requests you to loan') + ' ' + pieceOrEditionStr;
handleSuccess() {
if (this.isPiece()){
NotificationActions.fetchPieceListNotifications();
}
else {
NotificationActions.fetchEditionListNotifications();
}
if(this.props.handleSuccess) {
this.props.handleSuccess();
}
},
getContent() {
return (
<span>
{message}
{this.props.notifications.action_str + ' by ' + this.props.notifications.by}
</span>
);
},
getAcceptButtonForm(urls) {
if(this.props.requestAction === 'unconsign') {
if(this.props.notifications.action === 'unconsign') {
return (
<AclButton
availableAcls={{'acl_unconsign': true}}
@ -99,9 +106,9 @@ let RequestActionForm = React.createClass({
buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
pieceOrEditions={this.props.pieceOrEditions}
currentUser={this.props.currentUser}
handleSuccess={this.props.handleSuccess} />
handleSuccess={this.handleSuccess} />
);
} else if(this.props.requestAction === 'loan_request') {
} else if(this.props.notifications.action === 'loan_request') {
return (
<AclButton
availableAcls={{'acl_loan_request': true}}
@ -110,7 +117,7 @@ let RequestActionForm = React.createClass({
buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
pieceOrEditions={this.props.pieceOrEditions}
currentUser={this.props.currentUser}
handleSuccess={this.props.handleSuccess} />
handleSuccess={this.handleSuccess} />
);
} else {
return (
@ -118,7 +125,7 @@ let RequestActionForm = React.createClass({
url={urls.accept}
getFormData={this.getFormData}
handleSuccess={
this.showNotification(getLangText('accepted'), this.props.requestAction, this.props.requestUser)
this.showNotification(getLangText('accepted'), this.props.notifications.action, this.props.notifications.by)
}
isInline={true}
className='inline pull-right'>
@ -143,7 +150,7 @@ let RequestActionForm = React.createClass({
isInline={true}
getFormData={this.getFormData}
handleSuccess={
this.showNotification(getLangText('denied'), this.props.requestAction, this.props.requestUser)
this.showNotification(getLangText('denied'), this.props.notifications.action, this.props.notifications.by)
}
className='inline pull-right'>
<button

View File

@ -60,10 +60,10 @@ let ShareForm = React.createClass({
<Property
name='share_message'
label='Personal Message'
editable={true}>
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>

View File

@ -66,17 +66,24 @@ let SignupForm = React.createClass({
}
},
getFormData() {
if (this.getQuery().token){
return {token: this.getQuery().token};
}
return null;
},
render() {
let tooltipPassword = getLangText('Your password must be at least 10 characters') + '.\n ' +
getLangText('This password is securing your digital property like a bank account') + '.\n ' +
getLangText('Store it in a safe place') + '!';
let email = this.getQuery().email ? this.getQuery().email : null;
let email = this.getQuery().email || null;
return (
<Form
className="ascribe-form-bordered"
ref='form'
url={ApiUrls.users_signup}
getFormData={this.getQuery}
getFormData={this.getFormData}
handleSuccess={this.handleSuccess}
buttons={
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
@ -127,7 +134,7 @@ let SignupForm = React.createClass({
style={{paddingBottom: 0}}>
<InputCheckbox>
<span>
{' ' + getLangText('I agree to the Terms of Service') + ' '}
{' ' + getLangText('I agree to the Terms of Service of ascribe') + ' '}
(<a href="https://www.ascribe.io/terms" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}>
{getLangText('read')}
</a>)

View File

@ -45,20 +45,20 @@ let PieceSubmitToPrizeForm = React.createClass({
<Property
name='artist_statement'
label={getLangText('Artist statement')}
editable={true}>
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
editable={true}
placeholder={getLangText('Enter your statement')}
required="required"/>
</Property>
<Property
name='work_description'
label={getLangText('Work description')}
editable={true}>
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
editable={true}
placeholder={getLangText('Enter the description for your work')}
required="required"/>
</Property>

View File

@ -61,10 +61,10 @@ let TransferForm = React.createClass({
<Property
name='transfer_message'
label={getLangText('Personal Message')}
editable={true}>
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>

View File

@ -50,10 +50,10 @@ let UnConsignForm = React.createClass({
<Property
name='unconsign_message'
label={getLangText('Personal Message')}
editable={true}>
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>

View File

@ -50,10 +50,10 @@ let UnConsignRequestForm = React.createClass({
<Property
name='unconsign_request_message'
label={getLangText('Personal Message')}
editable={true}>
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>

View File

@ -25,7 +25,10 @@ let InputCheckbox = React.createClass({
// provided by Property
disabled: React.PropTypes.bool,
onChange: React.PropTypes.func
onChange: React.PropTypes.func,
// can be used to style the component from the outside
style: React.PropTypes.object
},
// As HTML inputs, we're setting the default value for an input to checked === false
@ -98,6 +101,7 @@ let InputCheckbox = React.createClass({
return (
<span
style={this.props.style}
onClick={this.onChange}>
<input
type="checkbox"

View File

@ -5,26 +5,48 @@ import React from 'react';
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
import AppConstants from '../../constants/application_constants';
import ApiUrls from '../../constants/api_urls';
import { getCookie } from '../../utils/fetch_api_utils';
let InputFileUploader = React.createClass({
let InputFineUploader = React.createClass({
propTypes: {
setIsUploadReady: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
submitFileName: React.PropTypes.func,
areAssetsDownloadable: React.PropTypes.bool,
onClick: React.PropTypes.func,
keyRoutine: React.PropTypes.shape({
url: React.PropTypes.string,
fileClass: React.PropTypes.string
}),
createBlobRoutine: React.PropTypes.shape({
url: React.PropTypes.string
}),
validation: React.PropTypes.shape({
itemLimit: React.PropTypes.number,
sizeLimit: React.PropTypes.string,
allowedExtensions: React.PropTypes.arrayOf(React.PropTypes.string)
}),
// isFineUploaderActive is used to lock react fine uploader in case
// a user is actually not logged in already to prevent him from droping files
// before login in
isFineUploaderActive: React.PropTypes.bool,
onLoggedOut: React.PropTypes.func,
editable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool,
// provided by Property
disabled: React.PropTypes.bool
disabled: React.PropTypes.bool,
// A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
})
},
getInitialState() {
@ -33,10 +55,14 @@ let InputFileUploader = React.createClass({
};
},
submitKey(key){
submitFile(file){
this.setState({
value: key
value: file.key
});
if(typeof this.props.submitFileName === 'function') {
this.props.submitFileName(file.originalName);
}
},
reset() {
@ -56,21 +82,13 @@ let InputFileUploader = React.createClass({
<ReactS3FineUploader
ref="fineuploader"
onClick={this.props.onClick}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'digitalwork'
}}
createBlobRoutine={{
url: ApiUrls.blob_digitalworks
}}
submitKey={this.submitKey}
validation={{
itemLimit: 100000,
sizeLimit: '25000000000'
}}
keyRoutine={this.props.keyRoutine}
createBlobRoutine={this.props.createBlobRoutine}
validation={this.props.validation}
submitFile={this.submitFile}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
areAssetsDownloadable={false}
areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={editable}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/',
@ -87,9 +105,10 @@ let InputFileUploader = React.createClass({
}
}}
onInactive={this.props.onLoggedOut}
enableLocalHashing={this.props.enableLocalHashing} />
enableLocalHashing={this.props.enableLocalHashing}
fileClassToUpload={this.props.fileClassToUpload}/>
);
}
});
export default InputFileUploader;
export default InputFineUploader;

View File

@ -7,7 +7,7 @@ import TextareaAutosize from 'react-textarea-autosize';
let InputTextAreaToggable = React.createClass({
propTypes: {
editable: React.PropTypes.bool.isRequired,
disabled: React.PropTypes.bool,
rows: React.PropTypes.number.isRequired,
required: React.PropTypes.string,
defaultValue: React.PropTypes.string
@ -15,10 +15,26 @@ let InputTextAreaToggable = React.createClass({
getInitialState() {
return {
value: this.props.defaultValue
value: null
};
},
componentDidMount() {
this.setState({
value: this.props.defaultValue
});
},
componentDidUpdate() {
// If the initial value of state.value is null, we want to set props.defaultValue
// as a value. In all other cases TextareaAutosize.onChange is updating.handleChange already
if(this.state.value === null && this.props.defaultValue) {
this.setState({
value: this.props.defaultValue
});
}
},
handleChange(event) {
this.setState({value: event.target.value});
this.props.onChange(event);
@ -28,7 +44,7 @@ let InputTextAreaToggable = React.createClass({
let className = 'form-control ascribe-textarea';
let textarea = null;
if(this.props.editable) {
if(!this.props.disabled) {
className = className + ' ascribe-textarea-editable';
textarea = (
<TextareaAutosize

View File

@ -12,20 +12,19 @@ let ListRequestActions = React.createClass({
]).isRequired,
currentUser: React.PropTypes.object.isRequired,
handleSuccess: React.PropTypes.func.isRequired,
requestActions: React.PropTypes.array.isRequired
notifications: React.PropTypes.array.isRequired
},
render () {
if (this.props.requestActions &&
this.props.requestActions.length > 0) {
if (this.props.notifications &&
this.props.notifications.length > 0) {
return (
<div>
{this.props.requestActions.map((requestAction) =>
{this.props.notifications.map((notification) =>
<RequestActionForm
currentUser={this.props.currentUser}
pieceOrEditions={ this.props.pieceOrEditions }
requestAction={requestAction.action}
requestUser={requestAction.by}
notifications={notification}
handleSuccess={this.props.handleSuccess}/>)}
</div>
);

View File

@ -70,7 +70,7 @@ let Property = React.createClass({
// In order to set this.state.value from another component
// the state of value should only be set if its not undefined and
// actually references something
if(typeof childInput.getDOMNode().value !== 'undefined') {
if(childInput && typeof childInput.getDOMNode().value !== 'undefined') {
this.setState({
value: childInput.getDOMNode().value
});

View File

@ -12,7 +12,10 @@ let ActionPanel = React.createClass({
]),
buttons: React.PropTypes.element,
onClick: React.PropTypes.func,
ignoreFocus: React.PropTypes.bool
ignoreFocus: React.PropTypes.bool,
leftColumnWidth: React.PropTypes.string,
rightColumnWidth: React.PropTypes.string
},
getInitialState() {
@ -41,14 +44,21 @@ let ActionPanel = React.createClass({
},
render() {
let { leftColumnWidth, rightColumnWidth } = this.props;
return (
<div className={classnames('ascribe-panel-wrapper', {'is-focused': this.state.isFocused})}>
<div className="ascribe-panel-table">
<div
className="ascribe-panel-table"
style={{width: leftColumnWidth}}>
<div className="ascribe-panel-content">
{this.props.content}
</div>
</div>
<div className="ascribe-panel-table">
<div
className="ascribe-panel-table"
style={{width: rightColumnWidth}}>
<div className="ascribe-panel-content">
{this.props.buttons}
</div>

View File

@ -14,7 +14,20 @@ let PieceListToolbar = React.createClass({
propTypes: {
className: React.PropTypes.string,
searchFor: React.PropTypes.func,
filterParams: React.PropTypes.array,
filterParams: React.PropTypes.arrayOf(
React.PropTypes.shape({
label: React.PropTypes.string,
items: React.PropTypes.arrayOf(
React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.shape({
key: React.PropTypes.string,
label: React.PropTypes.string
})
])
)
})
),
filterBy: React.PropTypes.object,
applyFilterBy: React.PropTypes.func,
orderParams: React.PropTypes.array,

View File

@ -3,20 +3,26 @@
import React from 'react';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItem from 'react-bootstrap/lib/MenuItem';
import { getLangText } from '../../utils/lang_utils.js';
let PieceListToolbarFilterWidgetFilter = React.createClass({
propTypes: {
// An array of either strings (which represent acl enums) or objects of the form
//
// {
// key: <acl enum>,
// label: <a human readable string>
// }
//
filterParams: React.PropTypes.arrayOf(React.PropTypes.any).isRequired,
filterParams: React.PropTypes.arrayOf(
React.PropTypes.shape({
label: React.PropTypes.string,
items: React.PropTypes.arrayOf(
React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.shape({
key: React.PropTypes.string,
label: React.PropTypes.string
})
])
)
})
).isRequired,
filterBy: React.PropTypes.object,
applyFilterBy: React.PropTypes.func
},
@ -79,35 +85,53 @@ let PieceListToolbarFilterWidgetFilter = React.createClass({
<DropdownButton
title={filterIcon}
className="ascribe-piece-list-toolbar-filter-widget">
<li style={{'textAlign': 'center'}}>
<em>{getLangText('Show works I can')}:</em>
</li>
{this.props.filterParams.map((param, i) => {
let label;
if(typeof param !== 'string') {
label = param.label;
param = param.key;
} else {
param = param;
label = param.split('_')[1];
}
{/* We iterate over filterParams, to receive the label and then for each
label also iterate over its items, to get all filterable options */}
{this.props.filterParams.map(({ label, items }, i) => {
return (
<MenuItem
key={i}
onClick={this.filterBy(param)}
className="filter-widget-item">
<div className="checkbox-line">
<span>
{getLangText(label)}
</span>
<input
readOnly
type="checkbox"
checked={this.props.filterBy[param]} />
</div>
</MenuItem>
<div>
<li
style={{'textAlign': 'center'}}
key={i}>
<em>{label}:</em>
</li>
{items.map((param, j) => {
// As can be seen in the PropTypes, a param can either
// be a string or an object of the shape:
//
// {
// key: <String>,
// label: <String>
// }
//
// This is why we need to distinguish between both here.
if(typeof param !== 'string') {
label = param.label;
param = param.key;
} else {
param = param;
label = param.split('acl_')[1].replace(/_/g, ' ');
}
return (
<li
key={j}
onClick={this.filterBy(param)}
className="filter-widget-item">
<div className="checkbox-line">
<span>
{getLangText(label)}
</span>
<input
readOnly
type="checkbox"
checked={this.props.filterBy[param]} />
</div>
</li>
);
})}
</div>
);
})}
</DropdownButton>

View File

@ -3,7 +3,6 @@
import React from 'react';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItem from 'react-bootstrap/lib/MenuItem';
import { getLangText } from '../../utils/lang_utils.js';
@ -62,20 +61,22 @@ let PieceListToolbarOrderWidget = React.createClass({
</li>
{this.props.orderParams.map((param) => {
return (
<MenuItem
key={param}
onClick={this.orderBy(param)}
className="filter-widget-item">
<div className="checkbox-line">
<span>
{getLangText(param.replace('_', ' '))}
</span>
<input
readOnly
type="checkbox"
checked={param.indexOf(this.props.orderBy) > -1} />
</div>
</MenuItem>
<div>
<li
key={param}
onClick={this.orderBy(param)}
className="filter-widget-item">
<div className="checkbox-line">
<span>
{getLangText(param.replace('_', ' '))}
</span>
<input
readOnly
type="checkbox"
checked={param.indexOf(this.props.orderBy) > -1} />
</div>
</li>
</div>
);
})}
</DropdownButton>

View File

@ -1,82 +0,0 @@
'use strict';
import React from 'react';
import PrizeListActions from '../../actions/prize_list_actions';
import PrizeListStore from '../../stores/prize_list_store';
import Table from '../ascribe_table/table';
import TableItem from '../ascribe_table/table_item';
import TableItemText from '../ascribe_table/table_item_text';
import { ColumnModel} from '../ascribe_table/models/table_models';
import { getLangText } from '../../utils/lang_utils';
let PrizesDashboard = React.createClass({
getInitialState() {
return PrizeListStore.getState();
},
componentDidMount() {
PrizeListStore.listen(this.onChange);
PrizeListActions.fetchPrizeList();
},
componentWillUnmount() {
PrizeListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
getColumnList() {
return [
new ColumnModel(
(item) => {
return {
'content': item.name
}; },
'name',
getLangText('Name'),
TableItemText,
6,
false,
null
),
new ColumnModel(
(item) => {
return {
'content': item.domain
}; },
'domain',
getLangText('Domain'),
TableItemText,
1,
false,
null
)
];
},
render() {
return (
<Table
responsive
className="ascribe-table"
columnList={this.getColumnList()}
itemList={this.state.prizeList}>
{this.state.prizeList.map((item, i) => {
return (
<TableItem
className="ascribe-table-item-selectable"
key={i}/>
);
})}
</Table>
);
}
});
export default PrizesDashboard;

View File

@ -0,0 +1,106 @@
'use strict';
import React from 'react';
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 InputCheckbox from '../ascribe_forms/input_checkbox';
import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph';
import AclProxy from '../acl_proxy';
import CopyrightAssociationForm from '../ascribe_forms/form_copyright_association';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils';
let AccountSettings = React.createClass({
propTypes: {
currentUser: React.PropTypes.object.isRequired,
loadUser: React.PropTypes.func.isRequired,
whitelabel: React.PropTypes.object.isRequired
},
handleSuccess(){
this.props.loadUser();
let notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getFormDataProfile(){
return {'email': this.props.currentUser.email};
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
let profile = null;
if (this.props.currentUser.username) {
content = (
<Form
url={ApiUrls.users_username}
handleSuccess={this.handleSuccess}>
<Property
name='username'
label={getLangText('Username')}>
<input
type="text"
defaultValue={this.props.currentUser.username}
placeholder={getLangText('Enter your username')}
required/>
</Property>
<Property
name='email'
label={getLangText('Email')}
overrideForm={true}
editable={false}>
<input
type="text"
defaultValue={this.props.currentUser.email}
placeholder={getLangText('Enter your username')}
required/>
</Property>
<hr />
</Form>
);
profile = (
<AclProxy
aclObject={this.props.whitelabel}
aclName="acl_view_settings_account_hash">
<Form
url={ApiUrls.users_profile}
handleSuccess={this.handleSuccess}
getFormData={this.getFormDataProfile}>
<Property
name="hash_locally"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox
defaultChecked={this.props.currentUser.profile.hash_locally}>
<span>
{' ' + getLangText('Enable hash option, e.g. slow connections or to keep piece private')}
</span>
</InputCheckbox>
</Property>
</Form>
</AclProxy>
);
}
return (
<CollapsibleParagraph
title={getLangText('Account')}
defaultExpanded={true}>
{content}
<CopyrightAssociationForm currentUser={this.props.currentUser}/>
{profile}
</CollapsibleParagraph>
);
}
});
export default AccountSettings;

View File

@ -0,0 +1,123 @@
'use strict';
import React from 'react';
import ApplicationStore from '../../stores/application_store';
import ApplicationActions from '../../actions/application_actions';
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 ActionPanel from '../ascribe_panel/action_panel';
import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils';
let APISettings = React.createClass({
propTypes: {
defaultExpanded: React.PropTypes.bool
},
getInitialState() {
return ApplicationStore.getState();
},
componentDidMount() {
ApplicationStore.listen(this.onChange);
ApplicationActions.fetchApplication();
},
componentWillUnmount() {
ApplicationStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
handleCreateSuccess() {
ApplicationActions.fetchApplication();
let notification = new GlobalNotificationModel(getLangText('Application successfully created'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
handleTokenRefresh(event) {
let applicationName = event.target.getAttribute('data-id');
ApplicationActions.refreshApplicationToken(applicationName);
let notification = new GlobalNotificationModel(getLangText('Token refreshed'), 'success', 2000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getApplications(){
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
if (this.state.applications.length > -1) {
content = this.state.applications.map(function(app, i) {
return (
<ActionPanel
name={app.name}
key={i}
content={
<div>
<div className='ascribe-panel-title'>
{app.name}
</div>
<div className="ascribe-panel-subtitle">
{'Bearer ' + app.bearer_token.token}
</div>
</div>
}
buttons={
<div className="pull-right">
<div className="pull-right">
<button
className="pull-right btn btn-default btn-sm"
onClick={this.handleTokenRefresh}
data-id={app.name}>
{getLangText('REFRESH')}
</button>
</div>
</div>
}/>
);
}, this);
}
return content;
},
render() {
return (
<CollapsibleParagraph
title={getLangText('API Integration')}
defaultExpanded={this.props.defaultExpanded}>
<Form
url={ApiUrls.applications}
handleSuccess={this.handleCreateSuccess}>
<Property
name='name'
label={getLangText('Application Name')}>
<input
type="text"
placeholder={getLangText('Enter the name of your app')}
required/>
</Property>
<hr />
</Form>
<pre>
Usage: curl &lt;url&gt; -H 'Authorization: Bearer &lt;token&gt;'
</pre>
{this.getApplications()}
</CollapsibleParagraph>
);
}
});
export default APISettings;

View File

@ -0,0 +1,71 @@
'use strict';
import React from 'react';
import WalletSettingsStore from '../../stores/wallet_settings_store';
import WalletSettingsActions from '../../actions/wallet_settings_actions';
import Form from '../ascribe_forms/form';
import Property from '../ascribe_forms/property';
import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils';
let BitcoinWalletSettings = React.createClass({
propTypes: {
defaultExpanded: React.PropTypes.bool
},
getInitialState() {
return WalletSettingsStore.getState();
},
componentDidMount() {
WalletSettingsStore.listen(this.onChange);
WalletSettingsActions.fetchWalletSettings();
},
componentWillUnmount() {
WalletSettingsStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
if (this.state.walletSettings.btc_public_key) {
content = (
<Form >
<Property
name='btc_public_key'
label={getLangText('Bitcoin public key')}
editable={false}>
<pre className="ascribe-pre">{this.state.walletSettings.btc_public_key}</pre>
</Property>
<Property
name='btc_root_address'
label={getLangText('Root Address')}
editable={false}>
<pre className="ascribe-pre">{this.state.walletSettings.btc_root_address}</pre>
</Property>
<hr />
</Form>);
}
return (
<CollapsibleParagraph
title={getLangText('Crypto Wallet')}
defaultExpanded={this.props.defaultExpanded}>
{content}
</CollapsibleParagraph>
);
}
});
export default BitcoinWalletSettings;

View File

@ -0,0 +1,186 @@
'use strict';
import React from 'react';
import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph';
import CreateContractForm from '../ascribe_forms/form_create_contract';
import ContractListStore from '../../stores/contract_list_store';
import ContractListActions from '../../actions/contract_list_actions';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
import WhitelabelStore from '../../stores/whitelabel_store';
import WhitelabelActions from '../../actions/whitelabel_actions';
import ActionPanel from '../ascribe_panel/action_panel';
import ContractSettingsUpdateButton from './contract_settings_update_button';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import AclProxy from '../acl_proxy';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions, truncateTextAtCharIndex } from '../../utils/general_utils';
let ContractSettings = React.createClass({
getInitialState(){
return mergeOptions(
ContractListStore.getState(),
UserStore.getState()
);
},
componentDidMount() {
ContractListStore.listen(this.onChange);
UserStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
UserActions.fetchCurrentUser();
ContractListActions.fetchContractList(true);
},
componentWillUnmount() {
WhitelabelStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
ContractListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
removeContract(contract) {
return () => {
ContractListActions.removeContract(contract.id)
.then((response) => {
ContractListActions.fetchContractList(true);
let notification = new GlobalNotificationModel(response.notification, 'success', 4000);
GlobalNotificationActions.appendGlobalNotification(notification);
})
.catch((err) => {
let notification = new GlobalNotificationModel(err, 'danger', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
});
};
},
getPublicContracts(){
return this.state.contractList.filter((contract) => contract.is_public);
},
getPrivateContracts(){
return this.state.contractList.filter((contract) => !contract.is_public);
},
render() {
let publicContracts = this.getPublicContracts();
let privateContracts = this.getPrivateContracts();
let createPublicContractForm = null;
if(publicContracts.length === 0) {
createPublicContractForm = (
<CreateContractForm
isPublic={true}
fileClassToUpload={{
singular: 'new contract',
plural: 'new contracts'
}}/>
);
}
return (
<div className="settings-container">
<CollapsibleParagraph
title={getLangText('Contracts')}
defaultExpanded={true}>
<AclProxy
aclName="acl_edit_public_contract"
aclObject={this.state.currentUser.acl}>
<div>
{createPublicContractForm}
{publicContracts.map((contract, i) => {
return (
<ActionPanel
key={i}
title={contract.name}
content={truncateTextAtCharIndex(contract.name, 120, '(...).pdf')}
buttons={
<div className="pull-right">
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_update_public_contract">
<ContractSettingsUpdateButton contract={contract}/>
</AclProxy>
<a
className="btn btn-default btn-sm margin-left-2px"
href={contract.blob.url_safe}
target="_blank">
{getLangText('PREVIEW')}
</a>
<button
className="btn btn-danger btn-sm margin-left-2px"
onClick={this.removeContract(contract)}>
{getLangText('REMOVE')}
</button>
</div>
}
leftColumnWidth="40%"
rightColumnWidth="60%"/>
);
})}
</div>
</AclProxy>
<AclProxy
aclName="acl_edit_private_contract"
aclObject={this.state.currentUser.acl}>
<div>
<CreateContractForm
isPublic={false}
fileClassToUpload={{
singular: getLangText('new contract'),
plural: getLangText('new contracts')
}}/>
{privateContracts.map((contract, i) => {
return (
<ActionPanel
key={i}
title={contract.name}
content={truncateTextAtCharIndex(contract.name, 120, '(...).pdf')}
buttons={
<div className="pull-right">
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_update_private_contract">
<ContractSettingsUpdateButton contract={contract}/>
</AclProxy>
<a
className="btn btn-default btn-sm margin-left-2px"
href={contract.blob.url_safe}
target="_blank">
{getLangText('PREVIEW')}
</a>
<button
className="btn btn-danger btn-sm margin-left-2px"
onClick={this.removeContract(contract)}>
{getLangText('REMOVE')}
</button>
</div>
}
leftColumnWidth="60%"
rightColumnWidth="40%"/>
);
})}
</div>
</AclProxy>
</CollapsibleParagraph>
</div>
);
}
});
export default ContractSettings;

View File

@ -0,0 +1,98 @@
'use strict';
import React from 'react';
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
import AppConstants from '../../constants/application_constants';
import ApiUrls from '../../constants/api_urls';
import ContractListActions from '../../actions/contract_list_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils';
let ContractSettingsUpdateButton = React.createClass({
propTypes: {
contract: React.PropTypes.object
},
submitFile(file) {
let contract = this.props.contract;
// override the blob with the key's value
contract.blob = file.key;
// send it to the server
ContractListActions
.changeContract(contract)
.then((res) => {
// Display feedback to the user
let notification = new GlobalNotificationModel(getLangText('Contract %s successfully updated', res.name), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
// and refresh the contract list to get the updated contracs
return ContractListActions.fetchContractList(true);
})
.then(() => {
// Also, reset the fineuploader component so that the user can again 'update' his contract
this.refs.fineuploader.reset();
})
.catch((err) => {
console.logGlobal(err);
let notification = new GlobalNotificationModel(getLangText('Contract could not be updated'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
});
},
render() {
return (
<ReactS3FineUploader
ref="fineuploader"
fileInputElement={UploadButton}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'contract'
}}
createBlobRoutine={{
url: ApiUrls.blob_contracts
}}
validation={{
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
allowedExtensions: ['pdf']
}}
setIsUploadReady={() =>{/* So that ReactS3FineUploader is not complaining */}}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/',
customHeaders: {
'X-CSRFToken': getCookie(AppConstants.csrftoken)
}
}}
deleteFile={{
enabled: true,
method: 'DELETE',
endpoint: AppConstants.serverUrl + 's3/delete',
customHeaders: {
'X-CSRFToken': getCookie(AppConstants.csrftoken)
}
}}
fileClassToUpload={{
singular: getLangText('UPDATE'),
plural: getLangText('UPDATE')
}}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
submitFile={this.submitFile}
/>
);
}
});
export default ContractSettingsUpdateButton;

View File

@ -0,0 +1,84 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
import WhitelabelStore from '../../stores/whitelabel_store';
import WhitelabelActions from '../../actions/whitelabel_actions';
import AccountSettings from './account_settings';
import BitcoinWalletSettings from './bitcoin_wallet_settings';
import APISettings from './api_settings';
import AclProxy from '../acl_proxy';
import { mergeOptions } from '../../utils/general_utils';
let SettingsContainer = React.createClass({
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element])
},
mixins: [Router.Navigation],
getInitialState() {
return mergeOptions(
UserStore.getState(),
WhitelabelStore.getState()
);
},
componentDidMount() {
UserStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
WhitelabelStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
},
loadUser(){
UserActions.fetchCurrentUser();
},
onChange(state) {
this.setState(state);
},
render() {
if (this.state.currentUser && this.state.currentUser.username) {
return (
<div className="settings-container">
<AccountSettings
currentUser={this.state.currentUser}
loadUser={this.loadUser}
whitelabel={this.state.whitelabel}/>
{this.props.children}
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_view_settings_api">
<APISettings />
</AclProxy>
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_view_settings_bitcoin">
<BitcoinWalletSettings />
</AclProxy>
</div>
);
}
return null;
}
});
export default SettingsContainer;

View File

@ -178,7 +178,7 @@ let SlidesContainer = React.createClass({
let breadcrumbs = [];
ReactAddons.Children.map(this.props.children, (child, i) => {
if(i >= this.state.startFrom && child.props['data-slide-title']) {
if(child && i >= this.state.startFrom && child.props['data-slide-title']) {
breadcrumbs.push(child.props['data-slide-title']);
}
});
@ -229,7 +229,7 @@ let SlidesContainer = React.createClass({
// since the default parameter of startFrom is -1, we do not need to check
// if its actually present in the url bar, as it will just not match
if(i >= this.state.startFrom) {
if(child && i >= this.state.startFrom) {
return ReactAddons.addons.cloneWithProps(child, {
className: 'ascribe-slide',
style: {

View File

@ -6,15 +6,15 @@ import React from 'react';
let TableItemAclFiltered = React.createClass({
propTypes: {
content: React.PropTypes.object,
requestAction: React.PropTypes.string
notifications: React.PropTypes.string
},
render() {
var availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete'];
if (this.props.requestAction && this.props.requestAction.length > 0){
if (this.props.notifications && this.props.notifications.length > 0){
return (
<span>
{this.props.requestAction[0].action + ' request pending'}
{this.props.notifications[0].action_str}
</span>
);
}

View File

@ -6,20 +6,14 @@ import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import FileDragAndDropDialog from './file_drag_and_drop_dialog';
import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator';
import { getLangText } from '../../utils/lang_utils';
import { getLangText } from '../../../utils/lang_utils';
// Taken from: https://github.com/fedosejev/react-file-drag-and-drop
let FileDragAndDrop = React.createClass({
propTypes: {
className: React.PropTypes.string,
onDragStart: React.PropTypes.func,
onDrop: React.PropTypes.func.isRequired,
onDrag: React.PropTypes.func,
onDragEnter: React.PropTypes.func,
onLeave: React.PropTypes.func,
onDragLeave: React.PropTypes.func,
onDragOver: React.PropTypes.func,
onDragEnd: React.PropTypes.func,
onInactive: React.PropTypes.func,
filesToUpload: React.PropTypes.array,
handleDeleteFile: React.PropTypes.func,
@ -37,37 +31,16 @@ let FileDragAndDrop = React.createClass({
hashingProgress: React.PropTypes.number,
// sets the value of this.state.hashingProgress in reactfineuploader
// to -1 which is code for: aborted
handleCancelHashing: React.PropTypes.func
},
handleCancelHashing: React.PropTypes.func,
handleDragStart(event) {
if (typeof this.props.onDragStart === 'function') {
this.props.onDragStart(event);
}
},
// A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
}),
handleDrag(event) {
if (typeof this.props.onDrag === 'function') {
this.props.onDrag(event);
}
},
handleDragEnd(event) {
if (typeof this.props.onDragEnd === 'function') {
this.props.onDragEnd(event);
}
},
handleDragEnter(event) {
if (typeof this.props.onDragEnter === 'function') {
this.props.onDragEnter(event);
}
},
handleDragLeave(event) {
if (typeof this.props.onDragLeave === 'function') {
this.props.onDragLeave(event);
}
allowedExtensions: React.PropTypes.string
},
handleDragOver(event) {
@ -159,14 +132,27 @@ let FileDragAndDrop = React.createClass({
},
render: function () {
let { filesToUpload,
dropzoneInactive,
className,
hashingProgress,
handleCancelHashing,
multiple,
enableLocalHashing,
fileClassToUpload,
areAssetsDownloadable,
areAssetsEditable,
allowedExtensions
} = this.props;
// 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 : '';
let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0;
let updatedClassName = hasFiles ? 'has-files ' : '';
updatedClassName += dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone';
updatedClassName += ' file-drag-and-drop';
// if !== -2: triggers a FileDragAndDrop-global spinner
if(this.props.hashingProgress !== -2) {
if(hashingProgress !== -2) {
return (
<div className={className}>
<div className="file-drag-and-drop-hashing-dialog">
@ -184,29 +170,26 @@ let FileDragAndDrop = React.createClass({
} else {
return (
<div
className={className}
onDragStart={this.handleDragStart}
className={updatedClassName}
onDrag={this.handleDrop}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onDragOver={this.handleDragOver}
onDrop={this.handleDrop}
onDragEnd={this.handleDragEnd}>
onDrop={this.handleDrop}>
<FileDragAndDropDialog
multipleFiles={this.props.multiple}
multipleFiles={multiple}
hasFiles={hasFiles}
onClick={this.handleOnClick}
enableLocalHashing={this.props.enableLocalHashing}/>
enableLocalHashing={enableLocalHashing}
fileClassToUpload={fileClassToUpload}/>
<FileDragAndDropPreviewIterator
files={this.props.filesToUpload}
files={filesToUpload}
handleDeleteFile={this.handleDeleteFile}
handleCancelFile={this.handleCancelFile}
handlePauseFile={this.handlePauseFile}
handleResumeFile={this.handleResumeFile}
areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={this.props.areAssetsEditable}/>
areAssetsDownloadable={areAssetsDownloadable}
areAssetsEditable={areAssetsEditable}/>
<input
multiple={this.props.multiple}
multiple={multiple}
ref="fileinput"
type="file"
style={{
@ -214,7 +197,8 @@ let FileDragAndDrop = React.createClass({
height: 0,
width: 0
}}
onChange={this.handleDrop} />
onChange={this.handleDrop}
accept={allowedExtensions}/>
</div>
);
}

View File

@ -3,7 +3,7 @@
import React from 'react';
import Router from 'react-router';
import { getLangText } from '../../utils/lang_utils';
import { getLangText } from '../../../utils/lang_utils';
let Link = Router.Link;
@ -12,7 +12,14 @@ let FileDragAndDropDialog = React.createClass({
hasFiles: React.PropTypes.bool,
multipleFiles: React.PropTypes.bool,
onClick: React.PropTypes.func,
enableLocalHashing: React.PropTypes.bool
enableLocalHashing: React.PropTypes.bool,
// A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
})
},
mixins: [Router.State],
@ -56,29 +63,29 @@ let FileDragAndDropDialog = React.createClass({
} else {
if(this.props.multipleFiles) {
return (
<div className="file-drag-and-drop-dialog">
<p>{getLangText('Drag files here')}</p>
<span className="file-drag-and-drop-dialog">
<p>{getLangText('Drag %s here', this.props.fileClassToUpload.plural)}</p>
<p>{getLangText('or')}</p>
<span
className="btn btn-default"
onClick={this.props.onClick}>
{getLangText('choose files to upload')}
{getLangText('choose %s to upload', this.props.fileClassToUpload.plural)}
</span>
</div>
</span>
);
} else {
let dialog = queryParams.method === 'hash' ? getLangText('choose a file to hash') : getLangText('choose a file to upload');
let dialog = queryParams.method === 'hash' ? getLangText('choose a %s to hash', this.props.fileClassToUpload.singular) : getLangText('choose a %s to upload', this.props.fileClassToUpload.singular);
return (
<div className="file-drag-and-drop-dialog">
<p>{getLangText('Drag a file here')}</p>
<span className="file-drag-and-drop-dialog">
<p>{getLangText('Drag a %s here', this.props.fileClassToUpload.singular)}</p>
<p>{getLangText('or')}</p>
<span
className="btn btn-default"
onClick={this.props.onClick}>
{dialog}
</span>
</div>
</span>
);
}
}

View File

@ -4,7 +4,9 @@ import React from 'react';
import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image';
import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other';
import { getLangText } from '../../utils/lang_utils.js';
import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropPreview = React.createClass({
@ -43,6 +45,7 @@ let FileDragAndDropPreview = React.createClass({
handleDownloadFile() {
if(this.props.file.s3Url) {
// This simply opens a new browser tab with the url provided
open(this.props.file.s3Url);
}
},

View File

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

View File

@ -5,7 +5,7 @@ import React from 'react';
import FileDragAndDropPreview from './file_drag_and_drop_preview';
import FileDragAndDropPreviewProgress from './file_drag_and_drop_preview_progress';
import { displayValidFilesFilter } from './react_s3_fine_uploader_utils';
import { displayValidFilesFilter } from '../react_s3_fine_uploader_utils';
let FileDragAndDropPreviewIterator = React.createClass({

View File

@ -3,8 +3,8 @@
import React from 'react';
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js';
import AppConstants from '../../../constants/application_constants';
import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropPreviewOther = React.createClass({
propTypes: {
@ -61,7 +61,7 @@ let FileDragAndDropPreviewOther = React.createClass({
<div className="file-drag-and-drop-preview-table-wrapper">
<div className="file-drag-and-drop-preview-other">
{actionSymbol}
<span>{'.' + this.props.type}</span>
<p>{'.' + this.props.type}</p>
</div>
</div>
</div>

View File

@ -4,7 +4,8 @@ import React from 'react';
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import { displayValidProgressFilesFilter } from './react_s3_fine_uploader_utils';
import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils';
import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropPreviewProgress = React.createClass({
@ -54,7 +55,7 @@ let FileDragAndDropPreviewProgress = React.createClass({
return (
<ProgressBar
now={Math.ceil(overallProgress)}
label="Overall progress: %(percent)s%"
label={getLangText('Overall progress%s', ': %(percent)s%')}
className="ascribe-progress-bar"
style={style} />
);

View File

@ -0,0 +1,103 @@
'use strict';
import React from 'react';
import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils';
import { getLangText } from '../../../utils/lang_utils';
let UploadButton = React.createClass({
propTypes: {
onDrop: React.PropTypes.func.isRequired,
filesToUpload: React.PropTypes.array,
multiple: React.PropTypes.bool,
// For simplification purposes we're just going to use this prop as a
// label for the upload button
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
}),
allowedExtensions: React.PropTypes.string
},
handleDrop(event) {
event.preventDefault();
event.stopPropagation();
let files = event.target.files;
if(typeof this.props.onDrop === 'function' && files) {
this.props.onDrop(files);
}
},
getUploadingFiles() {
return this.props.filesToUpload.filter((file) => file.status === 'uploading');
},
handleOnClick() {
let uploadingFiles = this.getUploadingFiles();
// We only want the button to be clickable if there are no files currently uploading
if(uploadingFiles.length === 0) {
// 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);
}
},
getButtonLabel() {
let { filesToUpload, fileClassToUpload } = this.props;
// filter invalid files that might have been deleted or canceled...
filesToUpload = filesToUpload.filter(displayValidProgressFilesFilter);
// Depending on wether there is an upload going on or not we
// display the progress
if(filesToUpload.length > 0) {
return getLangText('Upload progress') + ': ' + Math.ceil(filesToUpload[0].progress) + '%';
} else {
return fileClassToUpload.singular;
}
},
render() {
let {
multiple,
fileClassToUpload,
allowedExtensions
} = this.props;
return (
<button
onClick={this.handleOnClick}
className="btn btn-default btn-sm margin-left-2px"
disabled={this.getUploadingFiles().length !== 0}>
{this.getButtonLabel()}
<input
multiple={multiple}
ref="fileinput"
type="file"
style={{
display: 'none',
height: 0,
width: 0
}}
onChange={this.handleDrop}
accept={allowedExtensions}/>
</button>
);
}
});
export default UploadButton;

View File

@ -7,7 +7,7 @@ import Q from 'q';
import S3Fetcher from '../../fetchers/s3_fetcher';
import FileDragAndDrop from './file_drag_and_drop';
import FileDragAndDrop from './ascribe_file_drag_and_drop/file_drag_and_drop';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
@ -15,12 +15,12 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import AppConstants from '../../constants/application_constants';
import { computeHashOfFile } from '../../utils/file_utils';
import { displayValidFilesFilter } from './react_s3_fine_uploader_utils';
import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils';
import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils';
var ReactS3FineUploader = React.createClass({
let ReactS3FineUploader = React.createClass({
propTypes: {
keyRoutine: React.PropTypes.shape({
url: React.PropTypes.string,
@ -37,7 +37,7 @@ var ReactS3FineUploader = React.createClass({
React.PropTypes.number
])
}),
submitKey: React.PropTypes.func,
submitFile: React.PropTypes.func,
autoUpload: React.PropTypes.bool,
debug: React.PropTypes.bool,
objectProperties: React.PropTypes.shape({
@ -84,7 +84,8 @@ var ReactS3FineUploader = React.createClass({
}),
validation: React.PropTypes.shape({
itemLimit: React.PropTypes.number,
sizeLimit: React.PropTypes.string
sizeLimit: React.PropTypes.string,
allowedExtensions: React.PropTypes.arrayOf(React.PropTypes.string)
}),
messages: React.PropTypes.shape({
unsupportedBrowser: React.PropTypes.string
@ -111,7 +112,22 @@ var ReactS3FineUploader = React.createClass({
enableLocalHashing: React.PropTypes.bool,
// automatically injected by React-Router
query: React.PropTypes.object
query: React.PropTypes.object,
// A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
}),
// Uploading functionality of react fineuploader is disconnected from its UI
// layer, which means that literally every (properly adjusted) react element
// can handle the UI handling.
fileInputElement: React.PropTypes.oneOfType([
React.PropTypes.func,
React.PropTypes.element
])
},
mixins: [Router.State],
@ -163,7 +179,12 @@ var ReactS3FineUploader = React.createClass({
return name;
},
multiple: false,
defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.')
defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.'),
fileClassToUpload: {
singular: getLangText('file'),
plural: getLangText('files')
},
fileInputElement: FileDragAndDrop
};
},
@ -313,6 +334,9 @@ var ReactS3FineUploader = React.createClass({
} else if(res.digitalwork) {
file.s3Url = res.digitalwork.url_safe;
file.s3UrlSafe = res.digitalwork.url_safe;
} else if(res.contractblob) {
file.s3Url = res.contractblob.url_safe;
file.s3UrlSafe = res.contractblob.url_safe;
} else {
throw new Error(getLangText('Could not find a url to download.'));
}
@ -384,12 +408,12 @@ var ReactS3FineUploader = React.createClass({
// Only after the blob has been created server-side, we can make the form submittable.
this.createBlob(files[id])
.then(() => {
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
// are optional, we'll only trigger them when they're actually defined
if(this.props.submitKey) {
this.props.submitKey(files[id].key);
if(this.props.submitFile) {
this.props.submitFile(files[id]);
} else {
console.warn('You didn\'t define submitKey in as a prop in react-s3-fine-uploader');
console.warn('You didn\'t define submitFile in as a prop in react-s3-fine-uploader');
}
// for explanation, check comment of if statement above
@ -424,7 +448,7 @@ var ReactS3FineUploader = React.createClass({
});
this.state.uploader.cancelAll();
let notification = new GlobalNotificationModel(this.props.defaultErrorMessage, 'danger', 5000);
let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
@ -449,7 +473,7 @@ var ReactS3FineUploader = React.createClass({
let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
// are optional, we'll only trigger them when they're actually defined
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
@ -516,7 +540,7 @@ var ReactS3FineUploader = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification);
}
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
// are optional, we'll only trigger them when they're actually defined
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
// also, lets check if after the completion of this upload,
@ -541,7 +565,7 @@ var ReactS3FineUploader = React.createClass({
this.setStatusOfFile(fileId, 'deleted');
// In some instances (when the file was already uploaded and is just displayed to the user
// - for example in the loan contract or additional files dialog)
// - for example in the contract or additional files dialog)
// 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
//
@ -816,27 +840,48 @@ var ReactS3FineUploader = React.createClass({
},
getAllowedExtensions() {
let { validation } = this.props;
if(validation && validation.allowedExtensions && validation.allowedExtensions.length > 0) {
return transformAllowedExtensionsToInputAcceptProp(validation.allowedExtensions);
} else {
return null;
}
},
render() {
return (
<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}
handleCancelHashing={this.handleCancelHashing}
multiple={this.props.multiple}
areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={this.props.areAssetsEditable}
onInactive={this.props.onInactive}
dropzoneInactive={this.isDropzoneInactive()}
hashingProgress={this.state.hashingProgress}
enableLocalHashing={this.props.enableLocalHashing} />
</div>
);
let {
multiple,
areAssetsDownloadable,
areAssetsEditable,
onInactive,
enableLocalHashing,
fileClassToUpload,
validation,
fileInputElement
} = this.props;
// Here we initialize the template that has been either provided from the outside
// or the default input that is FileDragAndDrop.
return React.createElement(fileInputElement, {
onDrop: this.handleUploadFile,
filesToUpload: this.state.filesToUpload,
handleDeleteFile: this.handleDeleteFile,
handleCancelFile: this.handleCancelFile,
handlePauseFile: this.handlePauseFile,
handleResumeFile: this.handleResumeFile,
handleCancelHashing: this.handleCancelHashing,
multiple: multiple,
areAssetsDownloadable: areAssetsDownloadable,
areAssetsEditable: areAssetsEditable,
onInactive: onInactive,
dropzoneInactive: this.isDropzoneInactive(),
hashingProgress: this.state.hashingProgress,
enableLocalHashing: enableLocalHashing,
fileClassToUpload: fileClassToUpload,
allowedExtensions: this.getAllowedExtensions()
});
}
});

View File

@ -1,5 +1,38 @@
'use strict';
export const formSubmissionValidation = {
/**
* Returns a boolean if there has been at least one file uploaded
* successfully without it being deleted or canceled.
* @param {array of files} files provided by react fine uploader
* @return {boolean}
*/
atLeastOneUploadedFile(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;
}
},
/**
* File submission for the form is optional, but if the user decides to submit a file
* the form is not ready until there are no more files currently uploading.
* @param {array of files} files files provided by react fine uploader
* @return {boolean} [description]
*/
fileOptional(files) {
let uploadingFiles = files.filter((file) => file.status === 'submitting');
if (uploadingFiles.length === 0) {
return true;
} else {
return false;
}
}
};
/**
* Filter function for filtering all deleted and canceled files
* @param {object} file A file from filesToUpload that has status as a prop.
@ -9,20 +42,6 @@ export function displayValidFilesFilter(file) {
return file.status !== 'deleted' && file.status !== 'canceled';
}
/**
* Returns a boolean if there has been at least one file uploaded
* successfully without it being deleted or canceled.
* @param {array of files} files provided by react fine uploader
* @return {Boolean}
*/
export function isReadyForFormSubmission(files) {
files = files.filter(displayValidFilesFilter);
if (files.length > 0 && files[0].status === 'upload successful') {
return true;
} else {
return false;
}
}
/**
* Filter function for which files to integrate in the progress process
@ -32,3 +51,23 @@ export function isReadyForFormSubmission(files) {
export function displayValidProgressFilesFilter(file) {
return file.status !== 'deleted' && file.status !== 'canceled' && file.status !== 'online';
}
/**
* Fineuploader allows to specify the file extensions that are allowed to upload.
* For our self defined input, we can reuse those declarations to restrict which files
* the user can pick from his hard drive.
*
* Takes an array of file extensions (['pdf', 'png', ...]) and transforms them into a string
* that can be passed into an html5 input via its 'accept' prop.
* @param {array} allowedExtensions Array of strings without a dot prefixed
* @return {string} Joined string (comma-separated) of the passed-in array
*/
export function transformAllowedExtensionsToInputAcceptProp(allowedExtensions) {
// add a dot in front of the extension
let prefixedAllowedExtensions = allowedExtensions.map((ext) => '.' + ext);
// generate a comma separated list to add them to the DOM element
// See: http://stackoverflow.com/questions/4328947/limit-file-format-when-using-input-type-file
return prefixedAllowedExtensions.join(', ');
}

View File

@ -84,10 +84,11 @@ let CoaVerifyForm = React.createClass({
</Property>
<Property
name='signature'
label="Signature">
label="Signature"
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={3}
editable={true}
placeholder={getLangText('Copy paste the signature on the bottom of your Certificate of Authenticity')}
required/>
</Property>

View File

@ -0,0 +1,36 @@
'use strict';
import React from 'react';
import NotificationStore from '../stores/notification_store';
import { mergeOptions } from '../utils/general_utils';
let ContractNotification = React.createClass({
getInitialState() {
return mergeOptions(
NotificationStore.getState()
);
},
componentDidMount() {
NotificationStore.listen(this.onChange);
},
componentWillUnmount() {
NotificationStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
return (
null
);
}
});
export default ContractNotification;

View File

@ -0,0 +1,43 @@
'use strict';
import React from 'react';
let GlobalAction = React.createClass({
propTypes: {
requestActions: React.PropTypes.object
},
render() {
let pieceActions = null;
if (this.props.requestActions && this.props.requestActions.pieces){
pieceActions = this.props.requestActions.pieces.map((item) => {
return (
<div className="ascribe-global-action">
{item}
</div>);
});
}
let editionActions = null;
if (this.props.requestActions && this.props.requestActions.editions){
editionActions = Object.keys(this.props.requestActions.editions).map((pieceId) => {
return this.props.requestActions.editions[pieceId].map((item) => {
return (
<div className="ascribe-global-action">
{item}
</div>);
});
});
}
if (pieceActions || editionActions) {
return (
<div className="ascribe-global-action-wrapper">
{pieceActions}
{editionActions}
</div>);
}
return null;
}
});
export default GlobalAction;

View File

@ -2,13 +2,6 @@
import React from 'react';
import Router from 'react-router';
import Favico from 'favico.js';
import UserActions from '../actions/user_actions';
import UserStore from '../stores/user_store';
import WhitelabelActions from '../actions/whitelabel_actions';
import WhitelabelStore from '../stores/whitelabel_store';
import EventActions from '../actions/event_actions';
import Nav from 'react-bootstrap/lib/Nav';
import Navbar from 'react-bootstrap/lib/Navbar';
@ -18,6 +11,17 @@ import MenuItem from 'react-bootstrap/lib/MenuItem';
import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink';
import NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
import AclProxy from './acl_proxy';
import UserActions from '../actions/user_actions';
import UserStore from '../stores/user_store';
import WhitelabelActions from '../actions/whitelabel_actions';
import WhitelabelStore from '../stores/whitelabel_store';
import EventActions from '../actions/event_actions';
import HeaderNotifications from './header_notification';
import HeaderNotificationDebug from './header_notification_debug';
import NavRoutesLinks from './nav_routes_links';
@ -25,6 +29,7 @@ import NavRoutesLinks from './nav_routes_links';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
let setFavicon = require('favicon-setter');
let Header = React.createClass({
propTypes: {
@ -41,7 +46,10 @@ let Header = React.createClass({
},
getInitialState() {
return mergeOptions(WhitelabelStore.getState(), UserStore.getState());
return mergeOptions(
WhitelabelStore.getState(),
UserStore.getState()
);
},
componentDidMount() {
@ -56,59 +64,104 @@ let Header = React.createClass({
WhitelabelStore.unlisten(this.onChange);
},
getLogo(){
let logo = (
if (this.state.whitelabel && this.state.whitelabel.logo){
let logoPath = this.state.whitelabel.logo;
let logo = <img className="img-brand" src={logoPath} />;
console.log('should change browser icon');
console.log(logoPath);
try {
setFavicon(logoPath);
}
catch (e){
console.log(e.message());
}
return logo;
}
return (
<span>
<span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span>
</span>);
if (this.state.whitelabel && this.state.whitelabel.logo){
let logoPath = this.state.whitelabel.logo;
logo = <img className="img-brand" src={logoPath} />;
let favicon = new Favico();
let image = new Image();
image.src = logoPath;
console.log('should change browser icon');
console.log(logoPath);
favicon.image(image);
console.log(image);
console.log(favicon);
}
return logo;
},
getPoweredBy(){
if (this.state.whitelabel && this.state.whitelabel.logo) {
return (
<li>
<a className="pull-right" href="https://www.ascribe.io/" target="_blank">
<span id="powered">{getLangText('powered by')} </span>
<span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span>
</a>
</li>
);
}
return null;
return (
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_view_powered_by">
<li>
<a className="pull-right" href="https://www.ascribe.io/" target="_blank">
<span id="powered">{getLangText('powered by')} </span>
<span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span>
</a>
</li>
</AclProxy>
);
},
onChange(state) {
this.setState(state);
if(this.state.currentUser && this.state.currentUser.email) {
EventActions.profileDidLoad.defer(this.state.currentUser);
}
},
onMenuItemClick() {
/*
This is a hack to make the dropdown close after clicking on an item
The function just need to be defined
from https://github.com/react-bootstrap/react-bootstrap/issues/368:
@jvillasante - Have you tried to use onSelect with the DropdownButton?
I don't have a working example that is exactly like yours,
but I just noticed that the Dropdown closes when I've attached an event handler to OnSelect:
<DropdownButton eventKey={3} title="Admin" onSelect={ this.OnSelected } >
onSelected: function(e) {
// doesn't need to have functionality (necessarily) ... just wired up
}
Internally, a call to DropdownButton.setDropDownState(false) is made which will hide the dropdown menu.
So, you should be able to call that directly on the DropdownButton instance as well if needed.
NOW, THAT DIDN'T WORK - the onSelect routine isnt triggered in all cases
Hence, we do this manually
*/
this.refs.dropdownbutton.setDropdownState(false);
},
render() {
let account;
let signup;
let navRoutesLinks;
if (this.state.currentUser.username){
account = (
<DropdownButton eventKey="1" title={this.state.currentUser.username}>
<MenuItemLink eventKey="2" to="settings">{getLangText('Account Settings')}</MenuItemLink>
<DropdownButton
ref='dropdownbutton'
eventKey="1"
title={this.state.currentUser.username}>
<MenuItemLink
eventKey="2"
to="settings"
onClick={this.onMenuItemClick}>
{getLangText('Account Settings')}
</MenuItemLink>
<AclProxy
aclObject={this.state.currentUser.acl}
aclName="acl_view_settings_contract">
<MenuItemLink
to="contract_settings"
onClick={this.onMenuItemClick}>
{getLangText('Contract Settings')}
</MenuItemLink>
</AclProxy>
<MenuItem divider />
<MenuItemLink eventKey="3" to="logout">{getLangText('Log out')}</MenuItemLink>
</DropdownButton>
</DropdownButton>
);
navRoutesLinks = <NavRoutesLinks routes={this.props.routes} navbar right/>;
navRoutesLinks = <NavRoutesLinks routes={this.props.routes} userAcl={this.state.currentUser.acl} navbar right/>;
}
else {
account = <NavItemLink to="login">{getLangText('LOGIN')}</NavItemLink>;
@ -132,6 +185,7 @@ let Header = React.createClass({
{account}
{signup}
</Nav>
<HeaderNotifications />
{navRoutesLinks}
</CollapsibleNav>
</Navbar>

View File

@ -0,0 +1,218 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import MenuItem from 'react-bootstrap/lib/MenuItem';
import Nav from 'react-bootstrap/lib/Nav';
import NotificationActions from '../actions/notification_actions';
import NotificationStore from '../stores/notification_store';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
let Link = Router.Link;
let HeaderNotifications = React.createClass({
getInitialState() {
return mergeOptions(
NotificationStore.getState()
);
},
componentDidMount() {
NotificationStore.listen(this.onChange);
NotificationActions.fetchPieceListNotifications();
NotificationActions.fetchEditionListNotifications();
},
componentWillUnmount() {
NotificationStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
onMenuItemClick(event) {
/*
This is a hack to make the dropdown close after clicking on an item
The function just need to be defined
from https://github.com/react-bootstrap/react-bootstrap/issues/368:
@jvillasante - Have you tried to use onSelect with the DropdownButton?
I don't have a working example that is exactly like yours,
but I just noticed that the Dropdown closes when I've attached an event handler to OnSelect:
<DropdownButton eventKey={3} title="Admin" onSelect={ this.OnSelected } >
onSelected: function(e) {
// doesn't need to have functionality (necessarily) ... just wired up
}
Internally, a call to DropdownButton.setDropDownState(false) is made which will hide the dropdown menu.
So, you should be able to call that directly on the DropdownButton instance as well if needed.
NOW, THAT DIDN'T WORK - the onSelect routine isnt triggered in all cases
Hence, we do this manually
*/
this.refs.dropdownbutton.setDropdownState(false);
},
getPieceNotifications(){
if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) {
return (
<div>
<div className="notification-header">
Artworks ({this.state.pieceListNotifications.length})
</div>
{this.state.pieceListNotifications.map((pieceNotification, i) => {
return (
<MenuItem eventKey={i + 2}>
<NotificationListItem
ref={i}
notification={pieceNotification.notification}
pieceOrEdition={pieceNotification.piece}
onClick={this.onMenuItemClick}/>
</MenuItem>
);
}
)}
</div>
);
}
return null;
},
getEditionNotifications(){
if (this.state.editionListNotifications && this.state.editionListNotifications.length > 0) {
return (
<div>
<div className="notification-header">
Editions ({this.state.editionListNotifications.length})
</div>
{this.state.editionListNotifications.map((editionNotification, i) => {
return (
<MenuItem eventKey={i + 2}>
<NotificationListItem
ref={'edition' + i}
notification={editionNotification.notification}
pieceOrEdition={editionNotification.edition}
onClick={this.onMenuItemClick}/>
</MenuItem>
);
}
)}
</div>
);
}
return null;
},
render() {
if ((this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) ||
(this.state.editionListNotifications && this.state.editionListNotifications.length > 0)){
let numNotifications = 0;
if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) {
numNotifications += this.state.pieceListNotifications.length;
}
if (this.state.editionListNotifications && this.state.editionListNotifications.length > 0) {
numNotifications += this.state.editionListNotifications.length;
}
return (
<Nav navbar right>
<DropdownButton
ref='dropdownbutton'
eventKey="1"
title={
<span>
<Glyphicon glyph='envelope' color="green"/>
<span className="notification-amount">({numNotifications})</span>
</span>
}
className="notification-menu">
{this.getPieceNotifications()}
{this.getEditionNotifications()}
</DropdownButton>
</Nav>
);
}
return null;
}
});
let NotificationListItem = React.createClass({
propTypes: {
notification: React.PropTypes.array,
pieceOrEdition: React.PropTypes.object,
onClick: React.PropTypes.func
},
isPiece() {
return !(this.props.pieceOrEdition && this.props.pieceOrEdition.parent);
},
getLinkData() {
if (this.isPiece()) {
return {
to: 'piece',
params: {
pieceId: this.props.pieceOrEdition.id
}
};
} else {
return {
to: 'edition',
params: {
editionId: this.props.pieceOrEdition.bitcoin_id
}
};
}
},
onClick(event){
this.props.onClick(event);
},
getNotificationText(){
let numNotifications = null;
if (this.props.notification.length > 1){
numNotifications = <div>+ {this.props.notification.length - 1} more...</div>;
}
return (
<div className="notification-action">
{this.props.notification[0].action_str}
{numNotifications}
</div>);
},
render() {
if (this.props.pieceOrEdition) {
return (
<Link {...this.getLinkData()} onClick={this.onClick}>
<div className="row notification-wrapper">
<div className="col-xs-4 clear-paddings">
<div className="thumbnail-wrapper">
<img src={this.props.pieceOrEdition.thumbnail.url_safe}/>
</div>
</div>
<div className="col-xs-8 notification-list-item-header">
<h1>{this.props.pieceOrEdition.title}</h1>
<div className="sub-header">by {this.props.pieceOrEdition.artist_name}</div>
{this.getNotificationText()}
</div>
</div>
</Link>);
}
return null;
}
});
export default HeaderNotifications;

View File

@ -19,7 +19,7 @@ let LogoutContainer = React.createClass({
Alt.flush();
// kill intercom (with fire)
window.Intercom('shutdown');
this.transitionTo(baseUrl);
this.replaceWith(baseUrl);
})
.catch((err) => {
console.logGlobal(err);

View File

@ -3,53 +3,80 @@
import React from 'react';
import Nav from 'react-bootstrap/lib/Nav';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink';
import NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
import NavRoutesLinksLink from './nav_routes_links_link';
import AclProxy from './acl_proxy';
import { sanitizeList } from '../utils/general_utils';
let NavRoutesLinks = React.createClass({
propTypes: {
routes: React.PropTypes.element
routes: React.PropTypes.element,
userAcl: React.PropTypes.object
},
extractLinksFromRoutes(node, i) {
/**
* This method generales a bunch of react-bootstrap specific links
* from the routes we defined in one of the specific routes.js file
*
* We can define a headerTitle as well as a aclName and according to that the
* link will be created for a specific user
* @param {ReactElement} node Starts at the very top of a routes files root
* @param {object} userAcl ACL object we use throughout the whole app
* @param {number} i Depth of the route in comparison to the root
* @return {Array} Array of ReactElements that can be displayed to the user
*/
extractLinksFromRoutes(node, userAcl, i) {
if(!node) {
return;
}
node = node.props;
let links = node.props.children.map((child, j) => {
let childrenFn = null;
let { aclName, headerTitle, name, children } = child.props;
let links = node.children.map((child, j) => {
// If the node has children that could be rendered, then we want
// to execute this function again with the child as the root
//
// Otherwise we'll just pass childrenFn as false
if(child.props.children && child.props.children.length > 0) {
childrenFn = this.extractLinksFromRoutes(child, userAcl, i++);
}
// check if this a candidate for a link generation
if(child.props.headerTitle && typeof child.props.headerTitle === 'string') {
// also check if it is a candidate for generating a dropdown menu
if(child.props.children && child.props.children.length > 0) {
// We validate if the user has set the title correctly,
// otherwise we're not going to render his route
if(headerTitle && typeof headerTitle === 'string') {
// if there is an aclName present on the route definition,
// we evaluate it against the user's acl
if(aclName && typeof aclName !== 'undefined') {
return (
<DropdownButton title={child.props.headerTitle} key={j}>
{this.extractLinksFromRoutes(child, i++)}
</DropdownButton>
);
} else if(i === 1) {
// if the node's child is actually a node of level one (a child of a node), we're
// returning a DropdownButton matching MenuItemLink
return (
<MenuItemLink to={child.props.name} key={j}>{child.props.headerTitle}</MenuItemLink>
);
} else if(i === 0) {
return (
<NavItemLink to={child.props.name} key={j}>{child.props.headerTitle}</NavItemLink>
<AclProxy
key={j}
aclName={aclName}
aclObject={this.props.userAcl}>
<NavRoutesLinksLink
headerTitle={headerTitle}
routeName={name}
depth={i}
children={childrenFn}/>
</AclProxy>
);
} else {
return null;
return (
<NavRoutesLinksLink
key={j}
headerTitle={headerTitle}
routeName={name}
depth={i}
children={childrenFn}/>
);
}
} else {
return null;
}
});
// remove all nulls from the list of generated links
@ -57,9 +84,11 @@ let NavRoutesLinks = React.createClass({
},
render() {
let {routes, userAcl} = this.props;
return (
<Nav {...this.props}>
{this.extractLinksFromRoutes(this.props.routes, 0)}
{this.extractLinksFromRoutes(routes, userAcl, 0)}
</Nav>
);
}

View File

@ -0,0 +1,51 @@
'use strict';
import React from 'react';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink';
import NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
let NavRoutesLinksLink = React.createClass({
propTypes: {
headerTitle: React.PropTypes.string,
routeName: React.PropTypes.string,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]),
depth: React.PropTypes.number
},
render() {
let { children, headerTitle, depth, routeName } = this.props;
// if the route has children, we're returning a DropdownButton that will get filled
// with MenuItemLinks
if(children) {
return (
<DropdownButton title={headerTitle}>
{children}
</DropdownButton>
);
} else {
if(depth === 1) {
// if the node's child is actually a node of level one (a child of a node), we're
// returning a DropdownButton matching MenuItemLink
return (
<MenuItemLink to={routeName}>{headerTitle}</MenuItemLink>
);
} else if(depth === 0) {
return (
<NavItemLink to={routeName}>{headerTitle}</NavItemLink>
);
} else {
return null;
}
}
}
});
export default NavRoutesLinksLink;

View File

@ -15,12 +15,16 @@ import AccordionListItemTableEditions from './ascribe_accordion_list/accordion_l
import Pagination from './ascribe_pagination/pagination';
import PieceListFilterDisplay from './piece_list_filter_display';
import PieceListBulkModal from './ascribe_piece_list_bulk_modal/piece_list_bulk_modal';
import PieceListToolbar from './ascribe_piece_list_toolbar/piece_list_toolbar';
import AppConstants from '../constants/application_constants';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
let PieceList = React.createClass({
propTypes: {
@ -30,7 +34,6 @@ let PieceList = React.createClass({
filterParams: React.PropTypes.array,
orderParams: React.PropTypes.array,
orderBy: React.PropTypes.string
},
mixins: [Router.Navigation, Router.State],
@ -39,13 +42,14 @@ let PieceList = React.createClass({
return {
accordionListItemType: AccordionListItemWallet,
orderParams: ['artist_name', 'title'],
filterParams: [
'acl_transfer',
'acl_consign',
{
key: 'acl_create_editions',
label: 'create editions'
}]
filterParams: [{
label: getLangText('Show works I can'),
items: [
'acl_transfer',
'acl_consign',
'acl_create_editions'
]
}]
};
},
getInitialState() {
@ -60,18 +64,18 @@ let PieceList = React.createClass({
PieceListStore.listen(this.onChange);
EditionListStore.listen(this.onChange);
let orderBy = this.props.orderBy ? this.props.orderBy : this.state.orderBy;
if (this.state.pieceList.length === 0 || this.state.page !== page){
PieceListActions.fetchPieceList(page, this.state.pageSize, this.state.search,
orderBy, this.state.orderAsc, this.state.filterBy)
.then(() => PieceListActions.fetchPieceRequestActions());
orderBy, this.state.orderAsc, this.state.filterBy);
}
},
componentDidUpdate() {
if (this.props.redirectTo && this.state.unfilteredPieceListCount === 0) {
// FIXME: hack to redirect out of the dispatch cycle
window.setTimeout(() => this.transitionTo(this.props.redirectTo), 0);
window.setTimeout(() => this.transitionTo(this.props.redirectTo, this.getQuery()));
}
},
@ -90,8 +94,8 @@ let PieceList = React.createClass({
// the site should go to the top
document.body.scrollTop = document.documentElement.scrollTop = 0;
PieceListActions.fetchPieceList(page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc,
this.state.filterBy);
this.state.orderBy, this.state.orderAsc,
this.state.filterBy);
};
},
@ -147,6 +151,7 @@ let PieceList = React.createClass({
render() {
let loadingElement = (<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />);
let AccordionListItemType = this.props.accordionListItemType;
return (
<div>
<PieceListToolbar
@ -161,6 +166,9 @@ let PieceList = React.createClass({
{this.props.customSubmitButton}
</PieceListToolbar>
<PieceListBulkModal className="ascribe-piece-list-bulk-modal" />
<PieceListFilterDisplay
filterBy={this.state.filterBy}
filterParams={this.props.filterParams}/>
<AccordionList
className="ascribe-accordion-list"
changeOrder={this.accordionChangeOrder}

View File

@ -0,0 +1,118 @@
'use strict';
import React from 'react';
let PieceListFilterDisplay = React.createClass({
propTypes: {
filterBy: React.PropTypes.object,
filterParams: React.PropTypes.arrayOf(
React.PropTypes.shape({
label: React.PropTypes.string,
items: React.PropTypes.arrayOf(
React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.shape({
key: React.PropTypes.string,
label: React.PropTypes.string
})
])
)
})
)
},
/**
* Takes the above described filterParams prop,
* assigns it it's true filterBy value that is derived from the filterBy prop
* and also - if there wasn't already one defined - generates a label
* @return {object}
*/
transformFilterParamsItemsToBools() {
let { filterParams, filterBy } = this.props;
return filterParams.map((filterParam) => {
return {
label: filterParam.label,
items: filterParam.items.map((item) => {
if(typeof item !== 'string' && typeof item.key === 'string' && typeof item.label === 'string') {
return {
key: item.key,
label: item.label,
value: filterBy[item.key] || false
};
} else {
return {
key: item,
label: item.split('acl_')[1].replace(/_/g, ' '),
value: filterBy[item] || false
};
}
})
};
});
},
/**
* Takes the list of filters generated in transformFilterParamsItemsToBools and
* transforms them into human readable text.
* @param {Object} filtersWithLabel An object of the shape {key: <String>, label: <String>, value: <Bool>}
* @return {string} A human readable string
*/
getFilterText(filtersWithLabel) {
let filterTextList = filtersWithLabel
// Iterate over all provided filterLabels and generate a list
// of human readable strings
.map((filterWithLabel) => {
let activeFilterWithLabel = filterWithLabel
.items
// If the filter is active (which it is when its value is true),
// we're going to include it's label into a list,
// otherwise we'll just return nothing
.map((filter) => {
if(filter.value) {
return filter.label;
}
})
// if nothing is returned, that index is 'undefined'.
// As we only want active filter, we filter out all falsy values e.g. undefined
.filter((filterName) => !!filterName)
// and join the result to a string
.join(', ');
// If this actually didn't generate an empty string,
// we take the label and concat it to the result.
if(activeFilterWithLabel) {
return filterWithLabel.label + ': ' + activeFilterWithLabel;
}
})
// filter out strings that are undefined, as their filter's were not activated
.filter((filterText) => !!filterText)
// if there are multiple sentences, capitalize the first one and lowercase the others
.map((filterText, i) => i === 0 ? filterText.charAt(0).toUpperCase() + filterText.substr(1) : filterText.charAt(0).toLowerCase() + filterText.substr(1))
.join(' and ');
return filterTextList;
},
render() {
let { filterBy } = this.props;
let filtersWithLabel = this.transformFilterParamsItemsToBools();
// do not show the FilterDisplay if there are no filters applied
if(filterBy && Object.keys(filterBy).length === 0) {
return null;
} else {
return (
<div className="row">
<div className="ascribe-piece-list-filter-display col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2">
{this.getFilterText(filtersWithLabel)}
<hr />
</div>
</div>
);
}
}
});
export default PieceListFilterDisplay;

View File

@ -1,408 +0,0 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import UserActions from '../actions/user_actions';
import UserStore from '../stores/user_store';
import WalletSettingsActions from '../actions/wallet_settings_actions';
import WalletSettingsStore from '../stores/wallet_settings_store';
import ApplicationActions from '../actions/application_actions';
import ApplicationStore from '../stores/application_store';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import ReactS3FineUploader from './ascribe_uploader/react_s3_fine_uploader';
import CollapsibleParagraph from './ascribe_collapsible/collapsible_paragraph';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import InputCheckbox from './ascribe_forms/input_checkbox';
import ActionPanel from './ascribe_panel/action_panel';
import ApiUrls from '../constants/api_urls';
import AppConstants from '../constants/application_constants';
import { getLangText } from '../utils/lang_utils';
import { getCookie } from '../utils/fetch_api_utils';
let SettingsContainer = React.createClass({
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element])
},
mixins: [Router.Navigation],
render() {
return (
<div className="settings-container">
<AccountSettings />
{this.props.children}
<APISettings />
<BitcoinWalletSettings />
<LoanContractSettings />
<br />
<br />
</div>
);
}
});
let AccountSettings = React.createClass({
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
handleSuccess(){
UserActions.fetchCurrentUser();
let notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getFormDataProfile(){
return {'email': this.state.currentUser.email};
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
let profile = null;
if (this.state.currentUser.username) {
content = (
<Form
url={ApiUrls.users_username}
handleSuccess={this.handleSuccess}>
<Property
name='username'
label={getLangText('Username')}>
<input
type="text"
defaultValue={this.state.currentUser.username}
placeholder={getLangText('Enter your username')}
required/>
</Property>
<Property
name='email'
label={getLangText('Email')}
editable={false}>
<input
type="text"
defaultValue={this.state.currentUser.email}
placeholder={getLangText('Enter your username')}
required/>
</Property>
<hr />
</Form>
);
profile = (
<Form
url={ApiUrls.users_profile}
handleSuccess={this.handleSuccess}
getFormData={this.getFormDataProfile}>
<Property
name="hash_locally"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox
defaultChecked={this.state.currentUser.profile.hash_locally}>
<span>
{' ' + getLangText('Enable hash option, e.g. slow connections or to keep piece private')}
</span>
</InputCheckbox>
</Property>
<hr />
{/*<Property
name='language'
label={getLangText('Choose your Language')}
editable={true}>
<select id="select-lang" name="language">
<option value="fr">
Fran&ccedil;ais
</option>
<option value="en"
selected="selected">
English
</option>
</select>
</Property>*/}
</Form>
);
}
return (
<CollapsibleParagraph
title={getLangText('Account')}
show={true}
defaultExpanded={true}>
{content}
{profile}
{/*<Form
url={AppConstants.serverUrl + 'api/users/set_language/'}>
<Property
name='language'
label={getLangText('Choose your Language')}
editable={true}>
<select id="select-lang" name="language">
<option value="fr">
Fran&ccedil;ais
</option>
<option value="en"
selected="selected">
English
</option>
</select>
</Property>
<hr />
</Form>*/}
</CollapsibleParagraph>
);
}
});
let BitcoinWalletSettings = React.createClass({
propTypes: {
defaultExpanded: React.PropTypes.bool
},
getInitialState() {
return WalletSettingsStore.getState();
},
componentDidMount() {
WalletSettingsStore.listen(this.onChange);
WalletSettingsActions.fetchWalletSettings();
},
componentWillUnmount() {
WalletSettingsStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
if (this.state.walletSettings.btc_public_key) {
content = (
<Form >
<Property
name='btc_public_key'
label={getLangText('Bitcoin public key')}
editable={false}>
<pre className="ascribe-pre">{this.state.walletSettings.btc_public_key}</pre>
</Property>
<Property
name='btc_root_address'
label={getLangText('Root Address')}
editable={false}>
<pre className="ascribe-pre">{this.state.walletSettings.btc_root_address}</pre>
</Property>
<hr />
</Form>);
}
return (
<CollapsibleParagraph
title={getLangText('Crypto Wallet')}
show={true}
defaultExpanded={this.props.defaultExpanded}>
{content}
</CollapsibleParagraph>
);
}
});
let LoanContractSettings = React.createClass({
propTypes: {
defaultExpanded: React.PropTypes.bool
},
render() {
return (
<CollapsibleParagraph
title="Loan Contract Settings"
show={true}
defaultExpanded={this.props.defaultExpanded}>
<FileUploader />
</CollapsibleParagraph>
);
}
});
let FileUploader = React.createClass({
propTypes: {
},
render() {
return (
<Form>
<Property
label="Contract file">
<ReactS3FineUploader
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'contract'
}}
createBlobRoutine={{
url: ApiUrls.ownership_loans_contract
}}
validation={{
itemLimit: 100000,
sizeLimit: '50000000'
}}
session={{
endpoint: ApiUrls.ownership_loans_contract,
customHeaders: {
'X-CSRFToken': getCookie(AppConstants.csrftoken)
},
cors: {
expected: true,
sendCredentials: true
}
}}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/',
customHeaders: {
'X-CSRFToken': getCookie(AppConstants.csrftoken)
}
}}
deleteFile={{
enabled: true,
method: 'DELETE',
endpoint: AppConstants.serverUrl + 's3/delete',
customHeaders: {
'X-CSRFToken': getCookie(AppConstants.csrftoken)
}
}}
areAssetsDownloadable={true}
areAssetsEditable={true}/>
</Property>
<hr />
</Form>
);
}
});
let APISettings = React.createClass({
propTypes: {
defaultExpanded: React.PropTypes.bool
},
getInitialState() {
return ApplicationStore.getState();
},
componentDidMount() {
ApplicationStore.listen(this.onChange);
ApplicationActions.fetchApplication();
},
componentWillUnmount() {
ApplicationStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
handleCreateSuccess() {
ApplicationActions.fetchApplication();
let notification = new GlobalNotificationModel(getLangText('Application successfully created'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
handleTokenRefresh(event) {
let applicationName = event.target.getAttribute('data-id');
ApplicationActions.refreshApplicationToken(applicationName);
let notification = new GlobalNotificationModel(getLangText('Token refreshed'), 'success', 2000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getApplications(){
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
if (this.state.applications.length > -1) {
content = this.state.applications.map(function(app, i) {
return (
<ActionPanel
name={app.name}
key={i}
content={
<div>
<div className='ascribe-panel-title'>
{app.name}
</div>
<div className="ascribe-panel-subtitle">
{'Bearer ' + app.bearer_token.token}
</div>
</div>
}
buttons={
<div className="pull-right">
<div className="pull-right">
<button
className="pull-right btn btn-default btn-sm"
onClick={this.handleTokenRefresh}
data-id={app.name}>
{getLangText('REFRESH')}
</button>
</div>
</div>
}/>
);
}, this);
}
return content;
},
render() {
return (
<CollapsibleParagraph
title={getLangText('API Integration')}
show={true}
defaultExpanded={this.props.defaultExpanded}>
<Form
url={ApiUrls.applications}
handleSuccess={this.handleCreateSuccess}>
<Property
name='name'
label={getLangText('Application Name')}>
<input
type="text"
placeholder={getLangText('Enter the name of your app')}
required/>
</Property>
<hr />
</Form>
<pre>
Usage: curl &lt;url&gt; -H 'Authorization: Bearer &lt;token&gt;'
</pre>
{this.getApplications()}
</CollapsibleParagraph>
);
}
});
export default SettingsContainer;

View File

@ -1,8 +1,13 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import SignupForm from './ascribe_forms/form_signup';
import { getLangText } from '../utils/lang_utils';
let Link = Router.Link;
let SignupContainer = React.createClass({
getInitialState() {
@ -33,7 +38,11 @@ let SignupContainer = React.createClass({
return (
<div className="ascribe-login-wrapper">
<SignupForm handleSuccess={this.handleSuccess} />
<div className="ascribe-login-text">
{getLangText('Already an ascribe user')}&#63; <Link to="login">{getLangText('Log in')}...</Link><br/>
</div>
</div>
);
}
});

View File

@ -124,7 +124,7 @@ let AccordionListItemPrize = React.createClass({
<div>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_submit">
aclName="acl_wallet_submit">
<SubmitToPrizeButton
className="pull-right"
piece={this.props.content}

View File

@ -86,7 +86,19 @@ let PieceContainer = React.createClass({
loadPiece() {
PieceActions.fetchOne(this.props.params.pieceId);
this.setState(this.state);
},
getActions() {
if (this.state.piece &&
this.state.piece.notifications &&
this.state.piece.notifications.length > 0) {
return (
<ListRequestActions
pieceOrEditions={this.state.piece}
currentUser={this.state.currentUser}
handleSuccess={this.loadPiece}
notifications={this.state.piece.notifications}/>);
}
},
render() {
@ -121,11 +133,7 @@ let PieceContainer = React.createClass({
<DetailProperty label={getLangText('BY')} value={artistName} />
<DetailProperty label={getLangText('DATE')} value={ this.state.piece.date_created.slice(0, 4) } />
{artistEmail}
<ListRequestActions
pieceOrEditions={this.state.piece}
currentUser={this.state.currentUser}
handleSuccess={this.loadPiece}
requestActions={this.state.piece.request_action}/>
{this.getActions()}
<hr/>
</div>
}
@ -301,7 +309,6 @@ let PrizePieceRatings = React.createClass({
<div>
<CollapsibleParagraph
title={getLangText('Shortlisting')}
show={true}
defaultExpanded={true}>
<div className="row no-margin">
<span className="ascribe-checkbox-wrapper" style={{marginLeft: '1.5em'}}>
@ -321,7 +328,6 @@ let PrizePieceRatings = React.createClass({
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('Average Rating')}
show={true}
defaultExpanded={true}>
<div id="list-rating" style={{marginLeft: '1.5em', marginBottom: '1em'}}>
<StarRating
@ -367,7 +373,6 @@ let PrizePieceRatings = React.createClass({
return (
<CollapsibleParagraph
title={getLangText('Rating')}
show={true}
defaultExpanded={true}>
<div style={{marginLeft: '1.5em', marginBottom: '1em'}}>
<StarRating
@ -409,7 +414,6 @@ let PrizePieceDetails = React.createClass({
return (
<CollapsibleParagraph
title={getLangText('Prize Details')}
show={true}
defaultExpanded={true}>
<Form ref='form'>
{Object.keys(this.props.piece.extra_data).map((data) => {
@ -418,10 +422,10 @@ let PrizePieceDetails = React.createClass({
<Property
name={data}
label={label}
editable={false}>
editable={false}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
editable={false}
defaultValue={this.props.piece.extra_data[data]}/>
</Property>);
}

View File

@ -69,7 +69,7 @@ let PrizePieceList = React.createClass({
accordionListItemType={AccordionListItemPrize}
orderParams={orderParams}
orderBy={this.state.currentUser.is_jury ? 'rating' : null}
filterParams={null}
filterParams={[]}
customSubmitButton={this.getButtonSubmit()}/>
</div>
);

View File

@ -41,20 +41,20 @@ let PrizeRegisterPiece = React.createClass({
<Property
name='artist_statement'
label={getLangText('Artist statement')}
editable={true}>
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
editable={true}
placeholder={getLangText('Enter your statement')}
required="required"/>
</Property>
<Property
name='work_description'
label={getLangText('Work description')}
editable={true}>
editable={true}
overrideForm={true}>
<InputTextAreaToggable
rows={1}
editable={true}
placeholder={getLangText('Enter the description for your work')}
required="required"/>
</Property>

View File

@ -9,7 +9,7 @@ import PrizeStore from '../stores/prize_store';
import PrizeJuryActions from '../actions/prize_jury_actions';
import PrizeJuryStore from '../stores/prize_jury_store';
import SettingsContainer from '../../../settings_container';
import SettingsContainer from '../../../ascribe_settings/settings_container';
import CollapsibleParagraph from '../../../ascribe_collapsible/collapsible_paragraph';
import Form from '../../../ascribe_forms/form';
@ -79,7 +79,6 @@ let PrizeSettings = React.createClass({
return (
<CollapsibleParagraph
title={'Prize Settings for ' + this.state.prize.name}
show={true}
defaultExpanded={true}>
<Form >
<Property
@ -190,7 +189,7 @@ let PrizeJurySettings = React.createClass({
{getLangText('RESEND')}
</button>
<button
className="btn btn-default btn-sm ascribe-btn-gray margin-left-2px"
className="btn btn-warning btn-sm margin-left-2px"
onClick={this.handleRevoke}
data-id={member.email}>
{getLangText('REVOKE')}
@ -218,7 +217,7 @@ let PrizeJurySettings = React.createClass({
}
buttons={
<button
className="btn btn-default btn-sm ascribe-btn-gray"
className="btn btn-warning btn-sm"
onClick={this.handleRevoke}
data-id={member.email}>
{getLangText('REVOKE')}
@ -265,22 +264,19 @@ let PrizeJurySettings = React.createClass({
if (this.state.members.length > -1) {
content = (
<div style={{padding: '1em'}}>
<div>
<CollapsibleParagraph
title={getLangText('Active Jury Members')}
show={true}
defaultExpanded={true}>
{this.getMembersActive()}
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('Pending Jury Invitations')}
show={true}
defaultExpanded={true}>
{this.getMembersPending()}
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('Deactivated Jury Members')}
show={true}
defaultExpanded={false}>
{this.getMembersInactive()}
</CollapsibleParagraph>
@ -318,4 +314,4 @@ let PrizeJurySettings = React.createClass({
});
export default Settings;
export default Settings;

View File

@ -27,7 +27,7 @@ let PrizeApp = React.createClass({
}
return (
<div className="container ascribe-prize-app">
<div className={'container ascribe-prize-app client--' + subdomain}>
{header}
<RouteHandler />
<GlobalNotification />

View File

@ -13,6 +13,7 @@ import PrizePieceList from './components/prize_piece_list';
import PrizePieceContainer from './components/ascribe_detail/prize_piece_container';
import EditionContainer from '../../ascribe_detail/edition_container';
import SettingsContainer from './components/prize_settings_container';
import CoaVerifyContainer from '../../../components/coa_verify_container';
import App from './prize_app';
import AppConstants from '../../../constants/application_constants';
@ -34,6 +35,7 @@ function getRoutes() {
<Route name="piece" path="pieces/:pieceId" handler={PrizePieceContainer} />
<Route name="edition" path="editions/:editionId" handler={EditionContainer} />
<Route name="settings" path="settings" handler={SettingsContainer} />
<Route name="coa_verify" path="verify" handler={CoaVerifyContainer} />
</Route>
);
}

View File

@ -0,0 +1,74 @@
'use strict';
import React from 'react';
import ListRequestActions from '../../../../ascribe_forms/list_form_request_actions';
import AclButtonList from '../../../../ascribe_buttons/acl_button_list';
import DeleteButton from '../../../../ascribe_buttons/delete_button';
import AclProxy from '../../../../acl_proxy';
import { mergeOptions } from '../../../../../utils/general_utils';
let WalletActionPanel = React.createClass({
propTypes: {
piece: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired,
loadPiece: React.PropTypes.func.isRequired,
submitButtonType: React.PropTypes.func.isRequired
},
render(){
if (this.props.piece &&
this.props.piece.notifications &&
this.props.piece.notifications.length > 0) {
return (
<ListRequestActions
pieceOrEditions={this.props.piece}
currentUser={this.props.currentUser}
handleSuccess={this.props.loadPiece}
notifications={this.props.piece.notifications}/>);
}
else {
//We need to disable the normal acl_loan because we're inserting a custom acl_loan button
let availableAcls;
if (this.props.piece && this.props.piece.acl && typeof this.props.piece.acl.acl_loan !== 'undefined') {
// make a copy to not have side effects
availableAcls = mergeOptions({}, this.props.piece.acl);
availableAcls.acl_loan = false;
}
let SubmitButtonType = this.props.submitButtonType;
return (
<AclButtonList
className="text-center ascribe-button-list"
availableAcls={availableAcls}
editions={this.props.piece}
handleSuccess={this.loadPiece}>
<AclProxy
aclObject={this.props.currentUser.acl}
aclName="acl_wallet_submit">
<AclProxy
aclObject={availableAcls}
aclName="acl_wallet_submit">
<SubmitButtonType
className="btn-sm"
handleSuccess={this.handleSubmitSuccess}
piece={this.props.piece}/>
</AclProxy>
</AclProxy>
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
piece={this.props.piece}/>
</AclButtonList>
);
}
}
});
export default WalletActionPanel;

View File

@ -0,0 +1,92 @@
'use strict';
import React from 'react';
import Piece from '../../../../../components/ascribe_detail/piece';
import WalletActionPanel from './wallet_action_panel';
import CollapsibleParagraph from '../../../../../components/ascribe_collapsible/collapsible_paragraph';
import HistoryIterator from '../../../../ascribe_detail/history_iterator';
import Note from '../../../../ascribe_detail/note';
import DetailProperty from '../../../../ascribe_detail/detail_property';
import ApiUrls from '../../../../../constants/api_urls';
import AppConstants from '../../../../../constants/application_constants';
import { getLangText } from '../../../../../utils/lang_utils';
let WalletPieceContainer = React.createClass({
propTypes: {
piece: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired,
loadPiece: React.PropTypes.func.isRequired,
submitButtonType: React.PropTypes.func.isRequired
},
render() {
if(this.props.piece && this.props.piece.title) {
return (
<Piece
piece={this.props.piece}
loadPiece={this.props.loadPiece}
header={
<div className="ascribe-detail-header">
<hr style={{marginTop: 0}}/>
<h1 className="ascribe-detail-title">{this.props.piece.title}</h1>
<DetailProperty label="BY" value={this.props.piece.artist_name} />
<DetailProperty label="DATE" value={ this.props.piece.date_created.slice(0, 4) } />
<hr/>
</div>
}
subheader={
<div className="ascribe-detail-header">
<DetailProperty label={getLangText('REGISTREE')} value={ this.props.piece.user_registered } />
<DetailProperty label={getLangText('ID')} value={ this.props.piece.bitcoin_id } ellipsis={true} />
<hr/>
</div>
}>
<WalletActionPanel
piece={this.props.piece}
currentUser={this.props.currentUser}
loadPiece={this.props.loadPiece}
submitButtonType={this.props.submitButtonType}/>
<CollapsibleParagraph
title={getLangText('Loan History')}
show={this.props.piece.loan_history && this.props.piece.loan_history.length > 0}>
<HistoryIterator
history={this.props.piece.loan_history}/>
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('Notes')}
show={!!(this.props.currentUser.username || this.props.piece.public_note)}>
<Note
id={() => {return {'id': this.props.piece.id}; }}
label={getLangText('Personal note (private)')}
defaultValue={this.props.piece.private_note || null}
placeholder={getLangText('Enter your comments ...')}
editable={true}
successMessage={getLangText('Private note saved')}
url={ApiUrls.note_private_piece}
currentUser={this.props.currentUser}/>
</CollapsibleParagraph>
{this.props.children}
</Piece>
);
}
else {
return (
<div className="fullpage-spinner">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
</div>
);
}
}
});
export default WalletPieceContainer;

View File

@ -46,12 +46,19 @@ let CCRegisterPiece = React.createClass({
label={getLangText('Copyright license%s', '...')}
onChange={this.onLicenseChange}
footer={
<a
className="pull-right"
href={this.state.licenses[this.state.selectedLicense].url}
target="_blank">
{getLangText('Learn more')}
</a>
<span className="pull-right">
<a
href={this.state.licenses[this.state.selectedLicense].url}
target="_blank">
{getLangText('Learn more about ') + this.state.licenses[this.state.selectedLicense].code}
</a>
&nbsp;(
<a
href='https://www.ascribe.io/faq/#legals'
target="_blank">
{getLangText('ascribe faq')}
</a>)
</span>
}>
<select name="license">
{this.state.licenses.map((license, i) => {
@ -74,7 +81,7 @@ let CCRegisterPiece = React.createClass({
return (
<RegisterPiece
enableLocalHashing={false}
headerMessage={getLangText('Submit to Creative Commons')}
headerMessage={getLangText('Register under a Creative Commons license')}
submitMessage={getLangText('Submit')}>
{this.getLicenses()}
</RegisterPiece>

View File

@ -1,7 +1,6 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece';
@ -64,7 +63,7 @@ let CylandAccordionListItem = React.createClass({
<div>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_submit">
aclName="acl_wallet_submit">
<CylandSubmitButton
className="pull-right"
piece={this.props.content}
@ -72,7 +71,7 @@ let CylandAccordionListItem = React.createClass({
</AclProxy>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_submitted">
aclName="acl_wallet_submitted">
<button
disabled
className="btn btn-default btn-xs pull-right">
@ -80,6 +79,16 @@ let CylandAccordionListItem = React.createClass({
aria-hidden="true"></span>
</button>
</AclProxy>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_wallet_accepted">
<button
disabled
className="btn btn-default btn-xs pull-right">
{getLangText('Loaned to Cyland')} <span className="glyphicon glyphicon-ok"
aria-hidden="true"></span>
</button>
</AclProxy>
</div>
);
},

View File

@ -7,27 +7,19 @@ import PieceStore from '../../../../../../stores/piece_store';
import UserStore from '../../../../../../stores/user_store';
import Piece from '../../../../../../components/ascribe_detail/piece';
import CylandSubmitButton from '../ascribe_buttons/cyland_submit_button';
import CollapsibleParagraph from '../../../../../../components/ascribe_collapsible/collapsible_paragraph';
import CylandAdditionalDataForm from '../ascribe_forms/cyland_additional_data_form';
import WalletPieceContainer from '../../ascribe_detail/wallet_piece_container';
import AppConstants from '../../../../../../constants/application_constants';
import Form from '../../../../../../components/ascribe_forms/form';
import Property from '../../../../../../components/ascribe_forms/property';
import InputTextAreaToggable from '../../../../../../components/ascribe_forms/input_textarea_toggable';
import CollapsibleParagraph from '../../../../../../components/ascribe_collapsible/collapsible_paragraph';
import HistoryIterator from '../../../../../ascribe_detail/history_iterator';
import Note from '../../../../../ascribe_detail/note';
import FurtherDetailsFileuploader from '../../../../../ascribe_detail/further_details_fileuploader';
import DetailProperty from '../../../../../ascribe_detail/detail_property';
import ApiUrls from '../../../../../../constants/api_urls';
import { getLangText } from '../../../../../../utils/lang_utils';
import { mergeOptions } from '../../../../../../utils/general_utils';
let CylandPieceContainer = React.createClass({
getInitialState() {
return mergeOptions(
@ -38,23 +30,18 @@ let CylandPieceContainer = React.createClass({
componentDidMount() {
PieceStore.listen(this.onChange);
PieceActions.fetchOne(this.props.params.pieceId);
UserStore.listen(this.onChange);
},
componentWillReceiveProps(nextProps) {
if(this.props.params.pieceId !== nextProps.params.pieceId) {
PieceActions.updatePiece({});
PieceActions.fetchOne(nextProps.params.pieceId);
}
},
componentWillUnmount() {
// Every time we're leaving the piece detail page,
// just reset the piece that is saved in the piece store
// as it will otherwise display wrong/old data once the user loads
// the piece detail a second time
PieceActions.updatePiece({});
this.loadPiece();
},
componentWillUnmount() {
PieceStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
},
@ -70,50 +57,23 @@ let CylandPieceContainer = React.createClass({
render() {
if(this.state.piece && this.state.piece.title) {
return (
<Piece
<WalletPieceContainer
piece={this.state.piece}
currentUser={this.state.currentUser}
loadPiece={this.loadPiece}
header={
<div className="ascribe-detail-header">
<hr style={{marginTop: 0}}/>
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1>
<DetailProperty label="BY" value={this.state.piece.artist_name} />
<DetailProperty label="DATE" value={ this.state.piece.date_created.slice(0, 4) } />
<hr/>
</div>
}
subheader={
<div className="ascribe-detail-header">
<DetailProperty label={getLangText('REGISTREE')} value={ this.state.piece.user_registered } />
<DetailProperty label={getLangText('ID')} value={ this.state.piece.bitcoin_id } ellipsis={true} />
<hr/>
</div>
}>
submitButtonType={CylandSubmitButton}>
<CollapsibleParagraph
title={getLangText('Loan History')}
show={this.state.piece.loan_history && this.state.piece.loan_history.length > 0}>
<HistoryIterator
history={this.state.piece.loan_history} />
title={getLangText('Further Details')}
defaultExpanded={true}>
<CylandAdditionalDataForm
piece={this.state.piece}
disabled={!this.state.piece.acl.acl_edit}
isInline={true} />
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('Notes')}
show={(this.state.currentUser.username && true || false) ||
(this.state.piece.public_note)}>
<Note
id={() => {return {'id': this.state.piece.id}; }}
label={getLangText('Personal note (private)')}
defaultValue={this.state.piece.private_note ? this.state.piece.private_note : null}
placeholder={getLangText('Enter your comments ...')}
editable={true}
successMessage={getLangText('Private note saved')}
url={ApiUrls.note_private_piece}
currentUser={this.state.currentUser}/>
</CollapsibleParagraph>
<CylandPieceDetails piece={this.state.piece}/>
</Piece>
</WalletPieceContainer>
);
} else {
}
else {
return (
<div className="fullpage-spinner">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
@ -123,47 +83,4 @@ let CylandPieceContainer = React.createClass({
}
});
let CylandPieceDetails = React.createClass({
propTypes: {
piece: React.PropTypes.object
},
render() {
if (this.props.piece && Object.keys(this.props.piece.extra_data).length !== 0){
return (
<CollapsibleParagraph
title={getLangText('Further Details')}
show={true}
defaultExpanded={true}>
<Form ref='form'>
{Object.keys(this.props.piece.extra_data).map((data, i) => {
let label = data.replace('_', ' ');
return (
<Property
key={i}
name={data}
label={label}
editable={false}>
<InputTextAreaToggable
rows={1}
editable={false}
defaultValue={this.props.piece.extra_data[data]}/>
</Property>);
}
)}
<FurtherDetailsFileuploader
editable={false}
pieceId={this.props.piece.id}
otherData={this.props.piece.other_data}
multiple={false}/>
<hr />
</Form>
</CollapsibleParagraph>
);
}
return null;
}
});
export default CylandPieceContainer;

View File

@ -9,19 +9,35 @@ import InputTextAreaToggable from '../../../../../ascribe_forms/input_textarea_t
import FurtherDetailsFileuploader from '../../../../../ascribe_detail/further_details_fileuploader';
import GlobalNotificationModel from '../../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
import ApiUrls from '../../../../../../constants/api_urls';
import AppConstants from '../../../../../../constants/application_constants';
import requests from '../../../../../../utils/requests';
import { getLangText } from '../../../../../../utils/lang_utils';
import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_s3_fine_uploader_utils';
let CylandAdditionalDataForm = React.createClass({
propTypes: {
handleSuccess: React.PropTypes.func.isRequired,
handleSuccess: React.PropTypes.func,
piece: React.PropTypes.object.isRequired,
disabled: React.PropTypes.bool,
isInline: React.PropTypes.bool
},
disabled: React.PropTypes.bool
getDefaultProps() {
return {
isInline: false
};
},
handleSuccess() {
let notification = new GlobalNotificationModel('Further details successfully updated', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getInitialState() {
@ -60,72 +76,70 @@ let CylandAdditionalDataForm = React.createClass({
});
},
isReadyForFormSubmission(files) {
let uploadingFiles = files.filter((file) => file.status === 'submitting');
if (uploadingFiles.length === 0) {
return true;
} else {
return false;
}
},
render() {
if(this.props.piece && this.props.piece.id) {
let { piece, isInline, disabled, handleSuccess } = this.props;
let buttons, spinner, heading;
if(!isInline) {
buttons = (
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login"
disabled={!this.state.isUploadReady || disabled}>
{getLangText('Proceed to loan')}
</button>
);
spinner = (
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
</div>
);
heading = (
<div className="ascribe-form-header">
<h3>
{getLangText('Provide supporting materials')}
</h3>
</div>
);
}
if(piece && piece.id) {
return (
<Form
disabled={this.props.disabled}
disabled={disabled}
className="ascribe-form-bordered"
ref='form'
url={requests.prepareUrl(ApiUrls.piece_extradata, {piece_id: this.props.piece.id})}
handleSuccess={this.props.handleSuccess}
url={requests.prepareUrl(ApiUrls.piece_extradata, {piece_id: piece.id})}
handleSuccess={handleSuccess || this.handleSuccess}
getFormData={this.getFormData}
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login"
disabled={!this.state.isUploadReady || this.props.disabled}>
{getLangText('Proceed to loan')}
</button>
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
</div>
}>
<div className="ascribe-form-header">
<h3>
{getLangText('Provide supporting materials')}
</h3>
</div>
buttons={buttons}
spinner={spinner}>
{heading}
<Property
name='artist_bio'
label={getLangText('Artist Biography')}
editable={!this.props.disabled}>
label={getLangText('Artist Biography')}>
<InputTextAreaToggable
rows={1}
editable={!this.props.disabled}
placeholder={getLangText('Enter the artist\'s biography...')}
required="required"/>
defaultValue={piece.extra_data.artist_bio}
placeholder={getLangText('Enter the artist\'s biography...')}/>
</Property>
<Property
name='conceptual_overview'
label={getLangText('Conceptual Overview')}
editable={!this.props.disabled}>
label={getLangText('Conceptual Overview')}>
<InputTextAreaToggable
rows={1}
editable={!this.props.disabled}
placeholder={getLangText('Enter a conceptual overview...')}
required="required"/>
defaultValue={piece.extra_data.conceptual_overview}
placeholder={getLangText('Enter a conceptual overview...')}/>
</Property>
<FurtherDetailsFileuploader
uploadStarted={this.uploadStarted}
submitKey={this.submitKey}
submitFile={this.submitFile}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={this.isReadyForFormSubmission}
editable={!this.props.disabled}
pieceId={this.props.piece.id}
otherData={this.props.piece.other_data}
isReadyForFormSubmission={formSubmissionValidation.fileOptional}
pieceId={piece.id}
otherData={piece.other_data}
multiple={true}/>
</Form>
);

View File

@ -8,6 +8,8 @@ import UserStore from '../../../../../stores/user_store';
import CylandAccordionListItem from './ascribe_accordion_list/cyland_accordion_list_item';
import { getLangText } from '../../../../../utils/lang_utils';
let CylandPieceList = React.createClass({
getInitialState() {
@ -33,6 +35,13 @@ let CylandPieceList = React.createClass({
<PieceList
redirectTo="register_piece"
accordionListItemType={CylandAccordionListItem}
filterParams={[{
label: getLangText('Show works I have'),
items: [{
key: 'acl_loaned',
label: getLangText('loaned to Cyland')
}]
}]}
/>
</div>
);

View File

@ -10,9 +10,6 @@ import Row from 'react-bootstrap/lib/Row';
import RegisterPieceForm from '../../../../ascribe_forms/form_register_piece';
import Property from '../../../../ascribe_forms/property';
import InputCheckbox from '../../../../ascribe_forms/input_checkbox';
import WhitelabelActions from '../../../../../actions/whitelabel_actions';
import WhitelabelStore from '../../../../../stores/whitelabel_store';
@ -136,7 +133,7 @@ let CylandRegisterPiece = React.createClass({
this.transitionTo('piece', {pieceId: this.state.piece.id});
},
// We need to increase the step to lock the forms that are already filed out
// We need to increase the step to lock the forms that are already filled out
incrementStep() {
// also increase step
let newStep = this.state.step + 1;
@ -222,21 +219,7 @@ let CylandRegisterPiece = React.createClass({
showStartDate={false}
showEndDate={false}
showPersonalMessage={false}
handleSuccess={this.handleLoanSuccess}>
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox>
<span>
{' ' + getLangText('I agree to the Terms of Service of Cyland Archive') + ' '}
(<a href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/cyland/terms_and_contract.pdf" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}>
{getLangText('read')}
</a>)
</span>
</InputCheckbox>
</Property>
</LoanForm>
handleSuccess={this.handleLoanSuccess} />
</Col>
</Row>
</div>

View File

@ -63,16 +63,30 @@ let IkonotvAccordionListItem = React.createClass({
return (
<div>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_submit">
<IkonotvSubmitButton
className="btn-xs pull-right"
handleSuccess={this.handleSubmitSuccess}
piece={this.props.content}/>
aclObject={this.state.currentUser.acl}
aclName="acl_wallet_submit">
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_wallet_submit">
<IkonotvSubmitButton
className="btn-xs pull-right"
handleSuccess={this.handleSubmitSuccess}
piece={this.props.content}/>
</AclProxy>
</AclProxy>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_submitted">
aclName="acl_wallet_submitted">
<button
disabled
className="btn btn-default btn-xs pull-right">
{getLangText('Submitted to IkonoTV')} <span className="glyphicon glyphicon-ok"
aria-hidden="true"></span>
</button>
</AclProxy>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_wallet_accepted">
<button
disabled
className="btn btn-default btn-xs pull-right">

View File

@ -1,17 +1,8 @@
'use strict';
import React from 'react';
import Moment from 'moment';
import classNames from 'classnames';
import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper';
import LoanForm from '../../../../../ascribe_forms/form_loan';
import Property from '../../../../../ascribe_forms/property';
import InputCheckbox from '../../../../../ascribe_forms/input_checkbox';
import ApiUrls from '../../../../../../constants/api_urls';
import ButtonLink from 'react-router-bootstrap/lib/ButtonLink';
import { getLangText } from '../../../../../../utils/lang_utils';
@ -22,51 +13,36 @@ let IkonotvSubmitButton = React.createClass({
piece: React.PropTypes.object.isRequired
},
getSubmitButton() {
return (
<button
className={classNames('btn', 'btn-default', this.props.className)}>
{getLangText('Loan to IkonoTV')}
</button>
);
getDefaultProps() {
return {
className: 'btn-xs'
};
},
render() {
let piece = this.props.piece;
let startFrom = 1;
let today = new Moment();
let enddate = new Moment();
enddate.add(1, 'years');
// In the Ikonotv loan page a user has to complete two steps.
// Since every one of those steps is atomic a user should always be able to continue
// where he left of.
// This is why we start the process form slide 1/2 if the user has already finished
// it in another session.
if(piece && piece.extra_data && Object.keys(piece.extra_data).length > 0) {
startFrom = 1;
}
return (
<ModalWrapper
trigger={this.getSubmitButton()}
handleSuccess={this.props.handleSuccess}
title={getLangText('Loan to IkonoTV archive')}>
<LoanForm
id={{piece_id: this.props.piece.id}}
url={ApiUrls.ownership_loans_pieces}
email="submissions@ikono.org"
startdate={today}
enddate={enddate}
gallery="IkonoTV archive"
showPersonalMessage={false}
handleSuccess={this.props.handleSuccess}>
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox>
<span>
{' ' + getLangText('I agree to the Terms of Service of IkonoTV Archive') + ' '}
(<a href="https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/ikonotv/ikono-tos.pdf" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}>
{getLangText('read')}
</a>)
</span>
</InputCheckbox>
</Property>
</LoanForm>
</ModalWrapper>
<ButtonLink
to="register_piece"
query={{
'slide_num': 0,
'start_from': startFrom,
'piece_id': piece.id
}}
className={classNames('ascribe-margin-1px', this.props.className)}>
{getLangText('Loan to IkonoTV')}
</ButtonLink>
);
}
});

View File

@ -5,53 +5,45 @@ import React from 'react';
import PieceActions from '../../../../../../actions/piece_actions';
import PieceStore from '../../../../../../stores/piece_store';
import PieceListActions from '../../../../../../actions/piece_list_actions';
import PieceListStore from '../../../../../../stores/piece_list_store';
import UserStore from '../../../../../../stores/user_store';
import Piece from '../../../../../../components/ascribe_detail/piece';
import ListRequestActions from '../../../../../ascribe_forms/list_form_request_actions';
import AclButtonList from '../../../../../ascribe_buttons/acl_button_list';
import DeleteButton from '../../../../../ascribe_buttons/delete_button';
import CollapsibleParagraph from '../../../../../../components/ascribe_collapsible/collapsible_paragraph';
import IkonotvSubmitButton from '../ascribe_buttons/ikonotv_submit_button';
import HistoryIterator from '../../../../../ascribe_detail/history_iterator';
import CollapsibleParagraph from '../../../../../../components/ascribe_collapsible/collapsible_paragraph';
import DetailProperty from '../../../../../ascribe_detail/detail_property';
import IkonotvArtistDetailsForm from '../ascribe_forms/ikonotv_artist_details_form';
import IkonotvArtworkDetailsForm from '../ascribe_forms/ikonotv_artwork_details_form';
import GlobalNotificationModel from '../../../../../../models/global_notification_model';
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
import AclProxy from '../../../../../acl_proxy';
import WalletPieceContainer from '../../ascribe_detail/wallet_piece_container';
import AppConstants from '../../../../../../constants/application_constants';
import { getLangText } from '../../../../../../utils/lang_utils';
import { mergeOptions } from '../../../../../../utils/general_utils';
let IkonotvPieceContainer = React.createClass({
getInitialState() {
return mergeOptions(
PieceStore.getState(),
UserStore.getState(),
PieceListStore.getState()
UserStore.getState()
);
},
componentDidMount() {
PieceStore.listen(this.onChange);
PieceActions.fetchOne(this.props.params.pieceId);
UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
// Every time we're leaving the piece detail page,
// just reset the piece that is saved in the piece store
// as it will otherwise display wrong/old data once the user loads
// the piece detail a second time
PieceActions.updatePiece({});
this.loadPiece();
},
// We need this for when the user clicks on a notification while being in another piece view
componentWillReceiveProps(nextProps) {
if(this.props.params.pieceId !== nextProps.params.pieceId) {
PieceActions.updatePiece({});
@ -60,14 +52,8 @@ let IkonotvPieceContainer = React.createClass({
},
componentWillUnmount() {
// Every time we're leaving the piece detail page,
// just reset the piece that is saved in the piece store
// as it will otherwise display wrong/old data once the user loads
// the piece detail a second time
PieceActions.updatePiece({});
PieceStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange);
},
onChange(state) {
@ -78,92 +64,44 @@ let IkonotvPieceContainer = React.createClass({
PieceActions.fetchOne(this.props.params.pieceId);
},
handleSubmitSuccess(response) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
render() {
let furtherDetails = (
<CollapsibleParagraph
title={getLangText('Further Details')}
defaultExpanded={true}>
<span>{getLangText('This piece has been loaned before we started to collect further details.')}</span>
</CollapsibleParagraph>
);
this.loadPiece();
let notification = new GlobalNotificationModel(response.notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getActions(){
if (this.state.piece &&
this.state.piece.request_action &&
this.state.piece.request_action.length > 0) {
return (
<ListRequestActions
pieceOrEditions={this.state.piece}
currentUser={this.state.currentUser}
handleSuccess={this.loadPiece}
requestActions={this.state.piece.request_action}/>
);
}
else {
//We need to disable the normal acl_loan because we're inserting a custom acl_loan button
let availableAcls;
if(this.state.piece && this.state.piece.acl && typeof this.state.piece.acl.acl_loan !== 'undefined') {
// make a copy to not have side effects
availableAcls = mergeOptions({}, this.state.piece.acl);
availableAcls.acl_loan = false;
}
return (
<AclButtonList
className="text-center ascribe-button-list"
availableAcls={availableAcls}
editions={this.state.piece}
handleSuccess={this.loadPiece}>
<AclProxy
aclObject={availableAcls}
aclName="acl_submit">
<IkonotvSubmitButton
className="btn-sm"
handleSuccess={this.handleSubmitSuccess}
piece={this.state.piece}/>
</AclProxy>
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
piece={this.state.piece}/>
</AclButtonList>
if(this.state.piece.extra_data && Object.keys(this.state.piece.extra_data).length > 0 && this.state.piece.acl) {
furtherDetails = (
<CollapsibleParagraph
title={getLangText('Further Details')}
defaultExpanded={true}>
<IkonotvArtistDetailsForm
piece={this.state.piece}
isInline={true}
disabled={!this.state.piece.acl.acl_edit} />
<IkonotvArtworkDetailsForm
piece={this.state.piece}
isInline={true}
disabled={!this.state.piece.acl.acl_edit} />
</CollapsibleParagraph>
);
}
},
render() {
if(this.state.piece && this.state.piece.title) {
return (
<Piece
<WalletPieceContainer
piece={this.state.piece}
currentUser={this.state.currentUser}
loadPiece={this.loadPiece}
header={
<div className="ascribe-detail-header">
<hr style={{marginTop: 0}}/>
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1>
<DetailProperty label="BY" value={this.state.piece.artist_name} />
<DetailProperty label="DATE" value={ this.state.piece.date_created.slice(0, 4) } />
<hr/>
</div>
}
subheader={
<div className="ascribe-detail-header">
<DetailProperty label={getLangText('REGISTREE')} value={ this.state.piece.user_registered } />
<DetailProperty label={getLangText('ID')} value={ this.state.piece.bitcoin_id } ellipsis={true} />
<hr/>
</div>
}
buttons={this.getActions()}>
<CollapsibleParagraph
title={getLangText('Loan History')}
show={this.state.piece.loan_history && this.state.piece.loan_history.length > 0}>
<HistoryIterator
history={this.state.piece.loan_history} />
</CollapsibleParagraph>
</Piece>
submitButtonType={IkonotvSubmitButton}>
{furtherDetails}
</WalletPieceContainer>
);
} else {
}
else {
return (
<div className="fullpage-spinner">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />

Some files were not shown because too many files have changed in this diff Show More