1
0
mirror of https://github.com/ascribe/onion.git synced 2024-12-22 09:23:13 +01:00

Merge pull request #47 from ascribe/AD-1149-additional-details

AD-1149 Contract agreement in consign forms
This commit is contained in:
Brett Sun 2015-12-04 13:38:18 +01:00
commit c9949f0cc0
23 changed files with 563 additions and 312 deletions

View File

@ -22,8 +22,7 @@ class ContractAgreementListActions {
if (contractAgreementList.count > 0) {
this.actions.updateContractAgreementList(contractAgreementList.results);
resolve(contractAgreementList.results);
}
else{
} else {
resolve(null);
}
})
@ -35,13 +34,13 @@ class ContractAgreementListActions {
);
}
fetchAvailableContractAgreementList(issuer, createContractAgreement) {
fetchAvailableContractAgreementList(issuer, createPublicContractAgreement) {
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) {
if (acceptedContractAgreementList.count > 0) {
this.actions.updateContractAgreementList(acceptedContractAgreementList.results);
} else {
// otherwise, we're looking for contract agreements that are still pending
@ -50,15 +49,13 @@ class ContractAgreementListActions {
// overcomplicate the method
OwnershipFetcher.fetchContractAgreementList(issuer, null, true)
.then((pendingContractAgreementList) => {
if(pendingContractAgreementList.count > 0) {
if (pendingContractAgreementList.count > 0) {
this.actions.updateContractAgreementList(pendingContractAgreementList.results);
} else {
} else if (createPublicContractAgreement) {
// 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);
}
// found and createPublicContractAgreement is set to true, we create a
// new public contract agreement
this.actions.createContractAgreementFromPublicContract(issuer);
}
})
.catch((err) => {
@ -81,8 +78,7 @@ class ContractAgreementListActions {
// create an agreement with the public contract if there is one
if (publicContract && publicContract.length > 0) {
return this.actions.createContractAgreement(null, publicContract[0]);
}
else {
} else {
/*
contractAgreementList in the store is already set to null;
*/
@ -91,21 +87,17 @@ class ContractAgreementListActions {
if (publicContracAgreement) {
this.actions.updateContractAgreementList([publicContracAgreement]);
}
}).catch((err) => {
console.logGlobal(err);
});
}).catch(console.logGlobal);
}
createContractAgreement(issuer, contract){
return Q.Promise((resolve, reject) => {
OwnershipFetcher.createContractAgreement(issuer, contract).then(
(contractAgreement) => {
resolve(contractAgreement);
}
).catch((err) => {
console.logGlobal(err);
reject(err);
});
OwnershipFetcher
.createContractAgreement(issuer, contract).then(resolve)
.catch((err) => {
console.logGlobal(err);
reject(err);
});
});
}
}

View File

@ -127,7 +127,7 @@ let EditionActionPanel = React.createClass({
isInline={true}>
<Property
name="bitcoin_id"
hidden={true}>
expanded={false}>
<input
type="text"
value={edition.bitcoin_id}
@ -148,7 +148,7 @@ let EditionActionPanel = React.createClass({
isInline={true}>
<Property
name="bitcoin_id"
hidden={true}>
expanded={false}>
<input
type="text"
value={edition.bitcoin_id}

View File

@ -124,8 +124,18 @@ let Form = React.createClass({
getFormData() {
let data = {};
for (let ref in this.refs) {
data[this.refs[ref].props.name] = this.refs[ref].state.value;
for (let refName in this.refs) {
const ref = this.refs[refName];
if (ref.state && 'value' in ref.state) {
// An input can also provide an `Object` as a value
// which we're going to merge with `data` (overwrites)
if(ref.state.value.constructor === Object) {
Object.assign(data, ref.state.value);
} else {
data[ref.props.name] = ref.state.value;
}
}
}
if (typeof this.props.getFormData === 'function') {
@ -238,7 +248,15 @@ let Form = React.createClass({
renderChildren() {
return ReactAddons.Children.map(this.props.children, (child, i) => {
if (child) {
return ReactAddons.addons.cloneWithProps(child, {
// Since refs will be overwritten by this functions return statement,
// we still want to be able to define refs for nested `Form` or `Property`
// children, which is why we're upfront simply invoking the callback-ref-
// function before overwriting it.
if(typeof child.ref === 'function' && this.refs[child.props.name]) {
child.ref(this.refs[child.props.name]);
}
return React.cloneElement(child, {
handleChange: this.handleChangeChild,
ref: child.props.name,
key: i,

View File

@ -6,9 +6,13 @@ import Button from 'react-bootstrap/lib/Button';
import Form from './form';
import Property from './property';
import InputContractAgreementCheckbox from './input_contract_agreement_checkbox';
import InputTextAreaToggable from './input_textarea_toggable';
import AscribeSpinner from '../ascribe_spinner';
import AclInformation from '../ascribe_buttons/acl_information';
import { getLangText } from '../../utils/lang_utils.js';
@ -21,6 +25,7 @@ let ConsignForm = React.createClass({
email: React.PropTypes.string,
message: React.PropTypes.string,
labels: React.PropTypes.object,
createPublicContractAgreement: React.PropTypes.bool,
handleSuccess: React.PropTypes.func
},
@ -30,12 +35,34 @@ let ConsignForm = React.createClass({
};
},
getInitialState() {
return {
email: this.props.email
};
},
getFormData() {
return this.props.id;
},
handleEmailOnChange(event) {
// event.target.value is the submitted email of the consignee
this.setState({
email: event && event.target && event.target.value || ''
});
},
render() {
const { autoFocusProperty, email, id, handleSuccess, message, labels, url } = this.props;
const { email } = this.state;
const {
autoFocusProperty,
createPublicContractAgreement,
email: defaultEmail,
handleSuccess,
id,
message,
labels,
url } = this.props;
return (
<Form
@ -64,12 +91,13 @@ let ConsignForm = React.createClass({
autoFocus={autoFocusProperty === 'email'}
name='consignee'
label={labels.email || getLangText('Email')}
editable={!email}
overrideForm={!!email}>
editable={!defaultEmail}
onChange={this.handleEmailOnChange}
overrideForm={!!defaultEmail}>
<input
type="email"
value={email}
placeholder={getLangText('Email of the consignee')}
defaultValue={email}
required/>
</Property>
<Property
@ -84,6 +112,15 @@ let ConsignForm = React.createClass({
placeholder={getLangText('Enter a message...')}
required />
</Property>
<Property
name='contract_agreement'
label={getLangText('Consign Contract')}
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputContractAgreementCheckbox
createPublicContractAgreement={createPublicContractAgreement}
email={email} />
</Property>
<Property
name='password'
label={getLangText('Password')}>

View File

@ -91,14 +91,14 @@ let CreateContractForm = React.createClass({
<Property
name='name'
label={getLangText('Contract name')}
hidden={true}>
expanded={false}>
<input
type="text"
value={this.state.contractName}/>
</Property>
<Property
name="is_public"
hidden={true}>
expanded={false}>
<input
type="checkbox"
value={this.props.isPublic} />

View File

@ -1,33 +1,34 @@
'use strict';
import React from 'react';
import classnames from 'classnames';
import Button from 'react-bootstrap/lib/Button';
import ContractAgreementListStore from '../../stores/contract_agreement_list_store';
import Form from './form';
import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
import InputDate from './input_date';
import InputCheckbox from './input_checkbox';
import ContractAgreementListStore from '../../stores/contract_agreement_list_store';
import ContractAgreementListActions from '../../actions/contract_agreement_list_actions';
import InputDate from './input_date';
import InputTextAreaToggable from './input_textarea_toggable';
import InputContractAgreementCheckbox from './input_contract_agreement_checkbox';
import AscribeSpinner from '../ascribe_spinner';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
import AclInformation from '../ascribe_buttons/acl_information';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
let LoanForm = React.createClass({
propTypes: {
loanHeading: React.PropTypes.string,
email: React.PropTypes.string,
gallery: React.PropTypes.string,
startdate: React.PropTypes.object,
enddate: React.PropTypes.object,
startDate: React.PropTypes.object,
endDate: React.PropTypes.object,
showPersonalMessage: React.PropTypes.bool,
showEndDate: React.PropTypes.bool,
showStartDate: React.PropTypes.bool,
@ -36,7 +37,11 @@ let LoanForm = React.createClass({
id: React.PropTypes.object,
message: React.PropTypes.string,
createPublicContractAgreement: React.PropTypes.bool,
handleSuccess: React.PropTypes.func
handleSuccess: React.PropTypes.func,
children: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
])
},
getDefaultProps() {
@ -45,148 +50,33 @@ let LoanForm = React.createClass({
showPersonalMessage: true,
showEndDate: true,
showStartDate: true,
showPassword: true,
createPublicContractAgreement: true
showPassword: true
};
},
getInitialState() {
return ContractAgreementListStore.getState();
},
componentDidMount() {
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() {
ContractAgreementListStore.unlisten(this.onChange);
return {
email: this.props.email || ''
};
},
onChange(state) {
this.setState(state);
},
getContractAgreementsOrCreatePublic(email){
ContractAgreementListActions.flushContractAgreementList.defer();
if (email) {
// fetch the available contractagreements (pending/accepted)
ContractAgreementListActions.fetchAvailableContractAgreementList(email, true);
}
},
getFormData(){
return mergeOptions(
this.props.id,
this.getContractAgreementId()
);
},
handleOnChange(event) {
handleEmailOnChange(event) {
// event.target.value is the submitted email of the loanee
if(event && event.target && event.target.value && event.target.value.match(/.*@.*\..*/)) {
this.getContractAgreementsOrCreatePublic(event.target.value);
} else {
ContractAgreementListActions.flushContractAgreementList();
}
this.setState({
email: event && event.target && event.target.value || ''
});
},
getContractAgreementId() {
if (this.state.contractAgreementList && this.state.contractAgreementList.length > 0) {
return {'contract_agreement_id': this.state.contractAgreementList[0].id};
}
return {};
handleReset() {
this.handleEmailOnChange();
},
getContractCheckbox() {
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)
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"/>
<a href={contract.blob.url_safe} target="_blank">
<span className="glyphicon glyphicon-download" aria-hidden="true"></span> {getLangText('Download contract')}
</a>
{/* 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-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
name="terms"
style={{paddingBottom: 0}}
hidden={true}>
<InputCheckbox
key="terms_implicitly"
defaultChecked={true} />
</Property>
);
}
},
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;
getFormData() {
return this.props.id;
},
getButtons() {
@ -214,14 +104,31 @@ let LoanForm = React.createClass({
},
render() {
const { email } = this.state;
const {
children,
createPublicContractAgreement,
email: defaultEmail,
handleSuccess,
gallery,
loanHeading,
message,
showPersonalMessage,
endDate,
startDate,
showEndDate,
showStartDate,
showPassword,
url } = this.props;
return (
<Form
className={classnames({'ascribe-form-bordered': this.props.loanHeading})}
className={classnames({'ascribe-form-bordered': loanHeading})}
ref='form'
url={this.props.url}
url={url}
getFormData={this.getFormData}
onReset={this.handleOnChange}
handleSuccess={this.props.handleSuccess}
onReset={this.handleReset}
handleSuccess={handleSuccess}
buttons={this.getButtons()}
spinner={
<div className="modal-footer">
@ -229,18 +136,18 @@ let LoanForm = React.createClass({
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<div className={classnames({'ascribe-form-header': true, 'hidden': !this.props.loanHeading})}>
<h3>{this.props.loanHeading}</h3>
<div className={classnames({'ascribe-form-header': true, 'hidden': !loanHeading})}>
<h3>{loanHeading}</h3>
</div>
<AclInformation aim={'form'} verbs={['acl_loan']}/>
<Property
name='loanee'
label={getLangText('Loanee Email')}
editable={!this.props.email}
onChange={this.handleOnChange}
overrideForm={!!this.props.email}>
editable={!defaultEmail}
onChange={this.handleEmailOnChange}
overrideForm={!!defaultEmail}>
<input
value={this.props.email}
value={email}
type="email"
placeholder={getLangText('Email of the loanee')}
required/>
@ -248,31 +155,31 @@ let LoanForm = React.createClass({
<Property
name='gallery'
label={getLangText('Gallery/exhibition (optional)')}
editable={!this.props.gallery}
overrideForm={!!this.props.gallery}>
editable={!gallery}
overrideForm={!!gallery}>
<input
value={this.props.gallery}
value={gallery}
type="text"
placeholder={getLangText('Gallery/exhibition (optional)')}/>
</Property>
<Property
name='startdate'
label={getLangText('Start date')}
editable={!this.props.startdate}
overrideForm={!!this.props.startdate}
hidden={!this.props.showStartDate}>
editable={!startDate}
overrideForm={!!startDate}
expanded={showStartDate}>
<InputDate
defaultValue={this.props.startdate}
defaultValue={startDate}
placeholderText={getLangText('Loan start date')} />
</Property>
<Property
name='enddate'
editable={!this.props.enddate}
overrideForm={!!this.props.enddate}
editable={!endDate}
overrideForm={!!endDate}
label={getLangText('End date')}
hidden={!this.props.showEndDate}>
expanded={showEndDate}>
<InputDate
defaultValue={this.props.enddate}
defaultValue={endDate}
placeholderText={getLangText('Loan end date')} />
</Property>
<Property
@ -280,25 +187,32 @@ let LoanForm = React.createClass({
label={getLangText('Personal Message')}
editable={true}
overrideForm={true}
hidden={!this.props.showPersonalMessage}>
expanded={showPersonalMessage}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.message}
defaultValue={message}
placeholder={getLangText('Enter a message...')}
required={this.props.showPersonalMessage}/>
required={showPersonalMessage}/>
</Property>
<Property
name='contract_agreement'
label={getLangText('Loan Contract')}
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputContractAgreementCheckbox
createPublicContractAgreement={createPublicContractAgreement}
email={email} />
</Property>
{this.getContractCheckbox()}
{this.getAppendix()}
<Property
name='password'
label={getLangText('Password')}
hidden={!this.props.showPassword}>
expanded={showPassword}>
<input
type="password"
placeholder={getLangText('Enter your password')}
required={this.props.showPassword ? 'required' : ''}/>
required={showPassword ? 'required' : ''}/>
</Property>
{this.props.children}
{children}
</Form>
);
}

View File

@ -65,8 +65,8 @@ let LoanRequestAnswerForm = React.createClass({
url={this.props.url}
email={this.state.loanRequest ? this.state.loanRequest.new_owner : null}
gallery={this.state.loanRequest ? this.state.loanRequest.gallery : null}
startdate={startDate}
enddate={endDate}
startDate={startDate}
endDate={endDate}
showPassword={true}
showPersonalMessage={false}
handleSuccess={this.props.handleSuccess}/>
@ -76,4 +76,4 @@ let LoanRequestAnswerForm = React.createClass({
}
});
export default LoanRequestAnswerForm;
export default LoanRequestAnswerForm;

View File

@ -21,7 +21,7 @@ import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
let ContractAgreementForm = React.createClass({
let SendContractAgreementForm = React.createClass({
propTypes: {
handleSuccess: React.PropTypes.func
},
@ -55,7 +55,7 @@ let ContractAgreementForm = React.createClass({
},
handleSubmitSuccess() {
let notification = 'Contract agreement send';
let notification = 'Contract agreement sent';
notification = new GlobalNotificationModel(notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
@ -148,4 +148,4 @@ let ContractAgreementForm = React.createClass({
}
});
export default ContractAgreementForm;
export default SendContractAgreementForm;

View File

@ -0,0 +1,201 @@
'use strict';
import React from 'react/addons';
import InputCheckbox from './input_checkbox';
import ContractAgreementListStore from '../../stores/contract_agreement_list_store';
import ContractAgreementListActions from '../../actions/contract_agreement_list_actions';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
import { isEmail } from '../../utils/regex_utils';
const InputContractAgreementCheckbox = React.createClass({
propTypes: {
createPublicContractAgreement: React.PropTypes.bool,
email: React.PropTypes.string,
required: React.PropTypes.bool,
// provided by Property
disabled: React.PropTypes.bool,
onChange: React.PropTypes.func,
name: React.PropTypes.string,
setExpanded: React.PropTypes.func,
// can be used to style the component from the outside
style: React.PropTypes.object
},
getDefaultProps() {
return {
createPublicContractAgreement: true
};
},
getInitialState() {
return mergeOptions(
ContractAgreementListStore.getState(),
{
value: {
terms: null,
contract_agreement_id: null
}
}
);
},
componentDidMount() {
ContractAgreementListStore.listen(this.onStoreChange);
this.getContractAgreementsOrCreatePublic(this.props.email);
},
componentWillReceiveProps({ email: nextEmail }) {
const { contractAgreementList } = this.state;
if (this.props.email !== nextEmail) {
if (isEmail(nextEmail)) {
this.getContractAgreementsOrCreatePublic(nextEmail);
} else if (contractAgreementList && contractAgreementList.length > 0) {
ContractAgreementListActions.flushContractAgreementList();
}
}
},
componentWillUnmount() {
ContractAgreementListStore.unlisten(this.onStoreChange);
},
onStoreChange(state) {
const contractAgreement = this.getContractAgreement(state.contractAgreementList);
this.props.setExpanded(!!contractAgreement);
state = mergeOptions(state, {
value: {
// If `email` is defined in this component, `getContractAgreementsOrCreatePublic`
// is either:
//
// - fetching a already existing contract agreement; or
// - trying to create a contract agreement
//
// If both attempts result in `contractAgreement` being not defined,
// it means that the receiver hasn't defined a contract, which means
// a contract agreement cannot be created, which means we don't have to
// specify `contract_agreement_id` when sending a request to the server.
contract_agreement_id: contractAgreement ? contractAgreement.id : null,
// If the receiver hasn't set a contract or the contract was
// previously accepted, we set the terms to `true`
// as we always need to at least give a boolean value for `terms`
// to the API endpoint
terms: !contractAgreement || !!contractAgreement.datetime_accepted
}
});
this.setState(state);
},
onChange(event) {
this.setState({
value: React.addons.update(this.state.value, {
terms: { $set: event.target.value }
})
});
this.props.onChange(event);
},
getContractAgreement(contractAgreementList = this.state.contractAgreementList) {
if (contractAgreementList && contractAgreementList.length > 0) {
return contractAgreementList[0];
}
},
getContractAgreementsOrCreatePublic(email) {
ContractAgreementListActions.flushContractAgreementList.defer();
if (email) {
// fetch the available contractagreements (pending/accepted)
ContractAgreementListActions.fetchAvailableContractAgreementList(email, this.props.createPublicContractAgreement);
}
},
getAppendix() {
const contractAgreement = this.getContractAgreement();
if (contractAgreement &&
contractAgreement.appendix &&
contractAgreement.appendix.default) {
return (
<div className="ascribe-property contract-appendix-form">
<p><span>{getLangText('Appendix')}</span></p>
<pre className="ascribe-pre">{contractAgreement.appendix.default}</pre>
</div>
);
}
},
getContractCheckbox() {
const contractAgreement = this.getContractAgreement();
if(contractAgreement) {
const {
datetime_accepted: datetimeAccepted,
contract: {
issuer: contractIssuer,
blob: { url_safe: contractUrl }
}
} = contractAgreement;
if(datetimeAccepted) {
return (
<div
className="notification-contract-pdf"
style={{paddingBottom: '1em'}}>
<embed
className="embed-form"
src={contractUrl}
alt="pdf"
pluginspage="http://www.adobe.com/products/acrobat/readstep2.html"/>
<a href={contractUrl} target="_blank">
<span className="glyphicon glyphicon-download" aria-hidden="true" /> {getLangText('Download contract')}
</a>
</div>
);
} else {
const {
name,
disabled,
style } = this.props;
return (
<InputCheckbox
name={name}
disabled={disabled}
style={style}
onChange={this.onChange}
key="terms_explicitly"
defaultChecked={false}>
<span>
{getLangText('I agree to the')}&nbsp;
<a href={contractUrl} target="_blank">
{getLangText('terms of ')} {contractIssuer}
</a>
</span>
</InputCheckbox>
);
}
}
},
render() {
return (
<div>
{this.getContractCheckbox()}
{this.getAppendix()}
</div>
);
}
});
export default InputContractAgreementCheckbox;

View File

@ -3,62 +3,69 @@
import React from 'react';
import ReactAddons from 'react/addons';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import Panel from 'react-bootstrap/lib/Panel';
import AppConstants from '../../constants/application_constants';
import { mergeOptions } from '../../utils/general_utils';
let Property = React.createClass({
propTypes: {
hidden: React.PropTypes.bool,
const { bool, element, string, oneOfType, func, object, arrayOf } = React.PropTypes;
autoFocus: React.PropTypes.bool,
editable: React.PropTypes.bool,
const Property = React.createClass({
propTypes: {
editable: bool,
// If we want Form to have a different value for disabled as Property has one for
// editable, we need to set overrideForm to true, as it will then override Form's
// disabled value for individual Properties
overrideForm: React.PropTypes.bool,
overrideForm: bool,
tooltip: React.PropTypes.element,
label: React.PropTypes.string,
value: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.element
label: string,
value: oneOfType([
string,
element
]),
footer: React.PropTypes.element,
handleChange: React.PropTypes.func,
ignoreFocus: React.PropTypes.bool,
name: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number
]).isRequired,
className: React.PropTypes.string,
footer: element,
handleChange: func,
ignoreFocus: bool,
name: string.isRequired,
className: string,
onClick: React.PropTypes.func,
onChange: React.PropTypes.func,
onBlur: React.PropTypes.func,
onClick: func,
onChange: func,
onBlur: func,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
children: oneOfType([
arrayOf(element),
element
]),
style: React.PropTypes.object
style: object,
expanded: bool,
checkboxLabel: string,
autoFocus: bool
},
getDefaultProps() {
return {
editable: true,
hidden: false,
expanded: true,
className: ''
};
},
getInitialState() {
const { expanded, ignoreFocus, checkboxLabel } = this.props;
return {
// We're mirroring expanded here as a state
// React's docs do NOT consider this an antipattern as long as it's
// not a "source of truth"-duplication
expanded,
// When a checkboxLabel is defined in the props, we want to set
// `ignoreFocus` to true
ignoreFocus: ignoreFocus || checkboxLabel,
// Please don't confuse initialValue with react's defaultValue.
// initialValue is set by us to ensure that a user can reset a specific
// property (after editing) to its initial value
@ -70,14 +77,26 @@ let Property = React.createClass({
},
componentDidMount() {
if (this.props.autoFocus) {
if(this.props.autoFocus) {
this.handleFocus();
}
},
componentWillReceiveProps() {
componentWillReceiveProps(nextProps) {
let childInput = this.refs.input;
// For expanded there are actually three use cases:
//
// 1. Control its value from the outside completely (do not define `checkboxLabel`)
// 2. Let it be controlled from the inside (default value can be set though via `expanded`)
// 3. Let it be controlled from a child by using `setExpanded` (`expanded` must not be
// set from the outside as a prop then(!!!))
//
// This handles case 1. and 3.
if(nextProps.expanded !== this.props.expanded && nextProps.expanded !== this.state.expanded && !this.props.checkboxLabel) {
this.setState({ expanded: nextProps.expanded });
}
// 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
@ -90,13 +109,13 @@ let Property = React.createClass({
// from native HTML elements.
// To enable developers to create input elements, they can expose a property called value
// in their state that will be picked up by property.js
} else if(childInput.state && typeof childInput.state.value !== 'undefined') {
} else if(childInput && childInput.state && typeof childInput.state.value !== 'undefined') {
this.setState({
value: childInput.state.value
});
}
if(!this.state.initialValue && childInput.props.defaultValue) {
if(!this.state.initialValue && childInput && childInput.props.defaultValue) {
this.setState({
initialValue: childInput.props.defaultValue
});
@ -148,7 +167,7 @@ let Property = React.createClass({
handleFocus() {
// if ignoreFocus (bool) is defined, then just ignore focusing on
// the property and input
if(this.props.ignoreFocus) {
if(this.state.ignoreFocus) {
return;
}
@ -200,7 +219,7 @@ let Property = React.createClass({
},
getClassName() {
if(this.props.hidden){
if(!this.state.expanded && !this.props.checkboxLabel){
return 'is-hidden';
}
if(!this.props.editable){
@ -216,40 +235,92 @@ let Property = React.createClass({
}
},
setExpanded(expanded) {
this.setState({ expanded });
},
renderChildren(style) {
return ReactAddons.Children.map(this.props.children, (child) => {
return ReactAddons.addons.cloneWithProps(child, {
style,
onChange: this.handleChange,
onFocus: this.handleFocus,
onBlur: this.handleBlur,
disabled: !this.props.editable,
ref: 'input',
name: this.props.name
// Input's props should only be cloned and propagated down the tree,
// if the component is actually being shown (!== 'expanded === false')
if((this.state.expanded && this.props.checkboxLabel) || !this.props.checkboxLabel) {
return ReactAddons.Children.map(this.props.children, (child) => {
// Since refs will be overriden by this functions return statement,
// we still want to be able to define refs for nested `Form` or `Property`
// children, which is why we're upfront simply invoking the callback-ref-
// function before overriding it.
if(typeof child.ref === 'function' && this.refs.input) {
child.ref(this.refs.input);
}
return React.cloneElement(child, {
style,
onChange: this.handleChange,
onFocus: this.handleFocus,
onBlur: this.handleBlur,
disabled: !this.props.editable,
ref: 'input',
name: this.props.name,
setExpanded: this.setExpanded
});
});
});
}
},
getLabelAndErrors() {
if(this.props.label || this.state.errors) {
return (
<p>
<span className="pull-left">{this.props.label}</span>
<span className="pull-right">{this.state.errors}</span>
</p>
);
} else {
return null;
}
},
handleCheckboxToggle() {
this.setState({expanded: !this.state.expanded});
},
getCheckbox() {
const { checkboxLabel } = this.props;
if(checkboxLabel) {
return (
<div
className="ascribe-property-collapsible-toggle"
onClick={this.handleCheckboxToggle}>
<input
onChange={this.handleCheckboxToggle}
type="checkbox"
checked={this.state.expanded}
ref="checkboxCollapsible"/>
<span className="checkbox">{' ' + checkboxLabel}</span>
</div>
);
} else {
return null;
}
},
render() {
let footer = null;
let tooltip = <span/>;
let style = this.props.style ? mergeOptions({}, this.props.style) : {};
let style = Object.assign({}, this.props.style);
if(this.props.tooltip){
tooltip = (
<Tooltip>
{this.props.tooltip}
</Tooltip>);
}
if(this.props.footer){
footer = (
<div className="ascribe-property-footer">
{this.props.footer}
</div>);
</div>
);
}
if(!this.props.editable) {
if (!this.state.expanded) {
style.paddingBottom = 0;
}
if (!this.props.editable) {
style.cursor = 'not-allowed';
}
@ -258,19 +329,17 @@ let Property = React.createClass({
className={'ascribe-property-wrapper ' + this.getClassName()}
onClick={this.handleFocus}
style={style}>
<OverlayTrigger
delay={500}
placement="top"
overlay={tooltip}>
{this.getCheckbox()}
<Panel
collapsible
expanded={this.state.expanded}
className="bs-custom-panel">
<div className={'ascribe-property ' + this.props.className}>
<p>
<span className="pull-left">{this.props.label}</span>
<span className="pull-right">{this.state.errors}</span>
</p>
{this.getLabelAndErrors()}
{this.renderChildren(style)}
{footer}
</div>
</OverlayTrigger>
</Panel>
</div>
);
}

View File

@ -8,7 +8,7 @@ import Tooltip from 'react-bootstrap/lib/Tooltip';
import Panel from 'react-bootstrap/lib/Panel';
let PropertyCollapsile = React.createClass({
let PropertyCollapsible = React.createClass({
propTypes: {
children: React.PropTypes.arrayOf(React.PropTypes.element),
checkboxLabel: React.PropTypes.string,
@ -93,4 +93,4 @@ let PropertyCollapsile = React.createClass({
}
});
export default PropertyCollapsile;
export default PropertyCollapsible;

View File

@ -292,8 +292,8 @@ let PrizePieceRatings = React.createClass({
url={ApiUrls.ownership_loans_pieces_request}
email={this.props.currentUser.email}
gallery={this.props.piece.prize.name}
startdate={today}
enddate={endDate}
startDate={today}
endDate={endDate}
showPersonalMessage={true}
showPassword={false}
handleSuccess={this.handleLoanSuccess} />

View File

@ -122,7 +122,7 @@ let CylandAdditionalDataForm = React.createClass({
<Property
name='artist_bio'
label={getLangText('Artist Biography')}
hidden={disabled && !piece.extra_data.artist_bio}>
expanded={!disabled || !!piece.extra_data.artist_bio}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.artist_bio}
@ -131,7 +131,7 @@ let CylandAdditionalDataForm = React.createClass({
<Property
name='artist_contact_information'
label={getLangText('Artist Contact Information')}
hidden={disabled && !piece.extra_data.artist_contact_information}>
expanded={!disabled || !!piece.extra_data.artist_contact_information}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.artist_contact_information}
@ -140,7 +140,7 @@ let CylandAdditionalDataForm = React.createClass({
<Property
name='conceptual_overview'
label={getLangText('Conceptual Overview')}
hidden={disabled && !piece.extra_data.conceptual_overview}>
expanded={!disabled || !!piece.extra_data.conceptual_overview}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.conceptual_overview}
@ -149,7 +149,7 @@ let CylandAdditionalDataForm = React.createClass({
<Property
name='medium'
label={getLangText('Medium (technical specifications)')}
hidden={disabled && !piece.extra_data.medium}>
expanded={!disabled || !!piece.extra_data.medium}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.medium}
@ -158,7 +158,7 @@ let CylandAdditionalDataForm = React.createClass({
<Property
name='size_duration'
label={getLangText('Size / Duration')}
hidden={disabled && !piece.extra_data.size_duration}>
expanded={!disabled || !!piece.extra_data.size_duration}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.size_duration}
@ -167,7 +167,7 @@ let CylandAdditionalDataForm = React.createClass({
<Property
name='display_instructions'
label={getLangText('Display instructions')}
hidden={disabled && !piece.extra_data.display_instructions}>
expanded={!disabled || !!piece.extra_data.display_instructions}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.display_instructions}
@ -176,7 +176,7 @@ let CylandAdditionalDataForm = React.createClass({
<Property
name='additional_details'
label={getLangText('Additional details')}
hidden={disabled && !piece.extra_data.additional_details}>
expanded={!disabled || !!piece.extra_data.additional_details}>
<InputTextAreaToggable
rows={1}
defaultValue={piece.extra_data.additional_details}

View File

@ -214,8 +214,8 @@ let CylandRegisterPiece = React.createClass({
url={ApiUrls.ownership_loans_pieces}
email={this.state.whitelabel.user}
gallery="Cyland Archive"
startdate={today}
enddate={datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain}
startDate={today}
endDate={datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain}
showStartDate={false}
showEndDate={false}
showPersonalMessage={false}

View File

@ -104,7 +104,7 @@ let IkonotvArtistDetailsForm = React.createClass({
<Property
name='artist_website'
label={getLangText('Artist Website')}
hidden={this.props.disabled && !this.props.piece.extra_data.artist_website}>
expanded={!this.props.disabled || !!this.props.piece.extra_data.artist_website}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.piece.extra_data.artist_website}
@ -113,7 +113,7 @@ let IkonotvArtistDetailsForm = React.createClass({
<Property
name='gallery_website'
label={getLangText('Website of related Gallery, Museum, etc.')}
hidden={this.props.disabled && !this.props.piece.extra_data.gallery_website}>
expanded={!this.props.disabled || !!this.props.piece.extra_data.gallery_website}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.piece.extra_data.gallery_website}
@ -122,7 +122,7 @@ let IkonotvArtistDetailsForm = React.createClass({
<Property
name='additional_websites'
label={getLangText('Additional Websites/Publications/Museums/Galleries')}
hidden={this.props.disabled && !this.props.piece.extra_data.additional_websites}>
expanded={!this.props.disabled || !!this.props.piece.extra_data.additional_websites}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.piece.extra_data.additional_websites}
@ -131,7 +131,7 @@ let IkonotvArtistDetailsForm = React.createClass({
<Property
name='conceptual_overview'
label={getLangText('Short text about the Artist')}
hidden={this.props.disabled && !this.props.piece.extra_data.conceptual_overview}>
expanded={!this.props.disabled || !!this.props.piece.extra_data.conceptual_overview}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.piece.extra_data.conceptual_overview}

View File

@ -103,7 +103,7 @@ let IkonotvArtworkDetailsForm = React.createClass({
<Property
name='medium'
label={getLangText('Medium')}
hidden={this.props.disabled && !this.props.piece.extra_data.medium}>
expanded={!this.props.disabled || !!this.props.piece.extra_data.medium}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.piece.extra_data.medium}
@ -112,7 +112,7 @@ let IkonotvArtworkDetailsForm = React.createClass({
<Property
name='size_duration'
label={getLangText('Size/Duration')}
hidden={this.props.disabled && !this.props.piece.extra_data.size_duration}>
expanded={!this.props.disabled || !!this.props.piece.extra_data.size_duration}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.piece.extra_data.size_duration}
@ -121,7 +121,7 @@ let IkonotvArtworkDetailsForm = React.createClass({
<Property
name='copyright'
label={getLangText('Copyright')}
hidden={this.props.disabled && !this.props.piece.extra_data.copyright}>
expanded={!this.props.disabled || !!this.props.piece.extra_data.copyright}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.piece.extra_data.copyright}
@ -130,7 +130,7 @@ let IkonotvArtworkDetailsForm = React.createClass({
<Property
name='courtesy_of'
label={getLangText('Courtesy of')}
hidden={this.props.disabled && !this.props.piece.extra_data.courtesy_of}>
expanded={!this.props.disabled || !!this.props.piece.extra_data.courtesy_of}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.piece.extra_data.courtesy_of}
@ -139,7 +139,7 @@ let IkonotvArtworkDetailsForm = React.createClass({
<Property
name='copyright_of_photography'
label={getLangText('Copyright of Photography')}
hidden={this.props.disabled && !this.props.piece.extra_data.copyright_of_photography}>
expanded={!this.props.disabled || !!this.props.piece.extra_data.copyright_of_photography}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.piece.extra_data.copyright_of_photography}
@ -148,7 +148,7 @@ let IkonotvArtworkDetailsForm = React.createClass({
<Property
name='additional_details'
label={getLangText('Additional Details about the artwork')}
hidden={this.props.disabled && !this.props.piece.extra_data.additional_details}>
expanded={!this.props.disabled || !!this.props.piece.extra_data.additional_details}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.piece.extra_data.additional_details}

View File

@ -199,10 +199,11 @@ let IkonotvRegisterPiece = React.createClass({
getSlideLoan() {
if (this.canSubmit()) {
let today = new Moment();
let enddate = new Moment();
enddate.add(2, 'years');
const {piece, whitelabel} = this.state;
let today = new Moment();
let endDate = new Moment();
endDate.add(2, 'years');
return (
<div data-slide-title={getLangText('Loan')}>
<Row className="no-margin">
@ -212,8 +213,8 @@ let IkonotvRegisterPiece = React.createClass({
id={{piece_id: piece.id}}
url={ApiUrls.ownership_loans_pieces}
email={whitelabel.user}
startdate={today}
enddate={enddate}
startDate={today}
endDate={endDate}
showStartDate={false}
showEndDate={false}
gallery="IkonoTV archive"

View File

@ -129,7 +129,8 @@ let MarketSubmitButton = React.createClass({
handleSuccess={this.handleAdditionalDataSuccess.bind(this, solePieceId)}
title={getLangText('Add additional information')}>
<MarketAdditionalDataForm
pieceId={solePieceId} />
pieceId={solePieceId}
submitLabel={getLangText('Continue to consignment')} />
</ModalWrapper>
<ModalWrapper

View File

@ -35,9 +35,16 @@ let MarketAdditionalDataForm = React.createClass({
isInline: React.PropTypes.bool,
showHeading: React.PropTypes.bool,
showNotification: React.PropTypes.bool,
submitLabel: React.PropTypes.string,
handleSuccess: React.PropTypes.func
},
getDefaultProps() {
return {
submitLabel: getLangText('Register work')
};
},
getInitialState() {
const pieceStore = PieceStore.getState();
@ -123,7 +130,7 @@ let MarketAdditionalDataForm = React.createClass({
},
render() {
const { isInline, handleSuccess, showHeading, showNotification } = this.props;
const { isInline, handleSuccess, showHeading, showNotification, submitLabel } = this.props;
const { piece } = this.state;
let buttons, spinner, heading;
@ -133,7 +140,7 @@ let MarketAdditionalDataForm = React.createClass({
type="submit"
className="btn btn-default btn-wide"
disabled={!this.state.isUploadReady}>
{getLangText('Register work')}
{submitLabel}
</button>
);

View File

@ -25,7 +25,7 @@ import CylandPieceList from './components/cyland/cyland_piece_list';
import IkonotvLanding from './components/ikonotv/ikonotv_landing';
import IkonotvPieceList from './components/ikonotv/ikonotv_piece_list';
import ContractAgreementForm from '../../../components/ascribe_forms/form_contract_agreement';
import SendContractAgreementForm from '../../../components/ascribe_forms/form_send_contract_agreement';
import IkonotvRegisterPiece from './components/ikonotv/ikonotv_register_piece';
import IkonotvPieceContainer from './components/ikonotv/ikonotv_detail/ikonotv_piece_container';
import IkonotvContractNotifications from './components/ikonotv/ikonotv_contract_notifications';
@ -135,7 +135,7 @@ let ROUTES = {
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(ContractSettings)}/>
<Route
path='request_loan'
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(ContractAgreementForm)}
component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(SendContractAgreementForm)}
headerTitle='SEND NEW CONTRACT'
aclName='acl_create_contractagreement'/>
<Route

7
js/utils/regex_utils.js Normal file
View File

@ -0,0 +1,7 @@
'use strict'
export function isEmail(string) {
// This is a bit of a weak test for an email, but you really can't win them all
// http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address
return !!string && string.match(/.*@.*\..*/);
}

View File

@ -31,16 +31,11 @@
margin-top: .5em;
margin-bottom: 1em;
.loan-form {
margin-top: .5em;
&.embed-form {
height: 45vh;
}
}
.loan-form {
height: 40vh;
}
.notification-contract-pdf-download {
text-align: left;
margin-left: 1em;
@ -69,4 +64,8 @@
padding-left: 0;
width: 100%;
}
}
.ascribe-property.contract-appendix-form {
padding-left: 0;
}

View File

@ -1,3 +1,8 @@
.panel {
/* Here we are overriding bootstrap to show the is-focused background color */
background-color: transparent;
}
.ascribe-panel-wrapper {
border: 1px solid #ddd;
height: 5em;