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

Merged in AD-419-decouple-piece-registration-from- (pull request #8)

Ad 419 decouple piece registration from
This commit is contained in:
diminator 2015-07-14 17:50:35 +02:00
commit a86ea00c7e
58 changed files with 962 additions and 410 deletions

View File

@ -184,8 +184,8 @@ function bundle(watch) {
.on('error', notify.onError('Error: <%= error.message %>')) .on('error', notify.onError('Error: <%= error.message %>'))
.pipe(gulpif(!argv.production, sourcemaps.write())) // writes .map file .pipe(gulpif(!argv.production, sourcemaps.write())) // writes .map file
.on('error', notify.onError('Error: <%= error.message %>')) .on('error', notify.onError('Error: <%= error.message %>'))
//.pipe(gulpif(argv.production, uglify())) .pipe(gulpif(argv.production, uglify()))
//.on('error', notify.onError('Error: <%= error.message %>')) .on('error', notify.onError('Error: <%= error.message %>'))
.pipe(gulp.dest('./build/js')) .pipe(gulp.dest('./build/js'))
.on('error', notify.onError('Error: <%= error.message %>')) .on('error', notify.onError('Error: <%= error.message %>'))
.pipe(browserSync.stream()) .pipe(browserSync.stream())

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="images/hq-favicons/mstile-70x70.png"/>
<square150x150logo src="images/hq-favicons/mstile-150x150.png"/>
<square310x310logo src="images/hq-favicons/mstile-310x310.png"/>
<wide310x150logo src="images/hq-favicons/mstile-310x150.png"/>
<TileColor>#00aba9</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
img/hq-favicons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,35 @@
{
"name": "Ascribe",
"icons": [
{
"src": "images\/hq-favicons\/android-chrome-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "images\/hq-favicons\/android-chrome-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "images\/hq-favicons\/android-chrome-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "images\/hq-favicons\/android-chrome-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "images\/hq-favicons\/android-chrome-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -2,6 +2,24 @@
<html> <html>
<head> <head>
<link rel="apple-touch-icon" sizes="57x57" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-152x152.png">
<link rel="icon" type="image/png" href="<%= BASE_URL %>static/img/hq-favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="<%= BASE_URL %>static/img/hq-favicons/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="<%= BASE_URL %>static/img/hq-favicons/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="<%= BASE_URL %>static/img/hq-favicons/manifest.json">
<link rel="shortcut icon" href="<%= BASE_URL %>static/img/hq-favicons/favicon.ico">
<meta name="msapplication-TileColor" content="#00aba9">
<meta name="msapplication-TileImage" content="<%= BASE_URL %>static/img/hq-favicons/mstile-144x144.png">
<meta name="msapplication-config" content="<%= BASE_URL %>static/img/hq-favicons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>ascribe</title> <title>ascribe</title>

View File

@ -8,6 +8,7 @@ class EditionListActions {
constructor() { constructor() {
this.generateActions( this.generateActions(
'updateEditionList', 'updateEditionList',
'refreshEditionList',
'selectEdition', 'selectEdition',
'clearAllEditionSelections', 'clearAllEditionSelections',
'closeAllEditionLists', 'closeAllEditionLists',
@ -16,7 +17,7 @@ class EditionListActions {
} }
fetchEditionList(pieceId, page, pageSize, orderBy, orderAsc) { fetchEditionList(pieceId, page, pageSize, orderBy, orderAsc) {
if(!orderBy && typeof orderAsc == 'undefined') { if(!orderBy && typeof orderAsc === 'undefined') {
orderBy = 'edition_number'; orderBy = 'edition_number';
orderAsc = true; orderAsc = true;
} }

View File

@ -0,0 +1,38 @@
'use strict';
import React from 'react';
/**
* This component can easily be used to present another component conditionally
* - dependent on their acl.
*
* In order to do that, just wrap AclProxy around the component, add aclObject and
* the acl name you're looking for.
*/
let AclProxy = React.createClass({
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]).isRequired,
aclObject: React.PropTypes.object.isRequired,
aclName: React.PropTypes.string.isRequired
},
render() {
if(this.props.aclObject[this.props.aclName]) {
return (
<span>
{this.props.children}
</span>
);
} else {
if(typeof this.props.aclObject[this.props.aclName] === 'undefined') {
console.warn('The aclName you\'re filtering for was not present (undefined) in the aclObject.');
}
return null;
}
}
});
export default AclProxy;

View File

@ -13,6 +13,9 @@ import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import PieceListActions from '../../actions/piece_list_actions'; import PieceListActions from '../../actions/piece_list_actions';
import EditionListActions from '../../actions/edition_list_actions'; import EditionListActions from '../../actions/edition_list_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link; let Link = Router.Link;
@ -63,6 +66,9 @@ let AccordionListItem = React.createClass({
}); });
EditionListActions.toggleEditionList(pieceId); EditionListActions.toggleEditionList(pieceId);
let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
}, },
getCreateEditionsDialog() { getCreateEditionsDialog() {

View File

@ -162,11 +162,9 @@ let AccordionListItemTableEditions = React.createClass({
new ColumnModel( new ColumnModel(
(item) => { (item) => {
let content = item.acl; let content = item.acl;
if (item.request_action) {
content = [item.request_action + ' request'];
}
return { return {
'content': content 'content': content,
'requestAction': item.request_action
}; }, }; },
'acl', 'acl',
getLangText('Actions'), getLangText('Actions'),

View File

@ -38,7 +38,12 @@ let AclButton = React.createClass({
return { return {
title: getLangText('Consign artwork'), title: getLangText('Consign artwork'),
tooltip: getLangText('Have someone else sell the artwork'), tooltip: getLangText('Have someone else sell the artwork'),
form: <ConsignForm currentUser={ this.props.currentUser } editions={ this.props.pieceOrEditions }/>, form: (
<ConsignForm
message={this.getConsignMessage()}
id={this.getFormDataId()}
url={apiUrls.ownership_consigns}/>
),
handleSuccess: this.showNotification handleSuccess: this.showNotification
}; };
} }
@ -46,14 +51,24 @@ let AclButton = React.createClass({
return { return {
title: getLangText('Unconsign artwork'), title: getLangText('Unconsign artwork'),
tooltip: getLangText('Have the owner manage his sales again'), tooltip: getLangText('Have the owner manage his sales again'),
form: <UnConsignForm currentUser={ this.props.currentUser } editions={ this.props.pieceOrEditions }/>, form: (
<UnConsignForm
message={this.getUnConsignMessage()}
id={this.getFormDataId()}
url={apiUrls.ownership_unconsigns}/>
),
handleSuccess: this.showNotification handleSuccess: this.showNotification
}; };
}else if (this.props.action === 'acl_transfer') { }else if (this.props.action === 'acl_transfer') {
return { return {
title: getLangText('Transfer artwork'), title: getLangText('Transfer artwork'),
tooltip: getLangText('Transfer the ownership of the artwork'), tooltip: getLangText('Transfer the ownership of the artwork'),
form: <TransferForm currentUser={ this.props.currentUser } editions={ this.props.pieceOrEditions }/>, form: (
<TransferForm
message={this.getTransferMessage()}
id={this.getFormDataId()}
url={apiUrls.ownership_transfers}/>
),
handleSuccess: this.showNotification handleSuccess: this.showNotification
}; };
} }
@ -112,11 +127,52 @@ let AclButton = React.createClass({
} }
}, },
// plz move to transfer form
getTransferMessage(){
return (
`${getLangText('Hi')},
${getLangText('I transfer ownership of')}:
${this.getTitlesString()} ${getLangText('to you')}.
${getLangText('Truly yours')},
${this.props.currentUser.username}
`
);
},
// plz move to consign form
getConsignMessage(){
return (
`${getLangText('Hi')},
${getLangText('I consign')}:
${this.getTitlesString()} ${getLangText('to you')}.
${getLangText('Truly yours')},
${this.props.currentUser.username}
`
);
},
// plz move to consign form
getUnConsignMessage(){
return (
`${getLangText('Hi')},
${getLangText('I un-consign')}:
${this.getTitlesString()} ${getLangText('from you')}.
${getLangText('Truly yours')},
${this.props.currentUser.username}
`
);
},
// plz move to share form // plz move to share form
getShareMessage(){ getShareMessage(){
return ( return (
` `${getLangText('Hi')},
${getLangText('Hi')},
${getLangText('I am sharing')}: ${getLangText('I am sharing')}:
${this.getTitlesString()} ${getLangText('with you')}. ${this.getTitlesString()} ${getLangText('with you')}.

View File

@ -6,7 +6,6 @@ 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: {
@ -73,8 +72,6 @@ let AclButtonList = React.createClass({
pieceOrEditions={this.props.editions} pieceOrEditions={this.props.editions}
currentUser={this.state.currentUser} currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} /> handleSuccess={this.props.handleSuccess} />
<DeleteButton
editions={this.props.editions}/>
{this.props.children} {this.props.children}
</div> </div>
); );

View File

@ -6,59 +6,71 @@ import Router from 'react-router';
import Button from 'react-bootstrap/lib/Button'; import Button from 'react-bootstrap/lib/Button';
import EditionDeleteForm from '../ascribe_forms/form_delete_edition'; import EditionDeleteForm from '../ascribe_forms/form_delete_edition';
import EditionRemoveFromCollectionForm from '../ascribe_forms/form_remove_editions_from_collection'; import PieceDeleteForm from '../ascribe_forms/form_delete_piece';
import ModalWrapper from '../ascribe_modal/modal_wrapper';
import GlobalNotificationModel from '../../models/global_notification_model'; import EditionRemoveFromCollectionForm from '../ascribe_forms/form_remove_editions_from_collection';
import GlobalNotificationActions from '../../actions/global_notification_actions'; import PieceRemoveFromCollectionForm from '../ascribe_forms/form_remove_piece_from_collection';
import ModalWrapper from '../ascribe_modal/modal_wrapper';
import { getAvailableAcls } from '../../utils/acl_utils'; import { getAvailableAcls } from '../../utils/acl_utils';
import { getLangText } from '../../utils/lang_utils.js'; import { getLangText } from '../../utils/lang_utils.js';
import EditionListActions from '../../actions/edition_list_actions';
let DeleteButton = React.createClass({ let DeleteButton = React.createClass({
propTypes: { propTypes: {
editions: React.PropTypes.array.isRequired editions: React.PropTypes.array,
piece: React.PropTypes.object,
handleSuccess: React.PropTypes.func
}, },
mixins: [Router.Navigation], 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 () { render: function () {
let availableAcls = getAvailableAcls(this.props.editions); let availableAcls;
let btnDelete = null; let btnDelete;
let content = null; let content;
let title;
if (availableAcls.acl_delete) { if(this.props.piece && !this.props.editions) {
content = <EditionDeleteForm editions={ this.props.editions }/>; availableAcls = getAvailableAcls([this.props.piece]);
btnDelete = <Button bsStyle="danger" className="btn-delete" bsSize="small">{getLangText('DELETE')}</Button>; } else {
availableAcls = getAvailableAcls(this.props.editions);
} }
else if (availableAcls.acl_unshare || (this.props.editions.constructor !== Array && this.props.editions.acl.acl_unshare)){
content = <EditionRemoveFromCollectionForm editions={ this.props.editions }/>; if(availableAcls.acl_delete) {
if(this.props.piece && !this.props.editions) {
content = <PieceDeleteForm pieceId={this.props.piece.id}/>;
title = getLangText('Remove Piece');
} else {
content = <EditionDeleteForm editions={this.props.editions}/>;
title = getLangText('Remove Edition');
}
btnDelete = <Button bsStyle="danger" className="btn-delete" bsSize="small">{getLangText('DELETE')}</Button>;
} else if(availableAcls.acl_unshare){
if(this.props.editions && this.props.editions.constructor !== Array && this.props.editions.acl.acl_unshare) {
content = <EditionRemoveFromCollectionForm editions={this.props.editions}/>;
title = getLangText('Remove Edition from Collection');
} else {
content = <PieceRemoveFromCollectionForm pieceId={this.props.piece.id}/>;
title = getLangText('Remove Piece from Collection');
}
btnDelete = <Button bsStyle="danger" className="btn-delete" bsSize="small">{getLangText('REMOVE FROM COLLECTION')}</Button>; btnDelete = <Button bsStyle="danger" className="btn-delete" bsSize="small">{getLangText('REMOVE FROM COLLECTION')}</Button>;
} }
else{ else {
return null; return null;
} }
return ( return (
<ModalWrapper <ModalWrapper
button={ btnDelete } button={btnDelete}
handleSuccess={ this.showNotification } handleSuccess={this.props.handleSuccess}
title={getLangText('Remove Edition')} title={title}>
tooltip={getLangText('Click to remove edition')}> {content}
{ content }
</ModalWrapper> </ModalWrapper>
); );
} }

View File

@ -0,0 +1,47 @@
'use strict';
import React from 'react';
import Button from 'react-bootstrap/lib/Button';
import ModalWrapper from '../ascribe_modal/modal_wrapper';
import UnConsignRequestForm from './../ascribe_forms/form_unconsign_request';
import { getLangText } from '../../utils/lang_utils.js';
import apiUrls from '../../constants/api_urls';
let UnConsignRequestButton = React.createClass({
propTypes: {
currentUser: React.PropTypes.object.isRequired,
edition: React.PropTypes.object.isRequired,
handleSuccess: React.PropTypes.func.isRequired
},
render: function () {
return (
<ModalWrapper
button={
<Button bsStyle="danger" className="btn-delete pull-center" bsSize="small" type="submit">
REQUEST UNCONSIGN
</Button>
}
handleSuccess={this.props.handleSuccess}
title='Request to Un-Consign'
tooltip='Ask the consignee to return the ownership of the work back to you'>
<UnConsignRequestForm
url={apiUrls.ownership_unconsigns_request}
id={{'bitcoin_id': this.props.edition.bitcoin_id}}
message={`${getLangText('Hi')},
${getLangText('I request you to un-consign')} \" ${this.props.edition.title} \".
${getLangText('Truly yours')},
${this.props.currentUser.username}`}/>
</ModalWrapper>
);
}
});
export default UnConsignRequestButton;

View File

@ -6,11 +6,16 @@ import Router from 'react-router';
import Row from 'react-bootstrap/lib/Row'; import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col'; import Col from 'react-bootstrap/lib/Col';
import Glyphicon from 'react-bootstrap/lib/Glyphicon'; import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import Button from 'react-bootstrap/lib/Button';
import UserActions from '../../actions/user_actions'; import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store'; import UserStore from '../../stores/user_store';
import CoaActions from '../../actions/coa_actions'; import CoaActions from '../../actions/coa_actions';
import CoaStore from '../../stores/coa_store'; import CoaStore from '../../stores/coa_store';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import EditionListActions from '../../actions/edition_list_actions';
import MediaContainer from './media_container'; import MediaContainer from './media_container';
@ -23,12 +28,11 @@ import InputTextAreaToggable from './../ascribe_forms/input_textarea_toggable';
import EditionFurtherDetails from './further_details'; import EditionFurtherDetails from './further_details';
//import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata';
import RequestActionForm from './../ascribe_forms/form_request_action'; 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 UnConsignRequestButton from './../ascribe_buttons/unconsign_request_button';
//import ReactS3FineUploader from './../ascribe_uploader/react_s3_fine_uploader'; import DeleteButton from '../ascribe_buttons/delete_button';
import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions'; import GlobalNotificationActions from '../../actions/global_notification_actions';
@ -37,6 +41,7 @@ import apiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
let Link = Router.Link; let Link = Router.Link;
/** /**
@ -48,23 +53,44 @@ let Edition = React.createClass({
loadEdition: React.PropTypes.func loadEdition: React.PropTypes.func
}, },
mixins: [Router.Navigation],
getInitialState() { getInitialState() {
return UserStore.getState(); return mergeOptions(
UserStore.getState(),
PieceListStore.getState()
);
}, },
componentDidMount() { componentDidMount() {
UserStore.listen(this.onChange); UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
UserActions.fetchCurrentUser(); UserActions.fetchCurrentUser();
}, },
componentWillUnmount() { componentWillUnmount() {
UserStore.unlisten(this.onChange); UserStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
this.setState(state); this.setState(state);
}, },
handleDeleteSuccess(response) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, this.state.orderBy, this.state.orderAsc);
// we don't need to refresh the edition list for a piece here, since its reloaded from
// scatch once you click on show-editions anyway
EditionListActions.closeAllEditionLists();
EditionListActions.clearAllEditionSelections();
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
},
render() { render() {
return ( return (
<Row> <Row>
@ -80,12 +106,14 @@ let Edition = React.createClass({
<hr/> <hr/>
</div> </div>
<EditionSummary <EditionSummary
handleSuccess={this.props.loadEdition}
currentUser={this.state.currentUser} currentUser={this.state.currentUser}
edition={this.props.edition} /> edition={this.props.edition}
handleDeleteSuccess={this.handleDeleteSuccess}/>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Certificate of Authenticity')} title={getLangText('Certificate of Authenticity')}
show={this.props.edition.acl.acl_coa}> show={this.props.edition.acl.acl_coa === true}>
<CoaDetails <CoaDetails
edition={this.props.edition}/> edition={this.props.edition}/>
</CollapsibleParagraph> </CollapsibleParagraph>
@ -151,19 +179,21 @@ let Edition = React.createClass({
let EditionSummary = React.createClass({ let EditionSummary = React.createClass({
propTypes: { propTypes: {
edition: React.PropTypes.object edition: React.PropTypes.object,
handleSuccess: React.PropTypes.func,
currentUser: React.PropTypes.object,
handleDeleteSuccess: React.PropTypes.func
}, },
getTransferWithdrawData(){ getTransferWithdrawData(){
return {'bitcoin_id': this.props.edition.bitcoin_id}; return {'bitcoin_id': this.props.edition.bitcoin_id};
}, },
handleSuccess(){
EditionActions.fetchOne(this.props.edition.id);
},
showNotification(response){ showNotification(response){
this.handleSuccess(); this.props.handleSuccess();
let notification = new GlobalNotificationModel(response.notification, 'success'); if (response){
GlobalNotificationActions.appendGlobalNotification(notification); let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
}
}, },
getStatus(){ getStatus(){
let status = null; let status = null;
@ -172,32 +202,47 @@ let EditionSummary = React.createClass({
status = <EditionDetailProperty label="STATUS" value={ statusStr }/>; status = <EditionDetailProperty label="STATUS" value={ statusStr }/>;
if (this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer){ if (this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer){
status = ( status = (
<Form <EditionDetailProperty label="STATUS" value={ statusStr } />
url={apiUrls.ownership_transfers_withdraw}
getFormData={this.getTransferWithdrawData}
handleSuccess={this.showNotification}>
<EditionDetailProperty label="STATUS" value={ statusStr }>
<button
type="submit"
className="pull-right btn btn-default btn-sm">
WITHDRAW
</button>
</EditionDetailProperty>
</Form>
); );
} }
} }
return status; return status;
}, },
getActions(){ getActions(){
let actions = null; let actions = null;
if (this.props.edition.request_action && this.props.edition.request_action.length > 0){ if (this.props.edition.request_action && this.props.edition.request_action.length > 0){
actions = ( actions = (
<RequestActionForm <RequestActionForm
currentUser={this.props.currentUser}
editions={ [this.props.edition] } editions={ [this.props.edition] }
handleSuccess={this.showNotification}/>); handleSuccess={this.showNotification}/>);
} }
else { else {
let withdrawButton = null;
if (this.props.edition.status.length > 0 && this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer) {
withdrawButton = (
<Form
url={apiUrls.ownership_transfers_withdraw}
getFormData={this.getTransferWithdrawData}
handleSuccess={this.showNotification}
className='inline'>
<Button bsStyle="danger" className="btn-delete pull-center" bsSize="small" type="submit">
WITHDRAW TRANSFER
</Button>
</Form>
);
}
let unconsignRequestButton = null;
if (this.props.edition.acl.acl_request_unconsign) {
unconsignRequestButton = (
<UnConsignRequestButton
currentUser={this.props.currentUser}
edition={this.props.edition}
handleSuccess={this.props.handleSuccess} />
);
}
actions = ( actions = (
<Row> <Row>
<Col md={12}> <Col md={12}>
@ -205,7 +250,13 @@ let EditionSummary = React.createClass({
className="text-center ascribe-button-list" className="text-center ascribe-button-list"
availableAcls={this.props.edition.acl} availableAcls={this.props.edition.acl}
editions={[this.props.edition]} editions={[this.props.edition]}
handleSuccess={this.handleSuccess} /> handleSuccess={this.props.handleSuccess}>
{withdrawButton}
<DeleteButton
handleSuccess={this.props.handleDeleteSuccess}
editions={[this.props.edition]}/>
{unconsignRequestButton}
</AclButtonList>
</Col> </Col>
</Row>); </Row>);
} }

View File

@ -9,6 +9,8 @@ import MediaPlayer from './../ascribe_media/media_player';
import CollapsibleButton from './../ascribe_collapsible/collapsible_button'; import CollapsibleButton from './../ascribe_collapsible/collapsible_button';
import AclProxy from '../acl_proxy';
let MediaContainer = React.createClass({ let MediaContainer = React.createClass({
propTypes: { propTypes: {
@ -49,9 +51,13 @@ let MediaContainer = React.createClass({
url={this.props.content.digital_work.url} url={this.props.content.digital_work.url}
extraData={extraData} /> extraData={extraData} />
<p className="text-center"> <p className="text-center">
<Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.content.digital_work.url} target="_blank"> <AclProxy
Download <Glyphicon glyph="cloud-download"/> aclObject={this.props.content.acl}
</Button> aclName="acl_download">
<Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.content.digital_work.url} target="_blank">
Download <Glyphicon glyph="cloud-download"/>
</Button>
</AclProxy>
{embed} {embed}
</p> </p>
</div> </div>

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import Router from 'react-router';
import Row from 'react-bootstrap/lib/Row'; import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col'; import Col from 'react-bootstrap/lib/Col';
@ -14,6 +15,11 @@ import FurtherDetails from './further_details';
import UserActions from '../../actions/user_actions'; import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store'; import UserStore from '../../stores/user_store';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import EditionListActions from '../../actions/edition_list_actions';
import PieceActions from '../../actions/piece_actions'; import PieceActions from '../../actions/piece_actions';
import MediaContainer from './media_container'; import MediaContainer from './media_container';
@ -23,6 +29,10 @@ import EditionDetailProperty from './detail_property';
import AclButtonList from './../ascribe_buttons/acl_button_list'; import AclButtonList from './../ascribe_buttons/acl_button_list';
import CreateEditionsForm from '../ascribe_forms/create_editions_form'; import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import CreateEditionsButton from '../ascribe_buttons/create_editions_button'; import CreateEditionsButton from '../ascribe_buttons/create_editions_button';
import DeleteButton from '../ascribe_buttons/delete_button';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils'; import { mergeOptions } from '../../utils/general_utils';
@ -36,9 +46,12 @@ let Piece = React.createClass({
loadPiece: React.PropTypes.func loadPiece: React.PropTypes.func
}, },
mixins: [Router.Navigation],
getInitialState() { getInitialState() {
return mergeOptions( return mergeOptions(
UserStore.getState(), UserStore.getState(),
PieceListStore.getState(),
{ {
showCreateEditionsDialog: false showCreateEditionsDialog: false
} }
@ -47,11 +60,13 @@ let Piece = React.createClass({
componentDidMount() { componentDidMount() {
UserStore.listen(this.onChange); UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
UserActions.fetchCurrentUser(); UserActions.fetchCurrentUser();
}, },
componentWillUnmount() { componentWillUnmount() {
UserStore.unlisten(this.onChange); UserStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
@ -69,6 +84,20 @@ let Piece = React.createClass({
this.toggleCreateEditionsDialog(); this.toggleCreateEditionsDialog();
}, },
handleDeleteSuccess(response) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, this.state.orderBy, this.state.orderAsc);
// since we're deleting a piece, we just need to close
// all editions dialogs and not reload them
EditionListActions.closeAllEditionLists();
EditionListActions.clearAllEditionSelections();
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
},
getCreateEditionsDialog() { getCreateEditionsDialog() {
if(this.props.piece.num_editions < 1 && this.state.showCreateEditionsDialog) { if(this.props.piece.num_editions < 1 && this.state.showCreateEditionsDialog) {
return ( return (
@ -89,6 +118,9 @@ let Piece = React.createClass({
key: 'num_editions', key: 'num_editions',
value: numEditions value: numEditions
}); });
let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
}, },
render() { render() {
@ -121,6 +153,9 @@ let Piece = React.createClass({
piece={this.props.piece} piece={this.props.piece}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog} toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.handlePollingSuccess}/> onPollingSuccess={this.handlePollingSuccess}/>
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
piece={this.props.piece}/>
</AclButtonList> </AclButtonList>
{this.getCreateEditionsDialog()} {this.getCreateEditionsDialog()}

View File

@ -2,68 +2,83 @@
import React from 'react'; import React from 'react';
import ApiUrls from '../../constants/api_urls'; import Button from 'react-bootstrap/lib/Button';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text'; import Form from './form';
import InputTextArea from './input_textarea'; import Property from './property';
import ButtonSubmitOrClose from '../ascribe_buttons/button_submit_close'; import InputTextAreaToggable from './input_textarea_toggable';
import { getLangText } from '../../utils/lang_utils.js'
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js';
let ConsignForm = React.createClass({ let ConsignForm = React.createClass({
mixins: [FormMixin], propTypes: {
url: React.PropTypes.string,
id: React.PropTypes.object,
url() { message: React.PropTypes.string,
return ApiUrls.ownership_consigns; onRequestHide: React.PropTypes.func,
}, handleSuccess: React.PropTypes.func
getFormData() {
return {
bitcoin_id: this.getBitcoinIds().join(),
consignee: this.refs.consignee.state.value,
consign_message: this.refs.consign_message.state.value,
password: this.refs.password.state.value
};
}, },
renderForm() { getFormData(){
let title = this.getTitlesString().join(''); return this.props.id;
let username = this.props.currentUser.username; },
let message =
`${getLangText('Hi')},
${getLangText('I consign')} : render() {
${title}${getLangText('to you')}.
${getLangText('Truly yours')},
${username}`;
return ( return (
<form id="consign_modal_content" role="form" onSubmit={this.submit}> <Form
<input className="invisible" type="email" name="fake_consignee"/> ref='form'
<input className="invisible" type="password" name="fake_password"/> url={this.props.url}
<InputText getFormData={this.getFormData}
ref="consignee" handleSuccess={this.props.handleSuccess}
placeHolder={getLangText('Consignee email')} buttons={
required="required" <div className="modal-footer">
type="email" <p className="pull-right">
submitted={this.state.submitted}/> <Button
<InputTextArea className="btn btn-default btn-sm ascribe-margin-1px"
ref="consign_message" type="submit">{getLangText('CONSIGN')}</Button>
defaultValue={message} <Button
required="" className="btn btn-danger btn-delete btn-sm ascribe-margin-1px"
/> style={{marginLeft: '0'}}
<InputText onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</Button>
ref="password" </p>
placeHolder={getLangText('Password')} </div>}
required="required" spinner={
type="password" <div className="modal-footer">
submitted={this.state.submitted}/> <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<ButtonSubmitOrClose </div>}>
text={getLangText('CONSIGN')} <Property
onClose={this.props.onRequestHide} name='consignee'
submitted={this.state.submitted} /> label={getLangText('Email')}>
</form> <input
type="email"
placeholder={getLangText('Email of the consignee')}
required/>
</Property>
<Property
name='consign_message'
label={getLangText('Personal Message')}
editable={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>
</Property>
<Property
name='password'
label={getLangText('Password')}>
<input
type="password"
placeholder={getLangText('Enter your password')}
required/>
</Property>
<hr />
</Form>
); );
} }
}); });

View File

@ -0,0 +1,41 @@
'use strict';
import React from 'react';
import requests from '../../utils/requests';
import ApiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
import { getLangText } from '../../utils/lang_utils';
let PieceDeleteForm = React.createClass({
propTypes: {
pieceId: React.PropTypes.number
},
mixins: [FormMixin],
url() {
return requests.prepareUrl(ApiUrls.piece, {piece_id: this.props.pieceId});
},
httpVerb() {
return 'delete';
},
renderForm () {
return (
<div className="modal-body">
<p>{getLangText('Are you sure you would like to permanently delete this piece')}&#63;</p>
<p>{getLangText('This is an irrevocable action%s', '.')}</p>
<div className="modal-footer">
<button type="submit" className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" onClick={this.submit}>{getLangText('YES, DELETE')}</button>
<button className="btn btn-default btn-sm ascribe-margin-1px" style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</button>
</div>
</div>
);
}
});
export default PieceDeleteForm;

View File

@ -12,13 +12,9 @@ let EditionRemoveFromCollectionForm = React.createClass({
mixins: [FormMixin], mixins: [FormMixin],
url() { url() {
if (this.props.editions.constructor === Array) { return requests.prepareUrl(apiUrls.edition_remove_from_collection, {edition_id: this.getBitcoinIds().join()});
return requests.prepareUrl(apiUrls.edition_remove_from_collection, {edition_id: this.getBitcoinIds().join()});
}
else {
return requests.prepareUrl(apiUrls.piece_remove_from_collection, {piece_id: this.editions.piece_id});
}
}, },
httpVerb(){ httpVerb(){
return 'delete'; return 'delete';
}, },

View File

@ -0,0 +1,42 @@
'use strict';
import React from 'react';
import { getLangText } from '../../utils/lang_utils.js';
import requests from '../../utils/requests';
import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin';
let PieceRemoveFromCollectionForm = React.createClass({
propTypes: {
pieceId: React.PropTypes.number
},
mixins: [FormMixin],
url() {
return requests.prepareUrl(apiUrls.piece_remove_from_collection, {piece_id: this.props.pieceId});
},
httpVerb(){
return 'delete';
},
renderForm () {
return (
<div className="modal-body">
<p>{getLangText('Are you sure you would like to remove this piece from your collection')}&#63;</p>
<p>{getLangText('This is an irrevocable action%s', '.')}</p>
<div className="modal-footer">
<button type="submit" className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" onClick={this.submit}>{getLangText('YES, REMOVE')}</button>
<button className="btn btn-default btn-sm ascribe-margin-1px" style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</button>
</div>
</div>
);
}
});
export default PieceRemoveFromCollectionForm;

View File

@ -7,8 +7,10 @@ import Alert from 'react-bootstrap/lib/Alert';
import apiUrls from '../../constants/api_urls'; import apiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin'; import FormMixin from '../../mixins/form_mixin';
import AclButton from './../ascribe_buttons/acl_button';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js' import { getLangText } from '../../utils/lang_utils.js';
let RequestActionForm = React.createClass({ let RequestActionForm = React.createClass({
mixins: [FormMixin], mixins: [FormMixin],
@ -52,15 +54,31 @@ let RequestActionForm = React.createClass({
renderForm() { renderForm() {
let edition = this.props.editions[0]; let edition = this.props.editions[0];
let buttonAccept = (
<div id="request_accept"
onClick={this.handleRequest}
className='btn btn-default btn-sm ascribe-margin-1px'>{getLangText('ACCEPT')}
</div>);
if (edition.request_action === 'unconsign'){
console.log(this.props)
buttonAccept = (
<AclButton
availableAcls={{'acl_unconsign': true}}
action="acl_unconsign"
pieceOrEditions={this.props.editions}
currentUser={this.props.currentUser}
handleSuccess={this.props.handleSuccess} />
);
}
let buttons = ( let buttons = (
<span>
<span> <span>
<span> {buttonAccept}
<div id="request_accept" onClick={this.handleRequest} className='btn btn-default btn-sm ascribe-margin-1px'>{getLangText('ACCEPT')}</div>
</span>
<span>
<div id="request_deny" onClick={this.handleRequest} className='btn btn-danger btn-delete btn-sm ascribe-margin-1px'>{getLangText('REJECT')}</div>
</span>
</span> </span>
<span>
<div id="request_deny" onClick={this.handleRequest} className='btn btn-danger btn-delete btn-sm ascribe-margin-1px'>{getLangText('REJECT')}</div>
</span>
</span>
); );
if (this.state.submitted){ if (this.state.submitted){
buttons = ( buttons = (
@ -72,7 +90,7 @@ let RequestActionForm = React.createClass({
return ( return (
<Alert bsStyle='warning'> <Alert bsStyle='warning'>
<div style={{textAlign: 'center'}}> <div style={{textAlign: 'center'}}>
<div>{ edition.owner } {getFormData('requests you')} { edition.request_action } {getLangText('this edition%s', '.')}&nbsp;&nbsp;</div> <div>{ edition.owner } {getLangText('requests you')} { edition.request_action } {getLangText('this edition%s', '.')}&nbsp;&nbsp;</div>
{buttons} {buttons}
</div> </div>
</Alert> </Alert>

View File

@ -2,71 +2,91 @@
import React from 'react'; import React from 'react';
import ApiUrls from '../../constants/api_urls'; import Button from 'react-bootstrap/lib/Button';
import FormMixin from '../../mixins/form_mixin'; import Alert from 'react-bootstrap/lib/Alert';
import InputText from './input_text';
import InputTextArea from './input_textarea'; import Form from './form';
import ButtonSubmitOrClose from '../ascribe_buttons/button_submit_close'; import Property from './property';
import { getLangText } from '../../utils/lang_utils.js' import InputTextAreaToggable from './input_textarea_toggable';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js';
let TransferForm = React.createClass({ let TransferForm = React.createClass({
mixins: [FormMixin], propTypes: {
url: React.PropTypes.string,
url() { id: React.PropTypes.object,
return ApiUrls.ownership_transfers; message: React.PropTypes.string,
}, editions: React.PropTypes.array,
currentUser: React.PropTypes.object,
getFormData() { onRequestHide: React.PropTypes.func,
return { handleSuccess: React.PropTypes.func
bitcoin_id: this.getBitcoinIds().join(),
transferee: this.refs.transferee.state.value,
transfer_message: this.refs.transfer_message.state.value,
password: this.refs.password.state.value
};
}, },
renderForm() { getFormData(){
let title = this.getTitlesString().join(''); return this.props.id;
let username = this.props.currentUser.username; },
let message =
`${getLangText('Hi')},
${getLangText('I transfer ownership of')} : render() {
${title}${getLangText('to you')}.
${getLangText('Truly yours')},
${username}`;
return ( return (
<form id="transfer_modal_content" role="form" onSubmit={this.submit}> <Form
<input className="invisible" type="email" name="fake_transferee"/> ref='form'
<input className="invisible" type="password" name="fake_password"/> url={this.props.url}
<InputText getFormData={this.getFormData}
ref="transferee" handleSuccess={this.props.handleSuccess}
placeHolder={getLangText('Transferee email')} buttons={
required="required" <div className="modal-footer">
type="email" <p className="pull-right">
submitted={this.state.submitted}/> <Button
<InputTextArea className="btn btn-default btn-sm ascribe-margin-1px"
ref="transfer_message" type="submit">{getLangText('TRANSFER')}</Button>
defaultValue={message} <Button
required="" className="btn btn-danger btn-delete btn-sm ascribe-margin-1px"
/> style={{marginLeft: '0'}}
<InputText onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</Button>
ref="password" </p>
placeHolder={getLangText('Password')} </div>}
required="required" spinner={
type="password" <div className="modal-footer">
submitted={this.state.submitted}/> <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<div> </div>}>
Make sure that display instructions and technology details are correct. <Property
name='transferee'
label={getLangText('Email')}>
<input
type="email"
placeholder={getLangText('Email of the transferee')}
required/>
</Property>
<Property
name='transfer_message'
label={getLangText('Personal Message')}
editable={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>
</Property>
<Property
name='password'
label={getLangText('Password')}>
<input
type="password"
placeholder={getLangText('Enter your password')}
required/>
</Property>
<hr />
<br />
<Alert bsStyle='warning'>
Make sure that display instructions and technology details are correct.<br/>
They cannot be edited after the transfer. They cannot be edited after the transfer.
</div> </Alert>
<ButtonSubmitOrClose </Form>
text={getLangText('TRANSFER')}
onClose={this.props.onRequestHide}
submitted={this.state.submitted} />
</form>
); );
} }
}); });

View File

@ -2,59 +2,75 @@
import React from 'react'; import React from 'react';
import ApiUrls from '../../constants/api_urls'; import Button from 'react-bootstrap/lib/Button';
import FormMixin from '../../mixins/form_mixin';
import InputText from './input_text'; import Form from './form';
import InputTextArea from './input_textarea'; import Property from './property';
import ButtonSubmitOrClose from '../ascribe_buttons/button_submit_close'; import InputTextAreaToggable from './input_textarea_toggable';
import { getLangText } from '../../utils/lang_utils.js'
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js';
let UnConsignForm = React.createClass({ let UnConsignForm = React.createClass({
mixins: [FormMixin], propTypes: {
url: React.PropTypes.string,
url() { id: React.PropTypes.object,
return ApiUrls.ownership_unconsigns; message: React.PropTypes.string,
editions: React.PropTypes.array,
onRequestHide: React.PropTypes.func,
handleSuccess: React.PropTypes.func
}, },
getFormData() { getFormData(){
return { return this.props.id;
bitcoin_id: this.getBitcoinIds().join(),
unconsign_message: this.refs.unconsign_message.state.value,
password: this.refs.password.state.value
};
}, },
renderForm() { render() {
let title = this.getTitlesString().join('');
let username = this.props.currentUser.username;
let message =
`${getLangText('Hi')},
${getLangText('I un-consign')}:
${title}${getLangText('from you')}.
${getLangText('Truly yours')},
${username}`;
return ( return (
<form id="unconsign_modal_content" role="form" onSubmit={this.submit}> <Form
<input className="invisible" type="email" name="fake_unconsignee"/> ref='form'
<input className="invisible" type="password" name="fake_password"/> url={this.props.url}
<InputTextArea getFormData={this.getFormData}
ref="unconsign_message" handleSuccess={this.props.handleSuccess}
defaultValue={message} buttons={
required="" /> <div className="modal-footer">
<InputText <p className="pull-right">
ref="password" <Button
placeHolder={getLangText('Password')} className="btn btn-default btn-sm ascribe-margin-1px"
required="required" type="submit">{getLangText('UNCONSIGN')}</Button>
type="password" <Button
submitted={this.state.submitted} /> className="btn btn-danger btn-delete btn-sm ascribe-margin-1px"
<ButtonSubmitOrClose style={{marginLeft: '0'}}
text={getLangText('UNCONSIGN')} onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</Button>
onClose={this.props.onRequestHide} </p>
submitted={this.state.submitted} /> </div>}
</form> spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
</div>}>
<Property
name='unconsign_message'
label={getLangText('Personal Message')}
editable={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>
</Property>
<Property
name='password'
label={getLangText('Password')}>
<input
type="password"
placeholder={getLangText('Enter your password')}
required/>
</Property>
<hr />
</Form>
); );
} }
}); });

View File

@ -2,48 +2,68 @@
import React from 'react'; import React from 'react';
import ApiUrls from '../../constants/api_urls'; import Button from 'react-bootstrap/lib/Button';
import FormMixin from '../../mixins/form_mixin'; import Alert from 'react-bootstrap/lib/Alert';
import InputTextArea from './input_textarea';
import ButtonSubmitOrClose from '../ascribe_buttons/button_submit_close'; import Form from './form';
import { getLangText } from '../../utils/lang_utils.js' import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js';
let UnConsignRequestForm = React.createClass({ let UnConsignRequestForm = React.createClass({
mixins: [FormMixin], propTypes: {
url: React.PropTypes.string,
url() { id: React.PropTypes.object,
return ApiUrls.ownership_unconsigns_request; message: React.PropTypes.string,
onRequestHide: React.PropTypes.func,
handleSuccess: React.PropTypes.func
}, },
getFormData() { getFormData(){
return { return this.props.id;
bitcoin_id: this.props.edition.bitcoin_id,
unconsign_request_message: this.refs.unconsign_request_message.state.value
};
}, },
renderForm() { render() {
let title = this.props.edition.title;
let username = this.props.currentUser.username;
let message =
`${getLangText('Hi')},
${getLangText('I request you to un-consign')} \" ${title} \".
${getLangText('Truly yours')},
${username}`;
return ( return (
<form id="unconsign_request_modal_content" role="form" onSubmit={this.submit}> <Form
<InputTextArea ref='form'
ref="unconsign_request_message" url={this.props.url}
defaultValue={message} getFormData={this.getFormData}
required="" /> handleSuccess={this.props.handleSuccess}
<ButtonSubmitOrClose buttons={
text={getLangText('UNCONSIGN REQUEST')} <div className="modal-footer">
onClose={this.props.onRequestHide} <p className="pull-right">
submitted={this.state.submitted} /> <Button
</form> className="btn btn-default btn-sm ascribe-margin-1px"
type="submit">{getLangText('REQUEST UNCONSIGN')}</Button>
<Button
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px"
style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</Button>
</p>
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
</div>}>
<Property
name='unconsign_request_message'
label={getLangText('Personal Message')}
editable={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>
</Property>
<hr />
</Form>
); );
} }
}); });

View File

@ -32,6 +32,7 @@ let InputTextAreaToggable = React.createClass({
className={className} className={className}
value={this.state.value} value={this.state.value}
rows={this.props.rows} rows={this.props.rows}
maxRows={10}
required={this.props.required} required={this.props.required}
onChange={this.handleChange} onChange={this.handleChange}
onBlur={this.props.onBlur} onBlur={this.props.onBlur}

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import ReactAddons from 'react/addons';
import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin'; import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
@ -39,15 +40,17 @@ let PropertyCollapsile = React.createClass({
}); });
}, },
handleChange(event) {
this.setState({value: event.target.value});
},
renderChildren() { renderChildren() {
if(this.state.show) { if(this.state.show) {
return (<div return ReactAddons.Children.map(this.props.children, (child) => {
className={classNames(this.getCollapsibleClassSet()) + ' ascribe-settings-property'} return ReactAddons.addons.cloneWithProps(child, {
ref="panel"> onChange: this.handleChange
{this.props.children} });
</div>); });
} else {
return null;
} }
}, },
@ -75,13 +78,18 @@ let PropertyCollapsile = React.createClass({
onClick={this.handleFocus} onClick={this.handleFocus}
onFocus={this.handleFocus}> onFocus={this.handleFocus}>
<input <input
onChange={this.handleChange}
type="checkbox" type="checkbox"
ref="checkboxCollapsible"/> ref="checkboxCollapsible"/>
{/* PLEASE LEAVE THE SPACE BETWEEN LABEL and this.props.label */} {/* PLEASE LEAVE THE SPACE BETWEEN LABEL and this.props.label */}
<span className="checkbox"> {this.props.checkboxLabel}</span> <span className="checkbox"> {this.props.checkboxLabel}</span>
</div> </div>
</OverlayTrigger> </OverlayTrigger>
{this.renderChildren()} <div
className={classNames(this.getCollapsibleClassSet()) + ' ascribe-settings-property'}
ref="panel">
{this.renderChildren()}
</div>
</div> </div>
); );
} }

View File

@ -17,24 +17,41 @@ let ModalWrapper = React.createClass({
handleSuccess: React.PropTypes.func.isRequired, handleSuccess: React.PropTypes.func.isRequired,
button: React.PropTypes.object.isRequired, button: React.PropTypes.object.isRequired,
children: React.PropTypes.object, children: React.PropTypes.object,
tooltip: React.PropTypes.string.isRequired tooltip: React.PropTypes.string
},
getModalTrigger() {
return (
<ModalTrigger modal={
<ModalBody
title={this.props.title}
handleSuccess={this.props.handleSuccess}>
{this.props.children}
</ModalBody>
}>
{this.props.button}
</ModalTrigger>
);
}, },
render() { render() {
return ( if(this.props.tooltip) {
<OverlayTrigger delay={500} placement="left" return (
overlay={<Tooltip>{this.props.tooltip}</Tooltip>}> <OverlayTrigger
<ModalTrigger modal={ delay={500}
<ModalBody placement="left"
title={this.props.title} overlay={<Tooltip>{this.props.tooltip}</Tooltip>}>
handleSuccess={this.props.handleSuccess}> {this.getModalTrigger()}
{this.props.children} </OverlayTrigger>
</ModalBody> );
}> } else {
{this.props.button} return (
</ModalTrigger> <span>
</OverlayTrigger> {/* This needs to be some kind of inline-block */}
); {this.getModalTrigger()}
</span>
);
}
} }
}); });

View File

@ -10,9 +10,12 @@ import EditionListActions from '../../actions/edition_list_actions';
import UserStore from '../../stores/user_store'; import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions'; import UserActions from '../../actions/user_actions';
import PieceListStore from '../../stores/piece_list_store';
import PieceListActions from '../../actions/piece_list_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 DeleteButton from '../ascribe_buttons/delete_button';
import { getAvailableAcls } from '../../utils/acl_utils'; import { getAvailableAcls } from '../../utils/acl_utils';
import { getLangText } from '../../utils/lang_utils.js'; import { getLangText } from '../../utils/lang_utils.js';
@ -23,7 +26,11 @@ let PieceListBulkModal = React.createClass({
}, },
getInitialState() { getInitialState() {
return mergeOptions(EditionListStore.getState(), UserStore.getState()); return mergeOptions(
EditionListStore.getState(),
UserStore.getState(),
PieceListStore.getState()
);
}, },
onChange(state) { onChange(state) {
@ -69,20 +76,18 @@ let PieceListBulkModal = React.createClass({
}, },
handleSuccess() { handleSuccess() {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, this.state.orderBy, this.state.orderAsc);
this.fetchSelectedPieceEditionList() this.fetchSelectedPieceEditionList()
.forEach((pieceId) => { .forEach((pieceId) => {
let editionsForPiece = this.state.editionList[pieceId]; EditionListActions.refreshEditionList(pieceId);
for(let i = 1; i <= editionsForPiece.page; i++) {
EditionListActions.fetchEditionList(pieceId, i, editionsForPiece.pageSize, editionsForPiece.orderBy, editionsForPiece.orderAsc);
}
}); });
EditionListActions.clearAllEditionSelections(); EditionListActions.clearAllEditionSelections();
}, },
render() { render() {
let selectedEditions = this.fetchSelectedEditionList(); let selectedEditions = this.fetchSelectedEditionList();
let availableAcls = getAvailableAcls(selectedEditions); let availableAcls = getAvailableAcls(selectedEditions, (aclName) => aclName !== 'acl_view');
if(Object.keys(availableAcls).length > 0) { if(Object.keys(availableAcls).length > 0) {
return ( return (
@ -106,7 +111,11 @@ let PieceListBulkModal = React.createClass({
availableAcls={availableAcls} availableAcls={availableAcls}
editions={selectedEditions} editions={selectedEditions}
handleSuccess={this.handleSuccess} handleSuccess={this.handleSuccess}
className="text-center ascribe-button-list collapse-group"/> className="text-center ascribe-button-list collapse-group">
<DeleteButton
handleSuccess={this.handleSuccess}
editions={selectedEditions}/>
</AclButtonList>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,11 +5,19 @@ import React from 'react';
let TableItemAclFiltered = React.createClass({ let TableItemAclFiltered = React.createClass({
propTypes: { propTypes: {
content: React.PropTypes.object.isRequired content: React.PropTypes.object,
requestAction: React.PropTypes.string
}, },
render() { render() {
var availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare']; var availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete'];
if (this.props.requestAction){
return (
<span>
{this.props.requestAction + ' request pending'}
</span>
);
}
let filteredAcls = Object.keys(this.props.content).filter((key) => { let filteredAcls = Object.keys(this.props.content).filter((key) => {
return availableAcls.indexOf(key) > -1 && this.props.content[key]; return availableAcls.indexOf(key) > -1 && this.props.content[key];

View File

@ -2,11 +2,6 @@
import React from 'react/addons'; import React from 'react/addons';
import promise from 'es6-promise';
promise.polyfill();
import fetch from 'isomorphic-fetch';
import { getCookie } from '../../utils/fetch_api_utils'; import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
@ -96,7 +91,8 @@ var ReactS3FineUploader = React.createClass({
setIsUploadReady: React.PropTypes.func, setIsUploadReady: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func, isReadyForFormSubmission: React.PropTypes.func,
areAssetsDownloadable: React.PropTypes.bool, areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool areAssetsEditable: React.PropTypes.bool,
defaultErrorMessage: React.PropTypes.string
}, },
getDefaultProps() { getDefaultProps() {
@ -141,7 +137,8 @@ var ReactS3FineUploader = React.createClass({
} }
return name; return name;
}, },
multiple: false multiple: false,
defaultErrorMessage: 'Unexpected error. Please contact us if this happens repeatedly.'
}; };
}, },
@ -188,56 +185,87 @@ var ReactS3FineUploader = React.createClass({
multiple: this.props.multiple, multiple: this.props.multiple,
retry: this.props.retry, retry: this.props.retry,
callbacks: { callbacks: {
onSubmit: this.onSubmit,
onComplete: this.onComplete, onComplete: this.onComplete,
onCancel: this.onCancel, onCancel: this.onCancel,
onDelete: this.onDelete,
onProgress: this.onProgress, onProgress: this.onProgress,
onRetry: this.onRetry,
onAutoRetry: this.onAutoRetry,
onManualRetry: this.onManualRetry,
onDeleteComplete: this.onDeleteComplete, onDeleteComplete: this.onDeleteComplete,
onSessionRequestComplete: this.onSessionRequestComplete onSessionRequestComplete: this.onSessionRequestComplete,
onError: this.onError
} }
}; };
}, },
requestKey(fileId) { requestKey(fileId) {
let defer = new fineUploader.Promise();
let filename = this.state.uploader.getName(fileId); let filename = this.state.uploader.getName(fileId);
return new Promise((resolve, reject) => {
fetch(this.props.keyRoutine.url, { window.fetch(this.props.keyRoutine.url, {
method: 'post', method: 'post',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken') 'X-CSRFToken': getCookie('csrftoken')
}, },
credentials: 'include', credentials: 'include',
body: JSON.stringify({ body: JSON.stringify({
'filename': filename, 'filename': filename,
'file_class': this.props.keyRoutine.fileClass, 'file_class': this.props.keyRoutine.fileClass,
'piece_id': this.props.keyRoutine.pieceId 'piece_id': this.props.keyRoutine.pieceId
})
}) })
.then((res) => { })
return res.json(); .then((res) => {
}) return res.json();
.then((res) =>{ })
resolve(res.key); .then((res) =>{
}) defer.success(res.key);
.catch((err) => { })
console.error(err); .catch((err) => {
reject(err); defer.failure(err);
});
}); });
return defer;
},
createBlob(file) {
let defer = new fineUploader.Promise();
window.fetch(this.props.createBlobRoutine.url, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
credentials: 'include',
body: JSON.stringify({
'filename': file.name,
'key': file.key,
'piece_id': this.props.createBlobRoutine.pieceId
})
})
.then((res) => {
return res.json();
})
.then((res) =>{
if(res.otherdata) {
file.s3Url = res.otherdata.url_safe;
file.s3UrlSafe = res.otherdata.url_safe;
} else if(res.digitalwork) {
file.s3Url = res.digitalwork.url_safe;
file.s3UrlSafe = res.digitalwork.url_safe;
} else {
throw new Error('Could not find a url to download.');
}
defer.success(res.key);
})
.catch((err) => {
defer.failure(err);
console.error(err);
});
return defer;
}, },
/* FineUploader specific callback function handlers */ /* FineUploader specific callback function handlers */
onSubmit() {
console.log('submit');
},
onComplete(id) { onComplete(id) {
let files = this.state.filesToUpload; let files = this.state.filesToUpload;
files[id].status = 'upload successful'; files[id].status = 'upload successful';
@ -272,57 +300,9 @@ var ReactS3FineUploader = React.createClass({
} }
}, },
createBlob(file) { onError() {
let defer = new fineUploader.Promise(); let notification = new GlobalNotificationModel(this.props.defaultErrorMessage, 'danger', 5000);
fetch(this.props.createBlobRoutine.url, { GlobalNotificationActions.appendGlobalNotification(notification);
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
credentials: 'include',
body: JSON.stringify({
'filename': file.name,
'key': file.key,
'piece_id': this.props.createBlobRoutine.pieceId
})
})
.then((res) => {
return res.json();
})
.then((res) =>{
if(res.otherdata) {
file.s3Url = res.otherdata.url_safe;
file.s3UrlSafe = res.otherdata.url_safe;
} else if(res.digitalwork) {
file.s3Url = res.digitalwork.url_safe;
file.s3UrlSafe = res.digitalwork.url_safe;
} else {
throw new Error('Could not find a url to download.');
}
defer.success(res.key);
})
.catch((err) => {
console.error(err);
});
return defer;
},
onRetry() {
console.log('retry');
},
onAutoRetry() {
console.log('autoretry');
},
onManualRetry() {
console.log('manualretry');
},
onDelete() {
console.log('delete');
}, },
onCancel(id) { onCancel(id) {

View File

@ -139,6 +139,7 @@ let RegisterPiece = React.createClass( {
if (this.props.canSpecifyEditions) { if (this.props.canSpecifyEditions) {
return ( return (
<PropertyCollapsible <PropertyCollapsible
name="num_editions"
checkboxLabel={getLangText('Specify editions')}> checkboxLabel={getLangText('Specify editions')}>
<span>{getLangText('Editions')}</span> <span>{getLangText('Editions')}</span>
<input <input

View File

@ -56,6 +56,31 @@ class EditionListStore {
this.editionList[pieceId].count = count; this.editionList[pieceId].count = count;
} }
/**
* We often just have to refresh the edition list for a certain pieceId,
* this method provides exactly that functionality without any side effects
*/
onRefreshEditionList(pieceId) {
const prevEditionListLength = this.editionList[pieceId].length;
const prevEditionListPage = this.editionList[pieceId].page;
const prevEditionListPageSize = this.editionList[pieceId].pageSize;
// to clear an array, david walsh recommends to just set it's length to zero
// http://davidwalsh.name/empty-array
this.editionList[pieceId].length = 0;
// refetch editions with adjusted page size
EditionsListActions.fetchEditionList(pieceId, 1, prevEditionListLength, this.editionList[pieceId].orderBy, this.editionList[pieceId].orderAsc)
.then(() => {
// reset back to the normal pageSize and page
this.editionList[pieceId].page = prevEditionListPage;
this.editionList[pieceId].pageSize = prevEditionListPageSize;
})
.catch((err) => {
console.error(err);
});
}
onSelectEdition({pieceId, editionId, toValue}) { onSelectEdition({pieceId, editionId, toValue}) {
this.editionList[pieceId].forEach((edition) => { this.editionList[pieceId].forEach((edition) => {

View File

@ -6,9 +6,9 @@ function intersectAcls(a, b) {
return a.filter((val) => b.indexOf(val) > -1); return a.filter((val) => b.indexOf(val) > -1);
} }
export function getAvailableAcls(editions) { export function getAvailableAcls(editions, filterFn) {
let availableAcls = []; let availableAcls = [];
if (editions.constructor !== Array){ if (!editions || editions.constructor !== Array){
return []; return [];
} }
// if you copy a javascript array of objects using slice, then // if you copy a javascript array of objects using slice, then
@ -26,6 +26,13 @@ export function getAvailableAcls(editions) {
edition.acl = sanitize(edition.acl, (val) => !val); edition.acl = sanitize(edition.acl, (val) => !val);
edition.acl = Object.keys(edition.acl); edition.acl = Object.keys(edition.acl);
// additionally, the user can specify a filter function for
// an acl array
if(typeof filterFn === 'function') {
edition.acl = edition.acl.filter(filterFn);
}
return edition; return edition;
}); });

View File

@ -54,6 +54,11 @@ class Requests {
} }
getUrl(url) { getUrl(url) {
// Handle case, that the url string is not defined at all
if (!url) {
throw new Error('Url was not defined and could therefore not be mapped.');
}
let name = url; let name = url;
if (!url.match(/^http/)) { if (!url.match(/^http/)) {
url = this.urlMap[url]; url = this.urlMap[url];
@ -66,9 +71,16 @@ class Requests {
} }
prepareUrl(url, params, attachParamsToQuery) { prepareUrl(url, params, attachParamsToQuery) {
let newUrl = this.getUrl(url); let newUrl;
let re = /\${(\w+)}/g; let re = /\${(\w+)}/g;
// catch errors and throw them to react
try {
newUrl = this.getUrl(url);
} catch(err) {
throw err;
}
newUrl = newUrl.replace(re, (match, key) => { newUrl = newUrl.replace(re, (match, key) => {
let val = params[key]; let val = params[key];
if (!val) { if (!val) {

View File

@ -58,6 +58,10 @@ hr {
.no-padding{ .no-padding{
padding: 0; padding: 0;
} }
.inline{
display: inline;
}
.navbar-default { .navbar-default {
border: none; border: none;
border-left:0; border-left:0;