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

solve merge conflicts

This commit is contained in:
Tim Daubenschütz 2015-06-16 09:02:48 +02:00
commit 7910384d99
51 changed files with 848 additions and 439 deletions

View File

@ -105,7 +105,7 @@ gulp.task('sass:build', function () {
}); });
gulp.task('sass:watch', function () { gulp.task('sass:watch', function () {
gulp.watch('./sass/**/*.scss', ['sass']); gulp.watch('./sass/**/*.scss', ['sass:build']);
}); });
gulp.task('copy', function () { gulp.task('copy', function () {

View File

@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<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="<%= BASE_URL %>static/css/main.css"> <link rel="stylesheet" href="<%= BASE_URL %>static/css/main.css">
<link rel="stylesheet" href="<%= BASE_URL %>static/css/maps/main.css.map">
<script> <script>
window.BASE_URL = '<%= BASE_URL %>'; window.BASE_URL = '<%= BASE_URL %>';
window.API_ENDPOINT = '<%= API_ENDPOINT %>'; window.API_ENDPOINT = '<%= API_ENDPOINT %>';

View File

@ -9,7 +9,9 @@ class EditionListActions {
this.generateActions( this.generateActions(
'updateEditionList', 'updateEditionList',
'selectEdition', 'selectEdition',
'clearAllEditionSelections' 'clearAllEditionSelections',
'closeAllEditionLists',
'toggleEditionList'
); );
} }
@ -19,19 +21,24 @@ class EditionListActions {
orderAsc = true; orderAsc = true;
} }
EditionListFetcher return new Promise((resolve, reject) => {
.fetch(pieceId, orderBy, orderAsc) EditionListFetcher
.then((res) => { .fetch(pieceId, orderBy, orderAsc)
this.actions.updateEditionList({ .then((res) => {
'editionListOfPiece': res.editions, this.actions.updateEditionList({
pieceId, 'editionListOfPiece': res.editions,
orderBy, pieceId,
orderAsc orderBy,
orderAsc
});
resolve(res);
})
.catch((err) => {
reject(err);
console.log(err);
}); });
}) });
.catch((err) => {
console.log(err);
});
} }
} }

View File

@ -8,9 +8,7 @@ import PieceListFetcher from '../fetchers/piece_list_fetcher';
class PieceListActions { class PieceListActions {
constructor() { constructor() {
this.generateActions( this.generateActions(
'updatePieceList', 'updatePieceList'
'showEditionList',
'closeAllEditionLists'
); );
} }

View File

@ -1,12 +1,9 @@
'use strict'; 'use strict';
import React from 'react'; 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;
let AccordionListItem = React.createClass({ let AccordionListItem = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, className: React.PropTypes.string,

View File

@ -13,7 +13,6 @@ let AccordionListItemTable = React.createClass({
parentId: React.PropTypes.number, parentId: React.PropTypes.number,
itemList: React.PropTypes.array, itemList: React.PropTypes.array,
columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(ColumnModel)), columnList: React.PropTypes.arrayOf(React.PropTypes.instanceOf(ColumnModel)),
numOfTableItems: React.PropTypes.number,
show: React.PropTypes.bool, show: React.PropTypes.bool,
changeOrder: React.PropTypes.func, changeOrder: React.PropTypes.func,
orderBy: React.PropTypes.string, orderBy: React.PropTypes.string,

View File

@ -1,7 +1,6 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import Router from 'react-router';
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';
@ -19,15 +18,11 @@ import TableItemAclFiltered from '../ascribe_table/table_item_acl_filtered';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
let AccordionListItemTableEditions = React.createClass({ let AccordionListItemTableEditions = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, className: React.PropTypes.string,
parentId: React.PropTypes.number, parentId: React.PropTypes.number
numOfEditions: React.PropTypes.number,
show: React.PropTypes.bool
}, },
getInitialState() { getInitialState() {
@ -64,8 +59,13 @@ let AccordionListItemTableEditions = React.createClass({
}, },
toggleTable() { toggleTable() {
PieceListActions.showEditionList(this.props.parentId); let isEditionListOpen = this.state.isEditionListOpenForPieceId[this.props.parentId] ? this.state.isEditionListOpenForPieceId[this.props.parentId].show : false;
EditionListActions.fetchEditionList(this.props.parentId); if(isEditionListOpen) {
EditionListActions.toggleEditionList(this.props.parentId);
} else {
EditionListActions.toggleEditionList(this.props.parentId);
EditionListActions.fetchEditionList(this.props.parentId);
}
}, },
changeEditionListOrder(orderBy, orderAsc) { changeEditionListOrder(orderBy, orderAsc) {
@ -77,6 +77,7 @@ let AccordionListItemTableEditions = React.createClass({
let allEditionsCount = 0; let allEditionsCount = 0;
let orderBy; let orderBy;
let orderAsc; let orderAsc;
let show;
// here we need to check if all editions of a specific // here we need to check if all editions of a specific
// piece are already defined. Otherwise .length will throw an error and we'll not // piece are already defined. Otherwise .length will throw an error and we'll not
@ -88,7 +89,11 @@ let AccordionListItemTableEditions = React.createClass({
orderAsc = this.state.editionList[this.props.parentId].orderAsc; orderAsc = this.state.editionList[this.props.parentId].orderAsc;
} }
let transition = new TransitionModel('edition', 'editionId', 'bitcoin_id', PieceListActions.closeAllEditionLists); if(this.props.parentId in this.state.isEditionListOpenForPieceId) {
show = this.state.isEditionListOpenForPieceId[this.props.parentId].show;
}
let transition = new TransitionModel('edition', 'editionId', 'bitcoin_id');
let columnList = [ let columnList = [
new ColumnModel( new ColumnModel(
@ -111,10 +116,10 @@ let AccordionListItemTableEditions = React.createClass({
new ColumnModel( new ColumnModel(
(item) => { (item) => {
return { return {
'content': item.edition_number 'content': item.edition_number + ' of ' + item.num_editions
}; }, }; },
'edition_number', 'edition_number',
'#', 'Edition',
TableItemText, TableItemText,
1, 1,
true, true,
@ -153,16 +158,14 @@ let AccordionListItemTableEditions = React.createClass({
parentId={this.props.parentId} parentId={this.props.parentId}
itemList={this.state.editionList[this.props.parentId]} itemList={this.state.editionList[this.props.parentId]}
columnList={columnList} columnList={columnList}
numOfTableItems={this.props.numOfEditions} show={show}
show={this.props.show}
orderBy={orderBy} orderBy={orderBy}
orderAsc={orderAsc} orderAsc={orderAsc}
changeOrder={this.changeEditionListOrder}> changeOrder={this.changeEditionListOrder}>
<AccordionListItemTableToggle <AccordionListItemTableToggle
className="ascribe-accordion-list-table-toggle" className="ascribe-accordion-list-table-toggle"
onClick={this.toggleTable} onClick={this.toggleTable}
show={this.props.show} show={show} />
numOfTableItems={this.props.numOfEditions} />
</AccordionListItemTable> </AccordionListItemTable>
</div> </div>

View File

@ -6,8 +6,7 @@ let AccordionListItemTableToggle = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, className: React.PropTypes.string,
onClick: React.PropTypes.func, onClick: React.PropTypes.func,
show: React.PropTypes.bool, show: React.PropTypes.bool
numOfTableItems: React.PropTypes.number
}, },
render() { render() {
@ -15,7 +14,7 @@ let AccordionListItemTableToggle = React.createClass({
<span <span
className={this.props.className} className={this.props.className}
onClick={this.props.onClick}> onClick={this.props.onClick}>
{this.props.show ? 'Hide all ' + this.props.numOfTableItems + ' Editions' : 'Show all ' + this.props.numOfTableItems + ' Editions'} {this.props.show ? 'Hide all Editions' : 'Show all Editions'}
</span> </span>
); );
} }

View File

@ -3,12 +3,16 @@
import React from 'react'; import React from 'react';
import ConsignForm from '../ascribe_forms/form_consign'; import ConsignForm from '../ascribe_forms/form_consign';
import UnConsignForm from '../ascribe_forms/form_unconsign';
import TransferForm from '../ascribe_forms/form_transfer'; import TransferForm from '../ascribe_forms/form_transfer';
import LoanForm from '../ascribe_forms/form_loan'; import LoanForm from '../ascribe_forms/form_loan';
import ShareForm from '../ascribe_forms/form_share_email'; import ShareForm from '../ascribe_forms/form_share_email';
import ModalWrapper from '../ascribe_modal/modal_wrapper'; import ModalWrapper from '../ascribe_modal/modal_wrapper';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
let AclButton = React.createClass({ let AclButton = React.createClass({
propTypes: { propTypes: {
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired, action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
@ -23,31 +27,47 @@ let AclButton = React.createClass({
return { return {
title: 'Consign artwork', title: 'Consign artwork',
tooltip: 'Have someone else sell the artwork', tooltip: 'Have someone else sell the artwork',
form: <ConsignForm /> form: <ConsignForm currentUser={ this.props.currentUser } editions={ this.props.editions }/>,
handleSuccess: this.showNotification
}; };
} }
else if (this.props.action === 'transfer') { if (this.props.action === 'unconsign'){
return {
title: 'Unconsign artwork',
tooltip: 'Have the owner manage his sales again',
form: <UnConsignForm currentUser={ this.props.currentUser } editions={ this.props.editions }/>,
handleSuccess: this.showNotification
};
}else if (this.props.action === 'transfer') {
return { return {
title: 'Transfer artwork', title: 'Transfer artwork',
tooltip: 'Transfer the ownership of the artwork', tooltip: 'Transfer the ownership of the artwork',
form: <TransferForm /> form: <TransferForm currentUser={ this.props.currentUser } editions={ this.props.editions }/>,
handleSuccess: this.showNotification
}; };
} }
else if (this.props.action === 'loan'){ else if (this.props.action === 'loan'){
return { return {
title: 'Loan artwork', title: 'Loan artwork',
tooltip: 'Loan your artwork for a limited period of time', tooltip: 'Loan your artwork for a limited period of time',
form: <LoanForm /> form: <LoanForm currentUser={ this.props.currentUser } editions={ this.props.editions }/>,
handleSuccess: this.showNotification
}; };
} }
else if (this.props.action === 'share'){ else if (this.props.action === 'share'){
return { return {
title: 'Share artwork', title: 'Share artwork',
tooltip: 'Share the artwork', tooltip: 'Share the artwork',
form: <ShareForm /> form: <ShareForm currentUser={ this.props.currentUser } editions={ this.props.editions }/>,
handleSuccess: this.showNotification
}; };
} }
}, },
showNotification(response){
this.props.handleSuccess();
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
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(); let aclProps = this.actionProperties();
@ -58,9 +78,7 @@ let AclButton = React.createClass({
{this.props.action.toUpperCase()} {this.props.action.toUpperCase()}
</div> </div>
} }
currentUser={ this.props.currentUser } handleSuccess={ aclProps.handleSuccess }
editions={ this.props.editions }
handleSuccess={ this.props.handleSuccess }
title={ aclProps.title } title={ aclProps.title }
tooltip={ aclProps.tooltip }> tooltip={ aclProps.tooltip }>
{ aclProps.form } { aclProps.form }

View File

@ -6,6 +6,7 @@ import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store'; import UserStore from '../../stores/user_store';
import AclButton from '../ascribe_buttons/acl_button'; import AclButton from '../ascribe_buttons/acl_button';
import DeleteButton from '../ascribe_buttons/delete_button';
let AclButtonList = React.createClass({ let AclButtonList = React.createClass({
propTypes: { propTypes: {
@ -47,6 +48,12 @@ let AclButtonList = React.createClass({
editions={this.props.editions} editions={this.props.editions}
currentUser={this.state.currentUser} currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} /> handleSuccess={this.props.handleSuccess} />
<AclButton
availableAcls={this.props.availableAcls}
action="unconsign"
editions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} />
<AclButton <AclButton
availableAcls={this.props.availableAcls} availableAcls={this.props.availableAcls}
action="loan" action="loan"
@ -59,6 +66,7 @@ let AclButtonList = React.createClass({
editions={this.props.editions} editions={this.props.editions}
currentUser={this.state.currentUser} currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} /> handleSuccess={this.props.handleSuccess} />
<DeleteButton editions={this.props.editions}/>
</div> </div>
); );
} }

View File

@ -2,19 +2,20 @@
import React from 'react'; import React from 'react';
/*
Is this even used somewhere?
Deprecate? 5.6.15 - Tim
*/
let ButtonSubmitOrClose = React.createClass({ let ButtonSubmitOrClose = React.createClass({
propTypes: {
submitted: React.PropTypes.bool.isRequired,
text: React.PropTypes.string.isRequired,
onClose: React.PropTypes.func.isRequired
},
render() { render() {
if (this.props.submitted){ if (this.props.submitted){
return ( return (
<div className="modal-footer"> <div className="modal-footer">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" /> <img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</div> </div>
) );
} }
return ( return (
<div className="modal-footer"> <div className="modal-footer">

View File

@ -0,0 +1,67 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Button from 'react-bootstrap/lib/Button';
import EditionDeleteForm from '../ascribe_forms/form_delete_edition';
import EditionRemoveFromCollectionForm from '../ascribe_forms/form_remove_editions_from_collection';
import ModalWrapper from '../ascribe_modal/modal_wrapper';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getAvailableAcls } from '../../utils/acl_utils';
import EditionListActions from '../../actions/edition_list_actions';
let DeleteButton = React.createClass({
propTypes: {
editions: React.PropTypes.array.isRequired
},
mixins: [Router.Navigation],
showNotification(response) {
this.props.editions
.forEach((edition) => {
EditionListActions.fetchEditionList(edition.parent);
});
EditionListActions.clearAllEditionSelections();
EditionListActions.closeAllEditionLists();
this.transitionTo('pieces');
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
render: function () {
let availableAcls = getAvailableAcls(this.props.editions);
let btnDelete = null;
let content = null;
if (availableAcls.indexOf('delete') > -1) {
content = <EditionDeleteForm editions={ this.props.editions }/>;
btnDelete = <Button bsStyle="danger" bsSize="small">DELETE</Button>;
}
else if (availableAcls.indexOf('del_from_collection') > -1){
content = <EditionRemoveFromCollectionForm editions={ this.props.editions }/>;
btnDelete = <Button bsStyle="danger" bsSize="small">REMOVE FROM COLLECTION</Button>;
}
else{
return <div></div>;
}
return (
<ModalWrapper
button={ btnDelete }
handleSuccess={ this.showNotification }
title='Remove Edition'
tooltip='Click to remove edition'>
{ content }
</ModalWrapper>
);
}
});
export default DeleteButton;

View File

@ -4,6 +4,10 @@ import React from 'react';
import Alert from 'react-bootstrap/lib/Alert'; import Alert from 'react-bootstrap/lib/Alert';
let AlertDismissable = React.createClass({ let AlertDismissable = React.createClass({
propTypes: {
error: React.PropTypes.array.isRequired
},
getInitialState() { getInitialState() {
return { return {
alertVisible: true alertVisible: true
@ -20,7 +24,6 @@ let AlertDismissable = React.createClass({
render() { render() {
if (this.state.alertVisible) { if (this.state.alertVisible) {
let key = this.props.error;
return ( return (
<Alert bsStyle='danger' onDismiss={this.hide}> <Alert bsStyle='danger' onDismiss={this.hide}>
{this.props.error} {this.props.error}

View File

@ -11,6 +11,7 @@ import ButtonSubmitOrClose from '../ascribe_buttons/button_submit_close';
let ConsignForm = React.createClass({ let ConsignForm = React.createClass({
mixins: [FormMixin], mixins: [FormMixin],
url() { url() {
return ApiUrls.ownership_consigns; return ApiUrls.ownership_consigns;
}, },

View File

@ -0,0 +1,35 @@
'use strict';
import React from 'react';
import fetch from '../../utils/fetch';
import ApiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
let EditionDeleteForm = React.createClass({
mixins: [FormMixin],
url() {
return fetch.prepareUrl(ApiUrls.edition_delete, {edition_id: this.getBitcoinIds().join()});
},
httpVerb(){
return 'delete';
},
renderForm () {
return (
<div className="modal-body">
<p>Are you sure you would like to permanently delete this edition&#63;</p>
<p>This is an irrevocable action.</p>
<div className="modal-footer">
<button type="submit" className="btn btn-ascribe-inv" onClick={this.submit}>YES, DELETE</button>
<button className="btn btn-ascribe" onClick={this.props.onRequestHide}>CLOSE</button>
</div>
</div>
);
}
});
export default EditionDeleteForm;

View File

@ -52,7 +52,7 @@ let LoanForm = React.createClass({
loaneeHasContract: true loaneeHasContract: true
}); });
} }
else{ else {
this.resetLoanContract(); this.resetLoanContract();
} }
}) })
@ -63,10 +63,11 @@ let LoanForm = React.createClass({
}, },
resetLoanContract(){ resetLoanContract(){
this.setState({contract_key: null, this.setState({
contract_url: null, contract_key: null,
loaneeHasContract: false contract_url: null,
}); loaneeHasContract: false
});
}, },
renderForm() { renderForm() {

View File

@ -0,0 +1,43 @@
'use strict';
import React from 'react';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputTextAreaToggable from './input_textarea_toggable';
let PersonalNoteForm = React.createClass({
mixins: [FormMixin],
url() {
return apiUrls.note_notes;
},
getFormData() {
return {
bitcoin_id: this.getBitcoinIds().join(),
note: this.refs.personalNote.state.value
};
},
renderForm() {
return (
<form id="personal_note_content" role="form" key="personal_note_content">
<InputTextAreaToggable
ref="personalNote"
className="form-control"
defaultValue={this.props.editions[0].note_from_user}
rows={3}
editable={true}
required=""
onSubmit={this.submit}
/>
</form>
);
}
});
export default PersonalNoteForm;

View File

@ -0,0 +1,48 @@
'use strict';
import React from 'react';
import fetch from '../../utils/fetch';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import InputTextAreaToggable from './input_textarea_toggable';
let PieceExtraDataForm = React.createClass({
mixins: [FormMixin],
url() {
return fetch.prepareUrl(apiUrls.piece_extradata, {piece_id: this.props.editions[0].bitcoin_id});
},
getFormData() {
let extradata = {};
extradata[this.props.name] = this.refs[this.props.name].state.value;
return {
bitcoin_id: this.getBitcoinIds().join(),
extradata: extradata
};
},
renderForm() {
return (
<form role="form" key={this.props.name}>
<h5>{this.props.title}</h5>
<InputTextAreaToggable
ref={this.props.name}
className="form-control"
defaultValue={this.props.editions[0].extra_data[this.props.name]}
rows={3}
editable={true}
required=""
onSubmit={this.submit}
/>
</form>
);
}
});
export default PieceExtraDataForm;

View File

@ -0,0 +1,35 @@
'use strict';
import React from 'react';
import fetch from '../../utils/fetch';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
let EditionRemoveFromCollectionForm = React.createClass({
mixins: [FormMixin],
url() {
return fetch.prepareUrl(apiUrls.edition_remove_from_collection, {edition_id: this.getBitcoinIds().join()});
},
httpVerb(){
return 'delete';
},
renderForm () {
return (
<div className="modal-body">
<p>Are you sure you would like to remove these editions from your collection&#63;</p>
<p>This is an irrevocable action.</p>
<div className="modal-footer">
<button type="submit" className="btn btn-ascribe-inv" onClick={this.submit}>YES, REMOVE</button>
<button className="btn btn-ascribe" onClick={this.props.onRequestHide}>CLOSE</button>
</div>
</div>
);
}
});
export default EditionRemoveFromCollectionForm;

View File

@ -0,0 +1,79 @@
'use strict';
import React from 'react';
import Alert from 'react-bootstrap/lib/Alert';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
let RequestActionForm = React.createClass({
mixins: [FormMixin],
url(e){
let edition = this.props.editions[0];
if (e.target.id === 'request_accept'){
if (edition.request_action === 'consign'){
return apiUrls.ownership_consigns_confirm;
}
else if (edition.request_action === 'unconsign'){
return apiUrls.ownership_unconsigns;
}
else if (edition.request_action === 'loan'){
return apiUrls.ownership_loans_confirm;
}
}
else if(e.target.id === 'request_deny'){
if (edition.request_action === 'consign') {
return apiUrls.ownership_consigns_deny;
}
else if (edition.request_action === 'unconsign') {
return apiUrls.ownership_unconsigns_deny;
}
else if (edition.request_action === 'loan') {
return apiUrls.ownership_loans_deny;
}
}
},
handleRequest: function(e){
e.preventDefault();
this.submit(e);
},
getFormData() {
return {
bitcoin_id: this.getBitcoinIds().join()
};
},
renderForm() {
let edition = this.props.editions[0];
let buttons = (
<span>
<span>
<div id="request_accept" onClick={this.handleRequest} className='btn btn-default btn-sm'>ACCEPT</div>
</span>
<span>
<div id="request_deny" onClick={this.handleRequest} className='btn btn-default btn-sm'>REJECT</div>
</span>
</span>
);
if (this.state.submitted){
buttons = (
<span>
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_small.gif" />
</span>
);
}
return (
<Alert bsStyle='warning'>
<span>{ edition.owner } requests you { edition.request_action } this edition.&nbsp;&nbsp;</span>
{buttons}
</Alert>
);
}
});
export default RequestActionForm;

View File

@ -17,19 +17,20 @@ let UnConsignForm = React.createClass({
getFormData() { getFormData() {
return { return {
bitcoin_id: this.props.edition.bitcoin_id, bitcoin_id: this.getBitcoinIds().join(),
unconsign_message: this.refs.unconsign_message.state.value, unconsign_message: this.refs.unconsign_message.state.value,
password: this.refs.password.state.value password: this.refs.password.state.value
}; };
}, },
renderForm() { renderForm() {
let title = this.props.edition.title; let title = this.getTitlesString().join('');
let username = this.props.currentUser.username; let username = this.props.currentUser.username;
let message = let message =
`Hi, `Hi,
I un-consign \" ${title} \" from you. I un-consign:
${title}from you.
Truly yours, Truly yours,
${username}`; ${username}`;

View File

@ -5,6 +5,11 @@ import React from 'react';
import AlertMixin from '../../mixins/alert_mixin'; import AlertMixin from '../../mixins/alert_mixin';
let InputCheckbox = React.createClass({ let InputCheckbox = React.createClass({
propTypes: {
submitted: React.PropTypes.bool.isRequired,
required: React.PropTypes.string.isRequired,
label: React.PropTypes.string.isRequired
},
mixins: [AlertMixin], mixins: [AlertMixin],

View File

@ -6,6 +6,10 @@ import AlertMixin from '../../mixins/alert_mixin';
import DatePicker from 'react-datepicker/dist/react-datepicker'; import DatePicker from 'react-datepicker/dist/react-datepicker';
let InputDate = React.createClass({ let InputDate = React.createClass({
propTypes: {
submitted: React.PropTypes.bool,
placeholderText: React.PropTypes.string
},
mixins: [AlertMixin], mixins: [AlertMixin],
@ -20,11 +24,10 @@ let InputDate = React.createClass({
handleChange(date) { handleChange(date) {
this.setState({ this.setState({
value: date, value: date,
value_formatted: date.format("YYYY-MM-DD")}); value_formatted: date.format('YYYY-MM-DD')});
}, },
render: function () { render: function () {
let className = 'form-control input-text-ascribe';
let alerts = (this.props.submitted) ? null : this.state.alerts; let alerts = (this.props.submitted) ? null : this.state.alerts;
return ( return (
<div className="form-group"> <div className="form-group">
@ -37,24 +40,6 @@ let InputDate = React.createClass({
placeholderText={this.props.placeholderText}/> placeholderText={this.props.placeholderText}/>
</div> </div>
); );
// CAN THIS BE REMOVED???
//
// - Tim?
//
//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>
//)
} }
}); });

View File

@ -5,6 +5,10 @@ import React from 'react';
import AlertMixin from '../../mixins/alert_mixin'; import AlertMixin from '../../mixins/alert_mixin';
let InputHidden = React.createClass({ let InputHidden = React.createClass({
propTypes: {
submitted: React.PropTypes.bool,
value: React.PropTypes.string
},
mixins: [AlertMixin], mixins: [AlertMixin],

View File

@ -5,6 +5,13 @@ import React from 'react';
import AlertMixin from '../../mixins/alert_mixin'; import AlertMixin from '../../mixins/alert_mixin';
let InputText = React.createClass({ let InputText = React.createClass({
propTypes: {
submitted: React.PropTypes.bool,
onBlur: React.PropTypes.func,
type: React.PropTypes.string,
required: React.PropTypes.string,
placeHolder: React.PropTypes.string
},
mixins: [AlertMixin], mixins: [AlertMixin],

View File

@ -5,6 +5,11 @@ import React from 'react';
import AlertMixin from '../../mixins/alert_mixin'; import AlertMixin from '../../mixins/alert_mixin';
let InputTextArea = React.createClass({ let InputTextArea = React.createClass({
propTypes: {
submitted: React.PropTypes.bool,
required: React.PropTypes.string,
defaultValue: React.PropTypes.string
},
mixins: [AlertMixin], mixins: [AlertMixin],

View File

@ -0,0 +1,81 @@
'use strict';
import React from 'react';
import AlertMixin from '../../mixins/alert_mixin';
import TextareaAutosize from 'react-textarea-autosize';
import Button from 'react-bootstrap/lib/Button';
let InputTextAreaToggable = React.createClass({
propTypes: {
editable: React.PropTypes.bool.isRequired,
submitted: React.PropTypes.bool,
rows: React.PropTypes.number.isRequired,
onSubmit: React.PropTypes.func.isRequired,
required: React.PropTypes.string,
defaultValue: React.PropTypes.string
},
mixins: [AlertMixin],
getInitialState() {
return {
value: this.props.defaultValue,
edited: false,
alerts: null // needed in AlertMixin
};
},
handleChange(event) {
this.setState({
value: event.target.value,
edited: true
});
},
reset(){
this.setState(this.getInitialState());
},
submit(){
this.props.onSubmit();
this.setState({edited: false});
},
render() {
let className = 'form-control ascribe-textarea';
let buttons = null;
let textarea = null;
if (this.props.editable && this.state.edited){
buttons = (
<div className="pull-right">
<Button className="ascribe-btn" onClick={this.submit}>Save</Button>
<Button className="ascribe-btn" onClick={this.reset}>Cancel</Button>
</div>
);
}
if (this.props.editable){
className = className + ' ascribe-textarea-editable';
textarea = (
<TextareaAutosize
className={className}
value={this.state.value}
rows={this.props.rows}
required={this.props.required}
onChange={this.handleChange}
placeholder='Write something...' />
);
}
else{
textarea = <pre className="ascribe-pre">{this.state.value}</pre>;
}
let alerts = (this.props.submitted) ? null : this.state.alerts;
return (
<div className="form-group">
{alerts}
{textarea}
{buttons}
</div>
);
}
});
export default InputTextAreaToggable;

View File

@ -1,45 +0,0 @@
'use strict';
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

@ -1,44 +0,0 @@
'use strict';
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 ModalMixin from '../../mixins/modal_mixin';
import ShareForm from '../ascribe_forms/form_share_email';
let ShareModalButton = React.createClass({
render() {
return (
<OverlayTrigger delay={500} placement="left" overlay={<Tooltip>Share the artwork</Tooltip>}>
<ModalTrigger modal={<ShareModal edition={this.props.edition}
currentUser={this.props.currentUser}/>}>
<div className="btn btn-ascribe-inv btn-glyph-ascribe">
<span className="glyph-ascribe-share2"></span>
</div>
</ModalTrigger>
</OverlayTrigger>
);
}
});
let ShareModal = React.createClass({
mixins: [ModalMixin],
render() {
return (
<Modal {...this.props} title="Share artwork">
<div className="modal-body">
<ShareForm edition={this.props.edition}
currentUser={this.props.currentUser}
onRequestHide={this.onRequestHide}/>
</div>
</Modal>
);
}
});
export default ShareModalButton;

View File

@ -1,45 +0,0 @@
'use strict';
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

@ -1,45 +0,0 @@
'use strict';
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

@ -11,6 +11,14 @@ import Tooltip from 'react-bootstrap/lib/Tooltip';
import ModalMixin from '../../mixins/modal_mixin'; import ModalMixin from '../../mixins/modal_mixin';
let ModalWrapper = React.createClass({ let ModalWrapper = React.createClass({
propTypes: {
title: React.PropTypes.string.isRequired,
onRequestHide: React.PropTypes.func,
handleSuccess: React.PropTypes.func.isRequired,
button: React.PropTypes.object.isRequired,
children: React.PropTypes.object,
tooltip: React.PropTypes.string.isRequired
},
render() { render() {
return ( return (
@ -19,8 +27,6 @@ let ModalWrapper = React.createClass({
<ModalTrigger modal={ <ModalTrigger modal={
<ModalBody <ModalBody
title={this.props.title} title={this.props.title}
editions={this.props.editions}
currentUser={this.props.currentUser}
handleSuccess={this.props.handleSuccess}> handleSuccess={this.props.handleSuccess}>
{this.props.children} {this.props.children}
</ModalBody> </ModalBody>
@ -32,22 +38,26 @@ let ModalWrapper = React.createClass({
} }
}); });
//
let ModalBody = React.createClass({ let ModalBody = React.createClass({
propTypes: {
onRequestHide: React.PropTypes.func,
handleSuccess: React.PropTypes.func,
children: React.PropTypes.object,
title: React.PropTypes.string.isRequired
},
mixins: [ModalMixin], mixins: [ModalMixin],
handleSuccess(){ handleSuccess(response){
this.props.handleSuccess(); this.props.handleSuccess(response);
this.props.onRequestHide(); this.props.onRequestHide();
}, },
renderChildren() { renderChildren() {
return ReactAddons.Children.map(this.props.children, (child) => { return ReactAddons.Children.map(this.props.children, (child) => {
return ReactAddons.addons.cloneWithProps(child, { return ReactAddons.addons.cloneWithProps(child, {
editions: this.props.editions, onRequestHide: this.props.onRequestHide,
currentUser: this.props.currentUser,
onRequestHide: this.onRequestHide,
handleSuccess: this.handleSuccess handleSuccess: this.handleSuccess
}); });
}); });

View File

@ -13,7 +13,9 @@ import UserActions from '../../actions/user_actions';
import PieceListBulkModalSelectedEditionsWidget from './piece_list_bulk_modal_selected_editions_widget'; import PieceListBulkModalSelectedEditionsWidget from './piece_list_bulk_modal_selected_editions_widget';
import AclButtonList from '../ascribe_buttons/acl_button_list'; import AclButtonList from '../ascribe_buttons/acl_button_list';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getAvailableAcls } from '../../utils/acl_utils';
let PieceListBulkModal = React.createClass({ let PieceListBulkModal = React.createClass({
propTypes: { propTypes: {
@ -61,31 +63,9 @@ let PieceListBulkModal = React.createClass({
return selectedEditionList; return selectedEditionList;
}, },
intersectAcls(a, b) {
return a.filter((val) => b.indexOf(val) > -1);
},
getAvailableAcls() {
let availableAcls = [];
let selectedEditionList = this.fetchSelectedEditionList();
// If no edition has been selected, availableActions is empty
// If only one edition has been selected, their actions are available
// If more than one editions have been selected, their acl properties are intersected
if(selectedEditionList.length >= 1) {
availableAcls = selectedEditionList[0].acl;
}
if(selectedEditionList.length >= 2) {
for(let i = 1; i < selectedEditionList.length; i++) {
availableAcls = this.intersectAcls(availableAcls, selectedEditionList[i].acl);
}
}
return availableAcls;
},
clearAllSelections() { clearAllSelections() {
EditionListActions.clearAllEditionSelections(); EditionListActions.clearAllEditionSelections();
EditionListActions.closeAllEditionLists();
}, },
handleSuccess() { handleSuccess() {
@ -93,13 +73,12 @@ let PieceListBulkModal = React.createClass({
.forEach((pieceId) => { .forEach((pieceId) => {
EditionListActions.fetchEditionList(pieceId, this.state.orderBy, this.state.orderAsc); EditionListActions.fetchEditionList(pieceId, this.state.orderBy, this.state.orderAsc);
}); });
GlobalNotificationActions.updateGlobalNotification({message: 'Transfer successful'});
EditionListActions.clearAllEditionSelections(); EditionListActions.clearAllEditionSelections();
}, },
render() { render() {
let availableAcls = this.getAvailableAcls();
let selectedEditions = this.fetchSelectedEditionList(); let selectedEditions = this.fetchSelectedEditionList();
let availableAcls = getAvailableAcls(selectedEditions);
if(availableAcls.length > 0) { if(availableAcls.length > 0) {
return ( return (

View File

@ -20,7 +20,6 @@ let Table = React.createClass({
}, },
renderChildren() { renderChildren() {
var that = this;
return ReactAddons.Children.map(this.props.children, (child, i) => { return ReactAddons.Children.map(this.props.children, (child, i) => {
return ReactAddons.addons.cloneWithProps(child, { return ReactAddons.addons.cloneWithProps(child, {
columnList: this.props.columnList, columnList: this.props.columnList,

View File

@ -17,7 +17,9 @@ let TableItemCheckbox = React.createClass({
render() { render() {
return ( return (
<input type="checkbox" onChange={this.selectItem} checked={this.props.selected}/> <span>
<input type="checkbox" onChange={this.selectItem} checked={this.props.selected}/>
</span>
); );
} }
}); });

View File

@ -4,7 +4,6 @@ import React from 'react';
import Router from 'react-router'; import Router from 'react-router';
import { ColumnModel } from './models/table_models'; import { ColumnModel } from './models/table_models';
import TableColumnMixin from '../../mixins/table_column_mixin';
let Link = Router.Link; let Link = Router.Link;
@ -15,7 +14,7 @@ let TableItemWrapper = React.createClass({
columnWidth: React.PropTypes.number.isRequired columnWidth: React.PropTypes.number.isRequired
}, },
mixins: [TableColumnMixin, Router.Navigation], mixins: [Router.Navigation],
render() { render() {
return ( return (
@ -25,8 +24,6 @@ let TableItemWrapper = React.createClass({
let TypeElement = column.displayType; let TypeElement = column.displayType;
let typeElementProps = column.transformFn(this.props.columnContent); let typeElementProps = column.transformFn(this.props.columnContent);
let columnClass = this.calcColumnClasses(this.props.columnList, i, this.props.columnWidth);
if(!column.transition) { if(!column.transition) {
return ( return (
<td <td

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import MediaPlayer from './ascribe_media/media_player'; import MediaPlayer from './ascribe_media/media_player';
import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin'; import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin';
@ -8,11 +9,17 @@ import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col'; import Col from 'react-bootstrap/lib/Col';
import Button from 'react-bootstrap/lib/Button'; import Button from 'react-bootstrap/lib/Button';
import Glyphicon from 'react-bootstrap/lib/Glyphicon'; import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import TextareaAutosize from 'react-textarea-autosize';
import PersonalNoteForm from './ascribe_forms/form_note_personal';
import PieceExtraDataForm from './ascribe_forms/form_piece_extradata';
import RequestActionForm from './ascribe_forms/form_request_action';
import EditionActions from '../actions/edition_actions'; import EditionActions from '../actions/edition_actions';
import AclButtonList from './ascribe_buttons/acl_button_list'; import AclButtonList from './ascribe_buttons/acl_button_list';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import classNames from 'classnames'; import classNames from 'classnames';
/** /**
@ -21,9 +28,7 @@ import classNames from 'classnames';
let Edition = React.createClass({ let Edition = React.createClass({
propTypes: { propTypes: {
edition: React.PropTypes.object, edition: React.PropTypes.object,
currentUser: React.PropTypes.object, loadEdition: React.PropTypes.func
deleteEdition: React.PropTypes.func,
savePersonalNote: React.PropTypes.func
}, },
render() { render() {
@ -39,10 +44,14 @@ let Edition = React.createClass({
<a target="_blank" href={'https://www.blocktrail.com/BTC/address/' + this.props.edition.bitcoin_id}>{this.props.edition.bitcoin_id}</a> <a target="_blank" href={'https://www.blocktrail.com/BTC/address/' + this.props.edition.bitcoin_id}>{this.props.edition.bitcoin_id}</a>
); );
let hashOfArtwork = ( let hashOfArtwork = (
<a target="_blank" href={'https://www.blocktrail.com/BTC/address/' + this.props.edition.hash_as_address}>{this.props.edition.hash_as_address}</a> <a target="_blank" href={'https://www.blocktrail.com/BTC/address/' + this.props.edition.hash_as_address}>{this.props.edition.hash_as_address}</a>
); );
let ownerAddress = (
<a target="_blank" href={'https://www.blocktrail.com/BTC/address/' + this.props.edition.btc_owner_address_noprefix}>{this.props.edition.btc_owner_address_noprefix}</a>
);
return ( return (
<Row> <Row>
<Col md={6}> <Col md={6}>
@ -59,13 +68,19 @@ let Edition = React.createClass({
<Col md={6} className="ascribe-edition-details"> <Col md={6} className="ascribe-edition-details">
<EditionHeader edition={this.props.edition}/> <EditionHeader edition={this.props.edition}/>
<EditionSummary <EditionSummary
edition={this.props.edition} edition={this.props.edition} />
currentUser={ this.props.currentUser }/>
<CollapsibleEditionDetails <CollapsibleEditionDetails
title="Personal Note" title="Personal Note"
iconName="pencil"> iconName="pencil">
<EditionPersonalNote <EditionPersonalNote
savePersonalNote={this.props.savePersonalNote}/> handleSuccess={this.props.loadEdition}
edition={this.props.edition}/>
</CollapsibleEditionDetails>
<CollapsibleEditionDetails
title="Further Details">
<EditionFurtherDetails
handleSuccess={this.props.loadEdition}
edition={this.props.edition}/>
</CollapsibleEditionDetails> </CollapsibleEditionDetails>
<CollapsibleEditionDetails <CollapsibleEditionDetails
@ -75,6 +90,13 @@ let Edition = React.createClass({
history={this.props.edition.ownership_history} /> history={this.props.edition.ownership_history} />
</CollapsibleEditionDetails> </CollapsibleEditionDetails>
<CollapsibleEditionDetails
title="Consignment History"
show={this.props.edition.consign_history && this.props.edition.consign_history.length > 0}>
<EditionDetailHistoryIterator
history={this.props.edition.consign_history} />
</CollapsibleEditionDetails>
<CollapsibleEditionDetails <CollapsibleEditionDetails
title="Loan History" title="Loan History"
show={this.props.edition.loan_history && this.props.edition.loan_history.length > 0}> show={this.props.edition.loan_history && this.props.edition.loan_history.length > 0}>
@ -92,16 +114,7 @@ let Edition = React.createClass({
value={hashOfArtwork} /> value={hashOfArtwork} />
<EditionDetailProperty <EditionDetailProperty
label="Owned by SPOOL address" label="Owned by SPOOL address"
value="MISSING IN /editions/<id> RESOURCE!" /> value={ownerAddress} />
</CollapsibleEditionDetails>
<CollapsibleEditionDetails
title="Delete Actions">
<Button
bsStyle="danger"
onClick={this.props.deleteEdition}>
Remove this artwork from your list
</Button>
</CollapsibleEditionDetails> </CollapsibleEditionDetails>
</Col> </Col>
</Row> </Row>
@ -130,22 +143,31 @@ let EditionHeader = React.createClass({
let EditionSummary = React.createClass({ let EditionSummary = React.createClass({
propTypes: { propTypes: {
edition: React.PropTypes.object, edition: React.PropTypes.object
currentUser: React.PropTypes.object
}, },
handleSuccess(){ handleSuccess(){
EditionActions.fetchOne(this.props.edition.id); EditionActions.fetchOne(this.props.edition.id);
}, },
showNotification(response){
this.handleSuccess();
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() { render() {
return ( let status = null;
<div className="ascribe-detail-header"> if (this.props.edition.status.length > 0){
<EditionDetailProperty label="EDITION" status = <EditionDetailProperty label="STATUS" value={ this.props.edition.status.join().replace(/_/, ' ') } />;
value={this.props.edition.edition_number + ' of ' + this.props.edition.num_editions} /> }
<EditionDetailProperty label="ID" value={ this.props.edition.bitcoin_id } /> let actions = null;
<EditionDetailProperty label="OWNER" value={ this.props.edition.owner } /> if (this.props.edition.request_action){
<br/> actions = (
<RequestActionForm
editions={ [this.props.edition] }
handleSuccess={this.showNotification}/>);
}
else {
actions = (
<Row> <Row>
<Col md={12}> <Col md={12}>
<AclButtonList <AclButtonList
@ -154,7 +176,18 @@ let EditionSummary = React.createClass({
editions={[this.props.edition]} editions={[this.props.edition]}
handleSuccess={this.handleSuccess} /> handleSuccess={this.handleSuccess} />
</Col> </Col>
</Row> </Row>);
}
return (
<div className="ascribe-detail-header">
<EditionDetailProperty label="EDITION"
value={this.props.edition.edition_number + ' of ' + this.props.edition.num_editions} />
<EditionDetailProperty label="ID" value={ this.props.edition.bitcoin_id } />
<EditionDetailProperty label="OWNER" value={ this.props.edition.owner } />
{status}
<br/>
{actions}
<hr/> <hr/>
</div> </div>
); );
@ -257,8 +290,8 @@ let EditionDetailProperty = React.createClass({
getDefaultProps() { getDefaultProps() {
return { return {
separator: ':', separator: ':',
labelClassName: 'col-xs-5 col-sm-5 col-md-5 col-lg-5', labelClassName: 'col-xs-5 col-sm-4 col-md-3 col-lg-3',
valueClassName: 'col-xs-7 col-sm-7 col-md-7 col-lg-7' valueClassName: 'col-xs-7 col-sm-8 col-md-9 col-lg-9'
}; };
}, },
@ -267,7 +300,7 @@ let EditionDetailProperty = React.createClass({
<div className="row ascribe-detail-property"> <div className="row ascribe-detail-property">
<div className="row-same-height"> <div className="row-same-height">
<div className={this.props.labelClassName + ' col-xs-height col-bottom'}> <div className={this.props.labelClassName + ' col-xs-height col-bottom'}>
<div>{ this.props.label }{this.props.separator}</div> <div>{ this.props.label + this.props.separator}</div>
</div> </div>
<div className={this.props.valueClassName + ' col-xs-height col-bottom'}> <div className={this.props.valueClassName + ' col-xs-height col-bottom'}>
<div>{ this.props.value }</div> <div>{ this.props.value }</div>
@ -304,26 +337,21 @@ let EditionDetailHistoryIterator = React.createClass({
let EditionPersonalNote = React.createClass({ let EditionPersonalNote = React.createClass({
propTypes: { propTypes: {
savePersonalNote: React.PropTypes.func edition: React.PropTypes.object,
handleSuccess: React.PropTypes.func
}, },
showNotification(){
prepareSavePersonalNote() { this.props.handleSuccess();
let personalNote = React.findDOMNode(this.refs.personalNote).value; let notification = new GlobalNotificationModel('Note saved', 'success');
this.props.savePersonalNote(personalNote); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
render() { render() {
return ( return (
<Row> <Row>
<Col md={12} className="ascribe-edition-personal-note"> <Col md={12} className="ascribe-edition-personal-note">
<TextareaAutosize <PersonalNoteForm
ref="personalNote" handleSuccess={this.showNotification}
className="form-control" editions={[this.props.edition]} />
rows={3}
placeholder='Write something...' />
<Button
onClick={this.prepareSavePersonalNote}
className="pull-right">Save</Button>
</Col> </Col>
</Row> </Row>
); );
@ -331,4 +359,41 @@ let EditionPersonalNote = React.createClass({
}); });
let EditionFurtherDetails = React.createClass({
propTypes: {
edition: React.PropTypes.object,
handleSuccess: React.PropTypes.func
},
showNotification(){
this.props.handleSuccess();
let notification = new GlobalNotificationModel('Details updated', 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
},
render() {
return (
<Row>
<Col md={12} className="ascribe-edition-personal-note">
<PieceExtraDataForm
name='artist_contact_info'
title='Artist Contact Info'
handleSuccess={this.showNotification}
editions={[this.props.edition]} />
<PieceExtraDataForm
name='display_instructions'
title='Display Instructions'
handleSuccess={this.showNotification}
editions={[this.props.edition]} />
<PieceExtraDataForm
name='technology_details'
title='Technology Details'
handleSuccess={this.showNotification}
editions={[this.props.edition]} />
</Col>
</Row>
);
}
});
export default Edition; export default Edition;

View File

@ -2,12 +2,8 @@
import React from 'react'; import React from 'react';
import { mergeOptions } from '../utils/general_utils';
import EditionActions from '../actions/edition_actions'; import EditionActions from '../actions/edition_actions';
import EditionStore from '../stores/edition_store'; import EditionStore from '../stores/edition_store';
import UserActions from '../actions/user_actions';
import UserStore from '../stores/user_store';
import Edition from './edition'; import Edition from './edition';
@ -16,7 +12,7 @@ import Edition from './edition';
*/ */
let EditionContainer = React.createClass({ let EditionContainer = React.createClass({
getInitialState() { getInitialState() {
return mergeOptions(UserStore.getState(), EditionStore.getState()); return EditionStore.getState();
}, },
onChange(state) { onChange(state) {
@ -25,24 +21,16 @@ let EditionContainer = React.createClass({
componentDidMount() { componentDidMount() {
EditionStore.listen(this.onChange); EditionStore.listen(this.onChange);
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
EditionActions.fetchOne(this.props.params.editionId); EditionActions.fetchOne(this.props.params.editionId);
}, },
componentWillUnmount() { componentWillUnmount() {
EditionStore.unlisten(this.onChange); EditionStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
}, },
deleteEdition() {
// Delete Edition from server
},
savePersonalNote(note) { loadEdition() {
console.log(note); EditionActions.fetchOne(this.props.params.editionId);
// Save personalNote to server
}, },
render() { render() {
@ -50,9 +38,7 @@ let EditionContainer = React.createClass({
return ( return (
<Edition <Edition
edition={this.state.edition} edition={this.state.edition}
currentUser={this.state.currentUser} loadEdition={this.loadEdition}/>
deleteEdition={this.deleteEdition}
savePersonalNote={this.savePersonalNote}/>
); );
} else { } else {
return ( return (

View File

@ -38,7 +38,7 @@ let Header = React.createClass({
return ( return (
<Navbar> <Navbar>
<Nav> <Nav>
<Link className="navbar-brand" to="pieces"> <Link className="navbar-brand" to="pieces" path="/?page=1">
<span>ascribe </span> <span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span> <span className="glyph-ascribe-spool-chunked ascribe-color"></span>
</Link> </Link>

View File

@ -27,7 +27,10 @@ let PieceList = React.createClass({
componentDidMount() { componentDidMount() {
let page = this.props.query.page || 1; let page = this.props.query.page || 1;
PieceListStore.listen(this.onChange); PieceListStore.listen(this.onChange);
PieceListActions.fetchPieceList(page, this.state.pageSize, this.state.search, this.state.orderBy, this.state.orderAsc); if (this.state.pieceList.length === 0){
PieceListActions.fetchPieceList(page, this.state.pageSize, this.state.search, this.state.orderBy, this.state.orderAsc);
}
}, },
componentWillUnmount() { componentWillUnmount() {
@ -74,9 +77,7 @@ let PieceList = React.createClass({
key={i}> key={i}>
<AccordionListItemTableEditions <AccordionListItemTableEditions
className="ascribe-accordion-list-item-table col-xs-12 col-sm-8 col-md-6 col-lg-6 col-sm-offset-2 col-md-offset-3 col-lg-offset-3" className="ascribe-accordion-list-item-table col-xs-12 col-sm-8 col-md-6 col-lg-6 col-sm-offset-2 col-md-offset-3 col-lg-offset-3"
parentId={item.id} parentId={item.id} />
show={item.show}
numOfEditions={item.num_editions}/>
</AccordionListItem> </AccordionListItem>
); );
})} })}

View File

@ -3,17 +3,26 @@
import AppConstants from './application_constants'; import AppConstants from './application_constants';
let apiUrls = { let apiUrls = {
'ownership_shares_mail': AppConstants.apiEndpoint + 'ownership/shares/mail/',
'ownership_transfers': AppConstants.apiEndpoint + 'ownership/transfers/',
'user': AppConstants.apiEndpoint + 'users/', 'user': AppConstants.apiEndpoint + 'users/',
'pieces_list': AppConstants.apiEndpoint + 'pieces/',
'piece': AppConstants.apiEndpoint + 'pieces/${piece_id}', 'piece': AppConstants.apiEndpoint + 'pieces/${piece_id}',
'pieces_list': AppConstants.apiEndpoint + 'pieces/',
'piece_extradata': AppConstants.apiEndpoint + 'pieces/${piece_id}/extradata/',
'edition': AppConstants.apiEndpoint + 'editions/${bitcoin_id}/', 'edition': AppConstants.apiEndpoint + 'editions/${bitcoin_id}/',
'editions_list': AppConstants.apiEndpoint + 'pieces/${piece_id}/editions/', 'editions_list': AppConstants.apiEndpoint + 'pieces/${piece_id}/editions/',
'ownership_loans': AppConstants.apiEndpoint + 'ownership/loans/', 'edition_delete': AppConstants.apiEndpoint + 'editions/${edition_id}/',
'edition_remove_from_collection': AppConstants.apiEndpoint + 'ownership/shares/${edition_id}/',
'ownership_shares_mail': AppConstants.apiEndpoint + 'ownership/shares/mail/',
'ownership_transfers': AppConstants.apiEndpoint + 'ownership/transfers/',
'ownership_consigns': AppConstants.apiEndpoint + 'ownership/consigns/', 'ownership_consigns': AppConstants.apiEndpoint + 'ownership/consigns/',
'ownership_consigns_confirm': AppConstants.apiEndpoint + 'ownership/consigns/confirm/',
'ownership_consigns_deny': AppConstants.apiEndpoint + 'ownership/consigns/deny/',
'ownership_unconsigns': AppConstants.apiEndpoint + 'ownership/unconsigns/', 'ownership_unconsigns': AppConstants.apiEndpoint + 'ownership/unconsigns/',
'ownership_unconsigns_request': AppConstants.apiEndpoint + 'ownership/unconsigns/request/' 'ownership_unconsigns_request': AppConstants.apiEndpoint + 'ownership/unconsigns/request/',
'ownership_unconsigns_deny': AppConstants.apiEndpoint + 'ownership/unconsigns/deny/',
'ownership_loans': AppConstants.apiEndpoint + 'ownership/loans/',
'ownership_loans_confirm': AppConstants.apiEndpoint + 'ownership/loans/confirm/',
'ownership_loans_deny': AppConstants.apiEndpoint + 'ownership/loans/deny/',
'note_notes': AppConstants.apiEndpoint + 'note/notes/'
}; };
export default apiUrls; export default apiUrls;

View File

@ -8,7 +8,8 @@ let constants = {
'baseUrl': window.BASE_URL, 'baseUrl': window.BASE_URL,
'apiEndpoint': window.API_ENDPOINT, 'apiEndpoint': window.API_ENDPOINT,
'debugCredentialBase64': 'ZGltaUBtYWlsaW5hdG9yLmNvbTowMDAwMDAwMDAw', // dimi@mailinator:0000000000 'debugCredentialBase64': 'ZGltaUBtYWlsaW5hdG9yLmNvbTowMDAwMDAwMDAw', // dimi@mailinator:0000000000
'aclList': ['edit', 'consign', 'transfer', 'loan', 'share', 'download', 'view', 'delete', 'del_from_collection', 'add_to_collection'] 'aclList': ['edit', 'consign', 'consign_request', 'unconsign', 'unconsign_request', 'transfer',
'loan', 'loan_request', 'share', 'download', 'view', 'delete', 'del_from_collection', 'add_to_collection']
}; };
export default constants; export default constants;

View File

@ -6,6 +6,11 @@ import React from 'react';
import AlertDismissable from '../components/ascribe_forms/alert'; import AlertDismissable from '../components/ascribe_forms/alert';
export const FormMixin = { export const FormMixin = {
propTypes: {
editions: React.PropTypes.array,
currentUser: React.PropTypes.object
},
getInitialState() { getInitialState() {
return { return {
submitted: false, submitted: false,
@ -14,22 +19,44 @@ export const FormMixin = {
}, },
submit(e) { submit(e) {
e.preventDefault(); if (e) {
e.preventDefault();
}
this.setState({submitted: true}); this.setState({submitted: true});
this.clearErrors(); this.clearErrors();
let action = (this.httpVerb && this.httpVerb()) || 'post';
this[action](e);
},
post(e){
fetch fetch
.post(this.url(), { body: this.getFormData() }) .post(this.url(e), { body: this.getFormData() })
.then(() => this.props.handleSuccess()) .then(this.handleSuccess)
.catch(this.handleError);
},
delete(e){
fetch
.delete(this.url(e))
.then(this.handleSuccess)
.catch(this.handleError); .catch(this.handleError);
}, },
clearErrors(){ clearErrors(){
for (var ref in this.refs){ for (var ref in this.refs){
this.refs[ref].clearAlerts(); if ('clearAlerts' in this.refs[ref]){
this.refs[ref].clearAlerts();
}
} }
this.setState({errors: []}); this.setState({errors: []});
}, },
handleSuccess(response){
if ('handleSuccess' in this.props){
this.props.handleSuccess(response);
}
},
handleError(err){ handleError(err){
if (err.json) { if (err.json) {
for (var input in err.json.errors){ for (var input in err.json.errors){

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
export default class GlobalNotificationModel { export default class GlobalNotificationModel {
constructor(message, type = 'info', dismissAfter = 3500) { constructor(message, type = 'info', dismissAfter = 5000) {
if(message) { if(message) {
this.message = message; this.message = message;
} else { } else {

View File

@ -8,6 +8,7 @@ import EditionsListActions from '../actions/edition_list_actions';
class EditionListStore { class EditionListStore {
constructor() { constructor() {
this.editionList = {}; this.editionList = {};
this.isEditionListOpenForPieceId = {};
this.bindActions(EditionsListActions); this.bindActions(EditionsListActions);
} }
@ -16,13 +17,16 @@ class EditionListStore {
this.editionList[pieceId].forEach((edition, i) => { this.editionList[pieceId].forEach((edition, i) => {
// This uses the index of the new editionList for determining the edition. // This uses the index of the new editionList for determining the edition.
// If the list of editions can be sorted in the future, this needs to be changed! // If the list of editions can be sorted in the future, this needs to be changed!
editionListOfPiece[i] = React.addons.update(edition, {$merge: editionListOfPiece[i]}); if (editionListOfPiece[i]) {
editionListOfPiece[i] = React.addons.update(edition, {$merge: editionListOfPiece[i]});
}
}); });
} }
this.editionList[pieceId] = editionListOfPiece; this.editionList[pieceId] = editionListOfPiece;
/** /**
* orderBy and orderAsc are specific to a single list of editons * orderBy and orderAsc are specific to a single list of editions
* therefore they need to be saved in relation to their parent-piece. * therefore they need to be saved in relation to their parent-piece.
* *
* Default values for both are set in the editon_list-actions. * Default values for both are set in the editon_list-actions.
@ -34,7 +38,7 @@ class EditionListStore {
onSelectEdition({pieceId, editionId, toValue}) { onSelectEdition({pieceId, editionId, toValue}) {
this.editionList[pieceId].forEach((edition) => { this.editionList[pieceId].forEach((edition) => {
// http://stackoverflow.com/a/519157/1263876 // Taken from: http://stackoverflow.com/a/519157/1263876
if(typeof toValue !== 'undefined' && edition.id === editionId) { if(typeof toValue !== 'undefined' && edition.id === editionId) {
edition.selected = toValue; edition.selected = toValue;
} else if(edition.id === editionId) { } else if(edition.id === editionId) {
@ -55,12 +59,20 @@ class EditionListStore {
.forEach((edition) => { .forEach((edition) => {
try { try {
delete edition.selected; delete edition.selected;
} catch(err) { } catch(err) {/* ignore and keep going */}
//just ignore
}
}); });
}); });
} }
onToggleEditionList(pieceId) {
this.isEditionListOpenForPieceId[pieceId] = {
show: this.isEditionListOpenForPieceId[pieceId] ? !this.isEditionListOpenForPieceId[pieceId].show : true
};
}
onCloseAllEditionLists() {
this.isEditionListOpenForPieceId = {};
}
} }
export default alt.createStore(EditionListStore, 'EditionListStore'); export default alt.createStore(EditionListStore, 'EditionListStore');

View File

@ -1,5 +1,6 @@
'use strict'; 'use strict';
import React from 'react';
import alt from '../alt'; import alt from '../alt';
import PieceListActions from '../actions/piece_list_actions'; import PieceListActions from '../actions/piece_list_actions';
@ -27,7 +28,7 @@ class PieceListStore {
this.bindActions(PieceListActions); this.bindActions(PieceListActions);
} }
onShowEditionList(pieceId) { /*onShowEditionList(pieceId) {
this.pieceList this.pieceList
.forEach((piece) => { .forEach((piece) => {
if(piece.id === pieceId) { if(piece.id === pieceId) {
@ -38,14 +39,14 @@ class PieceListStore {
} }
} }
}); });
} }*/
onCloseAllEditionLists() { /*onCloseAllEditionLists() {
this.pieceList this.pieceList
.forEach((piece) => { .forEach((piece) => {
piece.show = false; piece.show = false;
}); });
} }*/
onUpdatePieceList({ page, pageSize, search, pieceList, orderBy, orderAsc, pieceListCount }) { onUpdatePieceList({ page, pageSize, search, pieceList, orderBy, orderAsc, pieceListCount }) {
this.page = page; this.page = page;
@ -72,6 +73,16 @@ class PieceListStore {
* We did not implement this, as we're going to add pagination to pieceList at some * We did not implement this, as we're going to add pagination to pieceList at some
* point anyway. Then, this problem is automatically resolved. * point anyway. Then, this problem is automatically resolved.
*/ */
pieceList.forEach((piece, i) => {
let oldPiece = this.pieceList[i];
if(oldPiece) {
piece = React.addons.update(piece, {
show: { $set: oldPiece.show }
});
}
});
this.pieceList = pieceList; this.pieceList = pieceList;
} }
} }

23
js/utils/acl_utils.js Normal file
View File

@ -0,0 +1,23 @@
'use strict';
export function getAvailableAcls(editions) {
let availableAcls = [];
// If no edition has been selected, availableActions is empty
// If only one edition has been selected, their actions are available
// If more than one editions have been selected, their acl properties are intersected
if(editions.length >= 1) {
availableAcls = editions[0].acl;
}
if(editions.length >= 2) {
for(let i = 1; i < editions.length; i++) {
availableAcls = intersectAcls(availableAcls, editions[i].acl);
}
}
return availableAcls;
}
export function intersectAcls(a, b) {
return a.filter((val) => b.indexOf(val) > -1);
}

View File

@ -88,19 +88,25 @@ class Fetch {
get(url, params) { get(url, params) {
let paramsCopy = this._merge(params); let paramsCopy = this._merge(params);
let newUrl = this.prepareUrl(url, params, true); let newUrl = this.prepareUrl(url, paramsCopy, true);
return this.request('get', newUrl); return this.request('get', newUrl);
} }
delete(url, params) {
let paramsCopy = this._merge(params);
let newUrl = this.prepareUrl(url, paramsCopy, true);
return this.request('delete', newUrl);
}
post(url, params) { post(url, params) {
let paramsCopy = this._merge(params); let paramsCopy = this._merge(params);
let newUrl = this.prepareUrl(url, params); let newUrl = this.prepareUrl(url, paramsCopy);
let body = null; let body = null;
if (params.body) { if (paramsCopy && paramsCopy.body) {
body = JSON.stringify(params.body); body = JSON.stringify(paramsCopy.body);
} }
return this.request('post', url, { body }); return this.request('post', newUrl, { body });
} }
defaults(options) { defaults(options) {

44
sass/ascribe_table.scss Normal file
View File

@ -0,0 +1,44 @@
.ascribe-table {
margin-bottom:0;
}
/*This is aligning the first checkbox in pieclist detail with all the other ones*/
.table > thead:first-child > tr:first-child > th {
padding-left:0;
}
.ascribe-table-header-column > span {
display: table-cell;
vertical-align: middle;
font-family: 'Source Sans Pro';
font-weight: 600;
color: #424242;
}
.ascribe-table-item-column {
display: table;
font-family: 'Source Sans Pro';
font-size: .8em;
height:3em;
}
.ascribe-table-item-column > * {
display: table-cell;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ascribe-table-item-column > span > input {
margin-top:14px;
}
.ascribe-table-item-selected {
background-color: rgba(2, 182, 163, 0.5);
}
.ascribe-table-item-selectable {
cursor: default;
}

View File

@ -0,0 +1,21 @@
.ascribe-textarea {
border: none;
box-shadow: none;
margin-bottom: 1em;
}
.ascribe-textarea-editable:hover {
border: 1px solid #AAA;
}
.ascribe-pre{
word-break: break-word;
/* white-space: pre-wrap; */
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
/* word-wrap: break-word; */
font-family: inherit;
text-align: justify;
background-color: white;
}

View File

@ -9,10 +9,12 @@ $BASE_URL: '<%= BASE_URL %>';
@import '../node_modules/react-datepicker/dist/react-datepicker'; @import '../node_modules/react-datepicker/dist/react-datepicker';
@import './ascribe-fonts/style'; @import './ascribe-fonts/style';
@import './ascribe-fonts/ascribe-fonts'; @import './ascribe-fonts/ascribe-fonts';
@import 'ascribe_table';
@import 'ascribe_accordion_list'; @import 'ascribe_accordion_list';
@import 'ascribe_piece_list_bulk_modal'; @import 'ascribe_piece_list_bulk_modal';
@import 'ascribe_piece_list_toolbar'; @import 'ascribe_piece_list_toolbar';
@import 'ascribe_edition'; @import 'ascribe_edition';
@import 'ascribe_textarea';
@import 'ascribe_media_player'; @import 'ascribe_media_player';
@import 'ascribe-global-notification'; @import 'ascribe-global-notification';
@import 'offset_right'; @import 'offset_right';
@ -28,6 +30,10 @@ $BASE_URL: '<%= BASE_URL %>';
border-top:0; border-top:0;
} }
.navbar-right {
margin-right: 0;
}
.clear-paddings { .clear-paddings {
padding-left:0; padding-left:0;
padding-right:0; padding-right:0;
@ -44,45 +50,6 @@ $BASE_URL: '<%= BASE_URL %>';
float: none; float: none;
} }
.ascribe-table {
margin-bottom:0;
}
.ascribe-table-header-column > span {
display: table-cell;
vertical-align: middle;
font-family: 'Source Sans Pro';
font-weight: 600;
color: #424242;
}
.ascribe-table-header-column > span > .glyphicon {
font-size: .5em;
}
.ascribe-table-item-column {
display: table;
font-family: 'Source Sans Pro';
font-size: .8em;
height:3em;
}
.ascribe-table-item-column > * {
display: table-cell;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ascribe-table-item-selected {
background-color: rgba(2, 182, 163, 0.5);
}
.ascribe-table-item-selectable {
cursor: default;
}
.no-margin { .no-margin {
margin-right: 0; margin-right: 0;
margin-left: 0; margin-left: 0;
@ -95,7 +62,6 @@ $BASE_URL: '<%= BASE_URL %>';
margin-left: 0 !important; margin-left: 0 !important;
font-family: sans-serif !important; font-family: sans-serif !important;
border-radius: 0 !important; border-radius: 0 !important;
} }
.btn-ascribe, .btn-ascribe-inv:active, .btn-ascribe-inv:hover { .btn-ascribe, .btn-ascribe-inv:active, .btn-ascribe-inv:hover {
@ -125,7 +91,6 @@ $BASE_URL: '<%= BASE_URL %>';
margin-left: 0 !important; margin-left: 0 !important;
font-family: sans-serif !important; font-family: sans-serif !important;
border-radius: 0 !important; border-radius: 0 !important;
} }
.btn-ascribe-green, .btn-ascribe-green-inv:active, .btn-ascribe-green-inv:hover { .btn-ascribe-green, .btn-ascribe-green-inv:active, .btn-ascribe-green-inv:hover {
@ -144,7 +109,6 @@ $BASE_URL: '<%= BASE_URL %>';
margin-top: 2em; margin-top: 2em;
} }
.ascribe-detail-title { .ascribe-detail-title {
font-size: 2em; font-size: 2em;
margin-bottom: -0.2em; margin-bottom: -0.2em;