resolve cluster-merge-fuck

This commit is contained in:
Tim Daubenschütz 2015-06-02 17:32:38 +02:00
commit 95c199bed6
36 changed files with 965 additions and 143 deletions

View File

@ -5,8 +5,9 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>ascribe</title> <title>ascribe</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="build/css/main.css"> <link rel="stylesheet" href="node_modules/react-datepicker/dist/react-datepicker.css">
<link rel="stylesheet" href="//brick.a.ssl.fastly.net/Source+Sans+Pro:400,600,700,900"> <link rel="stylesheet" href="//brick.a.ssl.fastly.net/Source+Sans+Pro:400,600,700,900">
<link rel="stylesheet" href="build/css/main.css">
</head> </head>
<body> <body>
<div id="main" class="container clear-paddings"></div> <div id="main" class="container clear-paddings"></div>

View File

@ -1,27 +1,66 @@
import React from 'react'; import React from 'react';
import ConsignForm from './ascribe_forms/form_consign';
import TransferForm from './ascribe_forms/form_transfer';
import LoanForm from './ascribe_forms/form_loan';
import ShareForm from './ascribe_forms/form_share_email';
import ModalWrapper from './ascribe_modal/modal_wrapper';
import AppConstants from '../constants/application_constants'; import AppConstants from '../constants/application_constants';
let AclButton = React.createClass({ let AclButton = React.createClass({
propTypes: { propTypes: {
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired, action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
availableAcls: React.PropTypes.array.isRequired, availableAcls: React.PropTypes.array.isRequired,
actionFunction: React.PropTypes.func.isRequired editions: React.PropTypes.array.isRequired,
currentUser: React.PropTypes.object,
handleSuccess: React.PropTypes.func.isRequired
}, },
actionFunction() { actionProperties(){
this.props.actionFunction(this.props.action); if (this.props.action == 'consign'){
return {
title: "Consign artwork",
tooltip: "Have someone else sell the artwork",
form: <ConsignForm />
}
}
else if (this.props.action == 'transfer') {
return {
title: "Transfer artwork",
tooltip: "Transfer the ownership of the artwork",
form: <TransferForm />
}
}
else if (this.props.action == 'loan'){
return {
title: "Loan artwork",
tooltip: "Loan your artwork for a limited period of time",
form: <LoanForm />}
}
else if (this.props.action == 'share'){
return {
title: "Share artwork",
tooltip: "Share the artwork",
form: <ShareForm />}
}
}, },
render() { render() {
let shouldDisplay = this.props.availableAcls.indexOf(this.props.action) > -1; let shouldDisplay = this.props.availableAcls.indexOf(this.props.action) > -1;
let aclProps = this.actionProperties();
return ( return (
<button <ModalWrapper
type="button" button={
className={shouldDisplay ? 'btn btn-default btn-sm' : 'hidden'} <div className={shouldDisplay ? 'btn btn-default btn-sm' : 'hidden'}>
onClick={this.actionFunction}> {this.props.action.toUpperCase()}
{this.props.action.toUpperCase()} </div>
</button> }
currentUser={ this.props.currentUser }
editions={ this.props.editions }
handleSuccess={ this.props.handleSuccess }
title={ aclProps.title }
tooltip={ aclProps.tooltip }>
{ aclProps.form }
</ModalWrapper>
); );
} }
}); });

View File

@ -3,11 +3,11 @@ import React from 'react';
let ButtonSubmitOrClose = React.createClass({ let ButtonSubmitOrClose = React.createClass({
render() { render() {
if (this.props.submitted){ if (this.props.submitted){
return ( //return (
<div className="modal-footer"> // <div className="modal-footer">
Loading // Loading
</div> // </div>
) //)
} }
return ( return (
<div className="modal-footer"> <div className="modal-footer">

View File

@ -0,0 +1,68 @@
import fetch from 'isomorphic-fetch';
import React from 'react';
import ApiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text';
import InputTextArea from './input_textarea';
import ButtonSubmitOrClose from './button_submit_close';
let ConsignForm = React.createClass({
mixins: [FormMixin],
url() {
return ApiUrls.ownership_consigns
},
getFormData() {
return {
bitcoin_id: this.getBitcoinIds().join(),
consignee: this.refs.consignee.state.value,
consign_message: this.refs.consign_message.state.value,
password: this.refs.password.state.value
}
},
renderForm() {
let title = this.getTitlesString().join("");
let username = this.props.currentUser.username;
let message =
`Hi,
I consign :
${title}to you.
Truly yours,
${username}`;
return (
<form id="consign_modal_content" role="form" onSubmit={this.submit}>
<input className="invisible" type="email" name="fake_consignee"/>
<input className="invisible" type="password" name="fake_password"/>
<InputText
ref="consignee"
placeHolder="Consignee email"
required="required"
type="email"
submitted={this.state.submitted}/>
<InputTextArea
ref="consign_message"
defaultValue={message}
required=""
/>
<InputText
ref="password"
placeHolder="Password"
required="required"
type="password"
submitted={this.state.submitted}/>
<ButtonSubmitOrClose
text="CONSIGN"
onClose={this.props.onRequestHide}
submitted={this.state.submitted} />
</form>
);
}
});
export default ConsignForm;

View File

@ -0,0 +1,137 @@
import fetch from 'isomorphic-fetch';
import React from 'react';
import ApiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text';
import InputHidden from './input_hidden';
import InputCheckbox from './input_checkbox';
import InputDate from './input_date';
import InputTextArea from './input_textarea';
import OwnershipFetcher from '../../fetchers/ownership_fetcher'
import ButtonSubmitOrClose from './button_submit_close';
let LoanForm = React.createClass({
mixins: [FormMixin],
url() {
return ApiUrls.ownership_loans
},
componentDidMount(){
this.setState({contract_key: null,
contract_url: null,
loaneeHasContract: false});
},
getFormData() {
return {
bitcoin_id: this.getBitcoinIds().join(),
loanee: this.refs.loanee.state.value,
gallery_name: this.refs.gallery_name.state.value,
startdate: this.refs.startdate.state.value.format("YYYY-MM-DD"),
enddate: this.refs.enddate.state.value.format("YYYY-MM-DD"),
loan_message: this.refs.loan_message.state.value,
password: this.refs.password.state.value,
terms: this.refs.terms.state.value
}
},
handleLoanEmailBlur(e){
OwnershipFetcher.fetchLoanContract(this.refs.loanee.state.value)
.then((res) => {
if (res && res.length > 0) {
this.setState({contract_key: res[0].s3Key,
contract_url: res[0].s3Url,
loaneeHasContract: true});
}
else{
this.resetLoanContract();
}
})
.catch((err) => {
console.log(err);
this.resetLoanContract();
});
},
resetLoanContract(){
this.setState({contract_key: null,
contract_url: null,
loaneeHasContract: false
});
},
renderForm() {
let title = this.getTitlesString().join("");
let username = this.props.currentUser.username;
let message =
`Hi,
I loan :
${title}to you.
Truly yours,
${username}`;
let contract = <InputHidden ref="terms" value="True"/>;
if (this.state.loaneeHasContract){
let label = <div>
I agree to the&nbsp;
<a href={this.state.contract_url} target="_blank">
terms of {this.refs.loanee.state.value}
</a>
</div>;
contract = <InputCheckbox
ref="terms"
required="required"
label={label}
/>
}
return (
<form id="loan_modal_content" role="form" onSubmit={this.submit}>
<input className="invisible" type="email" name="fake_loanee"/>
<input className="invisible" type="password" name="fake_password"/>
<InputText
ref="loanee"
placeHolder="Loanee email"
required="required"
type="email"
submitted={this.state.submitted}
onBlur={this.handleLoanEmailBlur}/>
<InputText
ref="gallery_name"
placeHolder="Gallery/exhibition (optional)"
required=""
type="text"
submitted={this.state.submitted}/>
<div className="row">
<div className="col-md-6">
<InputDate
ref="startdate"
placeholderText="Loan start date" />
</div>
<div className="col-md-6 form-group">
<InputDate
ref="enddate"
placeholderText="Loan end date" />
</div>
</div>
<InputTextArea
ref="loan_message"
defaultValue={message}
required=""
/>
<InputText
ref="password"
placeHolder="Password"
required="required"
type="password"
submitted={this.state.submitted}/>
{contract}
<ButtonSubmitOrClose
text="LOAN"
onClose={this.props.onRequestHide}
submitted={this.state.submitted} />
</form>
);
}
});
export default LoanForm;

View File

@ -16,18 +16,23 @@ let ShareForm = React.createClass({
}, },
getFormData() { getFormData() {
return { return {
bitcoin_id: this.props.edition.bitcoin_id, bitcoin_id: this.getBitcoinIds().join(),
share_emails: this.refs.share_emails.state.value, share_emails: this.refs.share_emails.state.value,
share_message: this.refs.share_message.state.value share_message: this.refs.share_message.state.value
} }
}, },
renderForm() { renderForm() {
let message = "Hi,\n" + let title = this.getTitlesString().join("");
"\n" + let username = this.props.currentUser.username;
"I am sharing \"" + this.props.edition.title + "\" with you.\n" + let message =
"\n" + `Hi,
"Truly yours,\n" +
this.props.currentUser.username; I am sharing :
${title}with you.
Truly yours,
${username}`;
return ( return (
<form id="share_modal_content" role="form" key="share_modal_content" onSubmit={this.submit}> <form id="share_modal_content" role="form" key="share_modal_content" onSubmit={this.submit}>
<InputText <InputText

View File

@ -18,19 +18,24 @@ let TransferForm = React.createClass({
}, },
getFormData() { getFormData() {
return { return {
bitcoin_id: this.props.edition.bitcoin_id, bitcoin_id: this.getBitcoinIds().join(),
transferee: this.refs.transferee.state.value, transferee: this.refs.transferee.state.value,
transfer_message: this.refs.transfer_message.state.value, transfer_message: this.refs.transfer_message.state.value,
password: this.refs.password.state.value password: this.refs.password.state.value
} }
}, },
renderForm() { renderForm() {
let message = "Hi,\n" + let title = this.getTitlesString().join("");
"\n" + let username = this.props.currentUser.username;
"I transfer ownership of \"" + this.props.edition.title + "\" to you.\n" + let message =
"\n" + `Hi,
"Truly yours,\n" +
this.props.currentUser.username; I transfer ownership of :
${title}to you.
Truly yours,
${username}`;
return ( return (
<form id="transfer_modal_content" role="form" onSubmit={this.submit}> <form id="transfer_modal_content" role="form" onSubmit={this.submit}>
<input className="invisible" type="email" name="fake_transferee"/> <input className="invisible" type="email" name="fake_transferee"/>

View File

@ -0,0 +1,58 @@
import fetch from 'isomorphic-fetch';
import React from 'react';
import ApiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text';
import InputTextArea from './input_textarea';
import ButtonSubmitOrClose from './button_submit_close';
let UnConsignForm = React.createClass({
mixins: [FormMixin],
url() {
return ApiUrls.ownership_unconsigns
},
getFormData() {
return {
bitcoin_id: this.props.edition.bitcoin_id,
unconsign_message: this.refs.unconsign_message.state.value,
password: this.refs.password.state.value
}
},
renderForm() {
let title = this.props.edition.title;
let username = this.props.currentUser.username;
let message =
`Hi,
I un-consign \" ${title} \" from you.
Truly yours,
${username}`;
return (
<form id="unconsign_modal_content" role="form" onSubmit={this.submit}>
<input className="invisible" type="email" name="fake_unconsignee"/>
<input className="invisible" type="password" name="fake_password"/>
<InputTextArea
ref="unconsign_message"
defaultValue={message}
required="" />
<InputText
ref="password"
placeHolder="Password"
required="required"
type="password"
submitted={this.state.submitted} />
<ButtonSubmitOrClose
text="UNCONSIGN"
onClose={this.props.onRequestHide}
submitted={this.state.submitted} />
</form>
);
}
});
export default UnConsignForm;

View File

@ -0,0 +1,49 @@
import fetch from 'isomorphic-fetch';
import React from 'react';
import ApiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text';
import InputTextArea from './input_textarea';
import ButtonSubmitOrClose from './button_submit_close';
let UnConsignRequestForm = React.createClass({
mixins: [FormMixin],
url() {
return ApiUrls.ownership_unconsigns_request
},
getFormData() {
return {
bitcoin_id: this.props.edition.bitcoin_id,
unconsign_request_message: this.refs.unconsign_request_message.state.value
}
},
renderForm() {
let title = this.props.edition.title;
let username = this.props.currentUser.username;
let message =
`Hi,
I request you to un-consign \" ${title} \".
Truly yours,
${username}`;
return (
<form id="unconsign_request_modal_content" role="form" onSubmit={this.submit}>
<InputTextArea
ref="unconsign_request_message"
defaultValue={message}
required="" />
<ButtonSubmitOrClose
text="UNCONSIGN REQUEST"
onClose={this.props.onRequestHide}
submitted={this.state.submitted} />
</form>
);
}
});
export default UnConsignRequestForm;

View File

@ -0,0 +1,40 @@
import React from 'react';
import AlertMixin from '../../mixins/alert_mixin'
let InputCheckbox = React.createClass({
mixins : [AlertMixin],
getInitialState() {
return {value: null,
alerts: null // needed in AlertMixin
};
},
handleChange(event) {
this.setState({value: event.target.value});
},
render() {
let alerts = (this.props.submitted) ? null : this.state.alerts;
return (
<div className="form-group">
{alerts}
<div className="input-checkbox-ascribe">
<div className="checkbox">
<label>
<input
type="checkbox"
required={this.props.required}
onChange={this.handleChange}
/>
{this.props.label}
</label>
</div>
</div>
</div>
);
}
});
export default InputCheckbox;

View File

@ -0,0 +1,51 @@
import React from 'react';
import AlertMixin from '../../mixins/alert_mixin'
import DatePicker from 'react-datepicker/dist/react-datepicker'
let InputDate = React.createClass({
mixins : [AlertMixin],
getInitialState() {
return {value: null,
alerts: null // needed in AlertMixin
};
},
handleChange(date) {
this.setState({value: date});
},
render: function () {
let className = "form-control input-text-ascribe";
let alerts = (this.props.submitted) ? null : this.state.alerts;
return (
<DatePicker
key="example2"
dateFormat="YYYY-MM-DD"
selected={this.state.value}
onChange={this.handleChange}
placeholderText={this.props.placeholderText}
/>
);
//return (
// <div className="input-group date"
// ref={this.props.name + "_picker"}
// onChange={this.handleChange}>
// <input className={className}
// ref={this.props.name}
// placeholder={this.props.placeholder}
// required={this.props.required}
// type="text"/>
// <span className="input-group-addon input-text-ascribe">
// <span className="glyphicon glyphicon-calendar" style={{"color": "black"}}></span>
// </span>
// </div>
//)
}
});
export default InputDate;

View File

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

View File

@ -8,8 +8,7 @@ let InputText = React.createClass({
getInitialState() { getInitialState() {
return {value: null, return {value: null,
alerts: null, // needed in AlertMixin alerts: null // needed in AlertMixin
retry: 0 // needed in AlertMixin for generating unique alerts
}; };
}, },
handleChange(event) { handleChange(event) {
@ -25,7 +24,8 @@ let InputText = React.createClass({
placeholder={this.props.placeHolder} placeholder={this.props.placeHolder}
required={this.props.required} required={this.props.required}
type={this.props.type} type={this.props.type}
onChange={this.handleChange}/> onChange={this.handleChange}
onBlur={this.props.onBlur}/>
</div> </div>
); );

View File

@ -8,8 +8,7 @@ let InputTextArea = React.createClass({
getInitialState() { getInitialState() {
return {value: this.props.defaultValue, return {value: this.props.defaultValue,
alerts: null, // needed in AlertMixin alerts: null // needed in AlertMixin
retry: 0 // needed in AlertMixin for generating unique alerts
}; };
}, },
handleChange(event) { handleChange(event) {
@ -28,7 +27,6 @@ let InputTextArea = React.createClass({
onChange={this.handleChange}></textarea> onChange={this.handleChange}></textarea>
</div> </div>
); );
} }
}); });

View File

@ -1,25 +0,0 @@
import React from 'react';
/**
* This is the component that implements display-specific functionality
*/
let ImageViewer = React.createClass({
propTypes: {
thumbnail: React.PropTypes.string.isRequired
},
render() {
let thumbnail = <img className="img-responsive" src={this.props.thumbnail}/>;
let aligner = <span className="vcenter"></span>;
return (
<div>
<div>
{aligner}
{thumbnail}
</div>
</div>
);
}
});
export default ImageViewer;

View File

@ -0,0 +1,40 @@
import React from 'react';
import InjectInHeadMixin from '../../mixins/inject_in_head_mixin';
/**
* This is the component that implements display-specific functionality.
*
* ResourceViewer handles the following mimetypes:
* - image
* - video
* - audio
* - pdf
* - other
*/
let resourceMap = {
'image': 1
}
let ResourceViewer = React.createClass({
propTypes: {
thumbnail: React.PropTypes.string.isRequired,
mimetype: React.PropTypes.oneOf(['image', 'video', 'audio', 'pdf', 'other']).isRequired
},
mixins: [InjectInHeadMixin],
componentDidMount() {
this.inject('http://antani.com');
},
render() {
return (
<div>
resourceviewer {this.props.thumbnail} {this.props.mimetype}
</div>
);
}
});
export default ResourceViewer;

View File

@ -0,0 +1,43 @@
import React from 'react';
import Modal from 'react-bootstrap/lib/Modal';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import ModalTrigger from 'react-bootstrap/lib/ModalTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import LoanForm from '../ascribe_forms/form_loan'
import ModalMixin from '../../mixins/modal_mixin'
let LoanModalButton = React.createClass({
render() {
return (
<OverlayTrigger delay={500} placement="left"
overlay={<Tooltip>Loan your artwork for a limited period of time</Tooltip>}>
<ModalTrigger modal={<LoanModal edition={this.props.edition}
currentUser={this.props.currentUser}/>}>
<div className="btn btn-ascribe-inv">
LOAN
</div>
</ModalTrigger>
</OverlayTrigger>
)
}
});
let LoanModal = React.createClass({
mixins : [ModalMixin],
render() {
return (
<Modal {...this.props} title="Loan artwork">
<div className="modal-body">
<LoanForm edition={this.props.edition}
currentUser={this.props.currentUser}
onRequestHide={this.onRequestHide}/>
</div>
</Modal>
)
}
});
export default LoanModalButton;

View File

@ -4,6 +4,7 @@ import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import ModalTrigger from 'react-bootstrap/lib/ModalTrigger'; import ModalTrigger from 'react-bootstrap/lib/ModalTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip'; import Tooltip from 'react-bootstrap/lib/Tooltip';
import ModalMixin from '../../mixins/modal_mixin'
import ShareForm from '../ascribe_forms/form_share_email' import ShareForm from '../ascribe_forms/form_share_email'
@ -24,11 +25,8 @@ let ShareModalButton = React.createClass({
}); });
let ShareModal = React.createClass({ let ShareModal = React.createClass({
onRequestHide(e){ mixins : [ModalMixin],
if (e)
e.preventDefault();
this.props.onRequestHide();
},
render() { render() {
return ( return (
<Modal {...this.props} title="Share artwork"> <Modal {...this.props} title="Share artwork">

View File

@ -1,45 +0,0 @@
import React from 'react';
import Modal from 'react-bootstrap/lib/Modal';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import ModalTrigger from 'react-bootstrap/lib/ModalTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import TransferForm from '../ascribe_forms/form_transfer'
let TransferModalButton = React.createClass({
render() {
return (
<OverlayTrigger delay={500} placement="left"
overlay={<Tooltip>Transfer the ownership of the artwork</Tooltip>}>
<ModalTrigger modal={<TransferModal edition={this.props.edition}
currentUser={this.props.currentUser}/>}>
<div className="btn btn-ascribe-inv">
TRANSFER
</div>
</ModalTrigger>
</OverlayTrigger>
)
}
});
let TransferModal = React.createClass({
onRequestHide(e){
e.preventDefault();
this.props.onRequestHide();
},
render() {
return (
<Modal {...this.props} title="Transfer artwork">
<div className="modal-body">
<TransferForm edition={this.props.edition}
currentUser={this.props.currentUser}
onRequestHide={this.onRequestHide}/>
</div>
</Modal>
)
}
});
export default TransferModalButton;

View File

@ -0,0 +1,43 @@
import React from 'react';
import Modal from 'react-bootstrap/lib/Modal';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import ModalTrigger from 'react-bootstrap/lib/ModalTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import UnConsignForm from '../ascribe_forms/form_unconsign'
import ModalMixin from '../../mixins/modal_mixin'
let UnConsignModalButton = React.createClass({
render() {
return (
<OverlayTrigger delay={500} placement="left"
overlay={<Tooltip>Unconsign this artwork</Tooltip>}>
<ModalTrigger modal={<UnConsignModal edition={this.props.edition}
currentUser={this.props.currentUser}/>}>
<div className="btn btn-ascribe-inv">
UNCONSIGN
</div>
</ModalTrigger>
</OverlayTrigger>
)
}
});
let UnConsignModal = React.createClass({
mixins : [ModalMixin],
render() {
return (
<Modal {...this.props} title="Consign artwork">
<div className="modal-body">
<UnConsignForm edition={this.props.edition}
currentUser={this.props.currentUser}
onRequestHide={this.onRequestHide}/>
</div>
</Modal>
)
}
});
export default UnConsignModalButton;

View File

@ -0,0 +1,43 @@
import React from 'react';
import Modal from 'react-bootstrap/lib/Modal';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import ModalTrigger from 'react-bootstrap/lib/ModalTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import UnConsignRequestForm from '../ascribe_forms/form_unconsign_request'
import ModalMixin from '../../mixins/modal_mixin'
let UnConsignRequestModalButton = React.createClass({
render() {
return (
<OverlayTrigger delay={500} placement="left"
overlay={<Tooltip>Request to unconsign the artwork</Tooltip>}>
<ModalTrigger modal={<UnConsignRequestModal edition={this.props.edition}
currentUser={this.props.currentUser}/>}>
<div className="btn btn-ascribe-inv">
UNCONSIGN REQUEST
</div>
</ModalTrigger>
</OverlayTrigger>
)
}
});
let UnConsignRequestModal = React.createClass({
mixins : [ModalMixin],
render() {
return (
<Modal {...this.props} title="Request to unconsign artwork">
<div className="modal-body">
<UnConsignRequestForm edition={this.props.edition}
currentUser={this.props.currentUser}
onRequestHide={this.onRequestHide}/>
</div>
</Modal>
)
}
});
export default UnConsignRequestModalButton;

View File

@ -0,0 +1,64 @@
import React from 'react';
import ReactAddons from 'react/addons';
import Modal from 'react-bootstrap/lib/Modal';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import ModalTrigger from 'react-bootstrap/lib/ModalTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import ModalMixin from '../../mixins/modal_mixin'
let ModalWrapper = React.createClass({
render() {
return (
<OverlayTrigger delay={500} placement="left"
overlay={<Tooltip>{this.props.tooltip}</Tooltip>}>
<ModalTrigger modal={
<ModalBody
title={this.props.title}
editions={this.props.editions}
currentUser={this.props.currentUser}
handleSuccess={this.props.handleSuccess}
>
{this.props.children}
</ModalBody>
}>
{this.props.button}
</ModalTrigger>
</OverlayTrigger>
)
}
});
//
let ModalBody = React.createClass({
mixins : [ModalMixin],
handleSuccess(){
this.props.handleSuccess();
this.props.onRequestHide();
},
renderChildren() {
return ReactAddons.Children.map(this.props.children, (child, i) => {
return ReactAddons.addons.cloneWithProps(child, {
editions: this.props.editions,
currentUser: this.props.currentUser,
onRequestHide: this.onRequestHide,
handleSuccess: this.handleSuccess
});
});
},
render() {
return (
<Modal {...this.props} title={this.props.title}>
<div className="modal-body">
{this.renderChildren()}
</div>
</Modal>
)
}
});
export default ModalWrapper;

View File

@ -3,6 +3,9 @@ import React from 'react';
import EditionListStore from '../../stores/edition_list_store'; import EditionListStore from '../../stores/edition_list_store';
import EditionListActions from '../../actions/edition_list_actions'; import EditionListActions from '../../actions/edition_list_actions';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import AclButton from '../acl_button'; import AclButton from '../acl_button';
import PieceListBulkModalSelectedEditionsWidget from './piece_list_bulk_modal_selected_editions_widget'; import PieceListBulkModalSelectedEditionsWidget from './piece_list_bulk_modal_selected_editions_widget';
@ -20,11 +23,14 @@ let PieceListBulkModal = React.createClass({
}, },
componentDidMount() { componentDidMount() {
EditionListStore.listen(this.onChange) UserActions.fetchCurrentUser();
EditionListStore.listen(this.onChange);
UserStore.listen(this.onChange);
}, },
componentDidUnmount() { componentDidUnmount() {
EditionListStore.unlisten(this.onChange) EditionListStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
}, },
filterForSelected(edition) { filterForSelected(edition) {
@ -75,8 +81,13 @@ let PieceListBulkModal = React.createClass({
EditionListActions.clearAllEditionSelections(); EditionListActions.clearAllEditionSelections();
}, },
handleSuccess(){
},
render() { render() {
let availableAcls = this.getAvailableAcls(); let availableAcls = this.getAvailableAcls();
let selectedEditions = this.fetchSelectedEditionList();
if(availableAcls.length > 0) { if(availableAcls.length > 0) {
return ( return (
@ -97,10 +108,30 @@ let PieceListBulkModal = React.createClass({
<p></p> <p></p>
<div className="row"> <div className="row">
<div className="text-center"> <div className="text-center">
<AclButton availableAcls={availableAcls} action="transfer" actionFunction={this.bulk} /> <AclButton
<AclButton availableAcls={availableAcls} action="consign" actionFunction={this.bulk} /> availableAcls={availableAcls}
<AclButton availableAcls={availableAcls} action="share" actionFunction={this.bulk} /> action="transfer"
<AclButton availableAcls={availableAcls} action="loan" actionFunction={this.bulk} /> editions={selectedEditions}
currentUser={this.state.currentUser}
handleSuccess={this.handleSuccess} />
<AclButton
availableAcls={availableAcls}
action="consign"
editions={selectedEditions}
currentUser={this.state.currentUser}
handleSuccess={this.handleSuccess} />
<AclButton
availableAcls={availableAcls}
action="loan"
editions={selectedEditions}
currentUser={this.state.currentUser}
handleSuccess={this.handleSuccess} />
<AclButton
availableAcls={availableAcls}
action="share"
editions={selectedEditions}
currentUser={this.state.currentUser}
handleSuccess={this.handleSuccess} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,17 +1,24 @@
import React from 'react'; import React from 'react';
import ImageViewer from './ascribe_media/image_viewer';
import TransferModalButton from './ascribe_modal/modal_transfer'; import ResourceViewer from './ascribe_media/resource_viewer';
import ShareModalButton from './ascribe_modal/modal_share';
import EditionActions from '../actions/edition_actions'
import AclButton from './acl_button'
/** /**
* This is the component that implements display-specific functionality * This is the component that implements display-specific functionality
*/ */
let Edition = React.createClass({ let Edition = React.createClass({
render() { render() {
let thumbnail = this.props.edition.thumbnail;
let mimetype = this.props.edition.digital_work.mime;
return ( return (
<div> <div>
<div className="col-md-7"> <div className="col-md-7">
<ImageViewer thumbnail={this.props.edition.thumbnail}/> <ResourceViewer thumbnail={thumbnail}
mimetype={mimetype}
/>
</div> </div>
<div className="col-md-5"> <div className="col-md-5">
<EditionHeader edition={this.props.edition}/> <EditionHeader edition={this.props.edition}/>
@ -38,6 +45,9 @@ let EditionHeader = React.createClass({
}); });
let EditionDetails = React.createClass({ let EditionDetails = React.createClass({
handleSuccess(){
EditionActions.fetchOne(this.props.edition.id);
},
render() { render() {
return ( return (
<div className="ascribe-detail-header"> <div className="ascribe-detail-header">
@ -46,8 +56,20 @@ let EditionDetails = React.createClass({
<EditionDetailProperty label="id" value={ this.props.edition.bitcoin_id } /> <EditionDetailProperty label="id" value={ this.props.edition.bitcoin_id } />
<EditionDetailProperty label="owner" value={ this.props.edition.owner } /> <EditionDetailProperty label="owner" value={ this.props.edition.owner } />
<br/> <br/>
<TransferModalButton edition={ this.props.edition } currentUser={ this.props.currentUser }/> <AclButton
<ShareModalButton edition={ this.props.edition } currentUser={ this.props.currentUser }/> availableAcls={["transfer"]}
action="transfer"
editions={[this.props.edition]}
currentUser={this.props.currentUser}
handleSuccess={this.handleSuccess}
/>
<AclButton
availableAcls={["consign"]}
action="consign"
editions={[this.props.edition]}
currentUser={this.props.currentUser}
handleSuccess={this.handleSuccess}
/>
<hr/> <hr/>
</div> </div>
); );
@ -74,4 +96,3 @@ let EditionDetailProperty = React.createClass({
export default Edition; export default Edition;

View File

@ -27,7 +27,6 @@ let EditionContainer = React.createClass({
UserActions.fetchCurrentUser(); UserActions.fetchCurrentUser();
UserStore.listen(this.onChange); UserStore.listen(this.onChange);
}, },
componentDidUnmount() { componentDidUnmount() {
EditionStore.unlisten(this.onChange); EditionStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange); UserStore.unlisten(this.onChange);

View File

@ -3,14 +3,15 @@ import AppConstants from './application_constants';
let apiUrls = { let apiUrls = {
'ownership_shares_mail' : AppConstants.baseUrl + 'ownership/shares/mail/', 'ownership_shares_mail' : AppConstants.baseUrl + 'ownership/shares/mail/',
'ownership_transfers' : AppConstants.baseUrl + 'ownership/transfers/', 'ownership_transfers' : AppConstants.baseUrl + 'ownership/transfers/',
'user': AppConstants.baseUrl + 'users/', 'user': AppConstants.baseUrl + 'users/',
'pieces_list': AppConstants.baseUrl + 'pieces/', 'pieces_list': AppConstants.baseUrl + 'pieces/',
'piece': AppConstants.baseUrl + 'pieces/${piece_id}', 'piece': AppConstants.baseUrl + 'pieces/${piece_id}',
'edition': AppConstants.baseUrl + 'editions/${bitcoin_id}/', 'edition': AppConstants.baseUrl + 'editions/${bitcoin_id}/',
'editions_list': AppConstants.baseUrl + 'pieces/${piece_id}/editions/' 'editions_list': AppConstants.baseUrl + 'pieces/${piece_id}/editions/',
'ownership_loans' : AppConstants.baseUrl + 'ownership/loans/',
'ownership_consigns' : AppConstants.baseUrl + 'ownership/consigns/',
'ownership_unconsigns' : AppConstants.baseUrl + 'ownership/unconsigns/',
'ownership_unconsigns_request' : AppConstants.baseUrl + 'ownership/unconsigns/request/'
}; };
export default apiUrls; export default apiUrls;

View File

@ -0,0 +1,24 @@
import fetch from 'isomorphic-fetch';
import AppConstants from '../constants/application_constants';
import FetchApiUtils from '../utils/fetch_api_utils';
let OwnershipFetcher = {
/**
* Fetch one user from the API.
* If no arg is supplied, load the current user
*
*/
fetchLoanContract(email) {
return fetch(AppConstants.baseUrl + 'ownership/loans/contract/?loanee=' + email, {
headers: {
'Authorization': 'Basic ' + AppConstants.debugCredentialBase64
}
}).then(
(res) => res.json()
);
}
};
export default OwnershipFetcher;

View File

@ -5,12 +5,15 @@ let AlertMixin = {
setAlerts(errors){ setAlerts(errors){
let alerts = errors.map( let alerts = errors.map(
function(error) { function(error) {
let key = error + this.state.retry; return <AlertDismissable error={error} key={error}/>;
return <AlertDismissable error={error} key={key}/>;
}.bind(this) }.bind(this)
); );
this.setState({alerts: alerts, retry: this.state.retry + 1}); this.setState({alerts: alerts});
},
clearAlerts(){
this.setState({alerts: null});
} }
}; };
export default AlertMixin; export default AlertMixin;

View File

@ -9,6 +9,7 @@ export const FormMixin = {
return { return {
submitted: false submitted: false
, status: null , status: null
, errors: []
} }
}, },
@ -19,7 +20,6 @@ export const FormMixin = {
.post(this.url(), { body: this.getFormData() }) .post(this.url(), { body: this.getFormData() })
.then(response => { this.props.onRequestHide(); }) .then(response => { this.props.onRequestHide(); })
.catch(this.handleError); .catch(this.handleError);
}, },
handleError(err){ handleError(err){
@ -27,19 +27,37 @@ export const FormMixin = {
for (var input in errors){ for (var input in errors){
if (this.refs && this.refs[input] && this.refs[input].state) { if (this.refs && this.refs[input] && this.refs[input].state) {
this.refs[input].setAlerts(errors[input]); this.refs[input].setAlerts(errors[input]);
} else {
this.setState({errors: this.state.errors.concat(errors[input])});
} }
} }
this.setState({submitted: false});
} else {
this.setState({submitted: false, status: response.status});
} }
}, },
getBitcoinIds(){
return this.props.editions.map(function(edition){
return edition.bitcoin_id
})
},
getTitlesString(){
return this.props.editions.map(function(edition){
return '- \"' + edition.title + ', edition ' + edition.edition_number + '\"\n'
})
},
render(){ render(){
let alert = null; let alert = null;
if (this.state.status >= 500){ if (this.state.status >= 500){
alert = <AlertDismissable error="Something went wrong, please try again later"/>; alert = <AlertDismissable error="Something went wrong, please try again later"/>;
} }
if (this.state.errors.length > 0){
alert = this.state.errors.map(
function(error) {
return <AlertDismissable error={error} key={error}/>;
}.bind(this)
);
}
return ( return (
<div> <div>
{alert} {alert}

View File

@ -0,0 +1,57 @@
let mapAttr = {
link: 'href',
source: 'src'
}
let mapExt = {
js: 'source',
css: 'link'
}
let InjectInHeadMixin = {
/**
* Provide functions to inject `<script>` and `<link>` in `<head>`.
* Useful when you have to load a huge external library and
* you don't want to embed everything inside the build file.
*/
isPresent(tag, src) {
let attr = mapAttr[tag];
let query = `head > ${tag}[${attr}="${src}"]`;
return document.querySelector(query);
},
injectTag(tag, src){
console.log(this.foobar);
if (InjectInHeadMixin.isPresent(tag, src))
return;
let attr = mapAttr[tag];
let element = document.createElement(tag);
document.head.appendChild(element);
element[attr] = src;
},
injectStylesheet(src) {
this.injectTag('link', src);
},
injectScript(src) {
this.injectTag('source', src);
},
inject(src) {
//debugger;
let ext = src.split('.').pop();
try {
let tag = mapAttr(src);
} catch (e) {
throw new Error(`Cannot inject ${src} in the DOM, cannot guess the tag name from extension ${ext}. Valid extensions are "js" and "css".`);
}
InjectInHeadMixin.injectTag(tag, src);
}
};
export default InjectInHeadMixin;

11
js/mixins/modal_mixin.js Normal file
View File

@ -0,0 +1,11 @@
import React from 'react';
let ModalMixin = {
onRequestHide(e){
if (e)
e.preventDefault();
this.props.onRequestHide();
}
};
export default ModalMixin;

View File

@ -4,13 +4,13 @@ import UserAction from '../actions/user_actions';
class UserStore{ class UserStore{
constructor() { constructor() {
this.currentUser = {} this.currentUser = {};
this.bindActions(UserAction); this.bindActions(UserAction);
} }
onUpdateCurrentUser(user) { onUpdateCurrentUser(user) {
this.currentUser = user; this.currentUser = user;
} }
}; }
export default alt.createStore(UserStore); export default alt.createStore(UserStore);

View File

@ -1,4 +1,5 @@
import { default as _fetch } from 'isomorphic-fetch'; import { default as _fetch } from 'isomorphic-fetch';
import { argsToQueryParams } from '../utils/fetch_api_utils'; import { argsToQueryParams } from '../utils/fetch_api_utils';
@ -55,7 +56,7 @@ class Fetch {
prepareUrl(url, params, attachParamsToQuery) { prepareUrl(url, params, attachParamsToQuery) {
let newUrl = this.getUrl(url); let newUrl = this.getUrl(url);
let re = /\${(\w+)}/g; let re = /\${(\w+)}/g;
newUrl = newUrl.replace(re, (match, key) => { newUrl = newUrl.replace(re, (match, key) => {
let val = params[key] let val = params[key]
if (!val) { if (!val) {

View File

@ -27,4 +27,4 @@ export function getLangText(s, ...args) {
} }
} }
}; };

View File

@ -36,7 +36,9 @@
"react": "^0.13.2", "react": "^0.13.2",
"react-bootstrap": "~0.22.6", "react-bootstrap": "~0.22.6",
"react-router": "^0.13.3", "react-router": "^0.13.3",
"uglifyjs": "^2.4.10" "uglifyjs": "^2.4.10",
"react-bootstrap": "~0.22.6",
"react-datepicker": "~0.8.0"
}, },
"jest": { "jest": {
"scriptPreprocessor": "node_modules/babel-jest", "scriptPreprocessor": "node_modules/babel-jest",

View File

@ -164,7 +164,8 @@
text-transform: uppercase; text-transform: uppercase;
} }
.input-text-ascribe { .input-text-ascribe,
.datepicker__input {
border-bottom: 1px solid black; border-bottom: 1px solid black;
border-top: 0; border-top: 0;
border-left: 0; border-left: 0;
@ -178,6 +179,14 @@
height: 13em !important; height: 13em !important;
} }
.input-checkbox-ascribe {
text-align: left;
line-height: 1.6;
width: 90%;
margin-left: auto;
margin-right: auto;
}
/* columns of same height styles */ /* columns of same height styles */
/* http://www.minimit.com/articles/solutions-tutorials/bootstrap-3-responsive-columns-of-same-height */ /* http://www.minimit.com/articles/solutions-tutorials/bootstrap-3-responsive-columns-of-same-height */
.row-full-height { .row-full-height {
@ -234,4 +243,4 @@
.col-bottom { .col-bottom {
vertical-align: bottom; vertical-align: bottom;
} }