From bf0a2aeefb0ea8114561fe78f590cd9fea13f922 Mon Sep 17 00:00:00 2001 From: ddejongh Date: Thu, 11 Jun 2015 15:03:55 +0200 Subject: [PATCH] consign unconsign requests async emails --- gulpfile.js | 2 +- .../accordion_list_item_table_editions.js | 4 +- js/components/ascribe_buttons/acl_button.js | 10 ++- .../ascribe_buttons/acl_button_list.js | 8 ++ .../ascribe_buttons/delete_button.js | 31 ++++++-- .../ascribe_forms/form_delete_edition.js | 2 +- .../form_remove_editions_from_collection.js | 35 ++++++++ .../ascribe_forms/form_request_action.js | 79 +++++++++++++++++++ js/components/ascribe_forms/form_unconsign.js | 7 +- .../piece_list_bulk_modal.js | 28 +------ js/components/edition.js | 67 ++++++++++------ js/components/edition_container.js | 15 +--- js/constants/api_urls.js | 13 ++- js/constants/application_constants.js | 3 +- js/mixins/form_mixin.js | 10 +-- js/stores/edition_list_store.js | 4 +- js/utils/acl_utils.js | 23 ++++++ 17 files changed, 252 insertions(+), 89 deletions(-) create mode 100644 js/components/ascribe_forms/form_remove_editions_from_collection.js create mode 100644 js/components/ascribe_forms/form_request_action.js create mode 100644 js/utils/acl_utils.js diff --git a/gulpfile.js b/gulpfile.js index 1534c561..80b57550 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -53,7 +53,7 @@ gulp.task('js:build', function() { bundle(false); }); -gulp.task('serve', ['browser-sync', 'run-server', 'lint:watch', 'sass:build', 'sass:watch', 'copy'], function() { +gulp.task('serve', ['browser-sync', 'run-server', 'sass:build', 'sass:watch', 'copy'], function() { bundle(true); }); diff --git a/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js b/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js index 9b843372..c98e632d 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js @@ -111,10 +111,10 @@ let AccordionListItemTableEditions = React.createClass({ new ColumnModel( (item) => { return { - 'content': item.edition_number + 'content': item.edition_number + ' of ' + item.num_editions }; }, 'edition_number', - '#', + 'Edition', TableItemText, 1, true, diff --git a/js/components/ascribe_buttons/acl_button.js b/js/components/ascribe_buttons/acl_button.js index b3930215..526581c8 100644 --- a/js/components/ascribe_buttons/acl_button.js +++ b/js/components/ascribe_buttons/acl_button.js @@ -3,6 +3,7 @@ import React from 'react'; import ConsignForm from '../ascribe_forms/form_consign'; +import UnConsignForm from '../ascribe_forms/form_unconsign'; import TransferForm from '../ascribe_forms/form_transfer'; import LoanForm from '../ascribe_forms/form_loan'; import ShareForm from '../ascribe_forms/form_share_email'; @@ -30,7 +31,14 @@ let AclButton = React.createClass({ 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: , + handleSuccess: this.showNotification + }; + }else if (this.props.action === 'transfer') { return { title: 'Transfer artwork', tooltip: 'Transfer the ownership of the artwork', diff --git a/js/components/ascribe_buttons/acl_button_list.js b/js/components/ascribe_buttons/acl_button_list.js index ccabaa48..fca06408 100644 --- a/js/components/ascribe_buttons/acl_button_list.js +++ b/js/components/ascribe_buttons/acl_button_list.js @@ -6,6 +6,7 @@ import UserActions from '../../actions/user_actions'; import UserStore from '../../stores/user_store'; import AclButton from '../ascribe_buttons/acl_button'; +import DeleteButton from '../ascribe_buttons/delete_button'; let AclButtonList = React.createClass({ propTypes: { @@ -47,6 +48,12 @@ let AclButtonList = React.createClass({ editions={this.props.editions} currentUser={this.state.currentUser} handleSuccess={this.props.handleSuccess} /> + + ); } diff --git a/js/components/ascribe_buttons/delete_button.js b/js/components/ascribe_buttons/delete_button.js index d37546f6..0d37a236 100644 --- a/js/components/ascribe_buttons/delete_button.js +++ b/js/components/ascribe_buttons/delete_button.js @@ -1,34 +1,51 @@ +'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, + editions: React.PropTypes.array.isRequired }, mixins: [Router.Navigation], showNotification(response){ + this.props.editions + .forEach((edition) => { + EditionListActions.fetchEditionList(edition.parent); + }); + EditionListActions.clearAllEditionSelections(); 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 = ; - if (this.props.edition.acl.indexOf('delete') > -1) { - btnDelete = ; + let content = null; + + if (availableAcls.indexOf('delete') > -1) { + content = ; + btnDelete = ; } - else if (this.props.edition.acl.indexOf('del_from_collection') > -1){ - btnDelete = ; + else if (availableAcls.indexOf('del_from_collection') > -1){ + content = ; + btnDelete = ; } else{ return
; @@ -36,8 +53,6 @@ let DeleteButton = React.createClass({ return ( diff --git a/js/components/ascribe_forms/form_delete_edition.js b/js/components/ascribe_forms/form_delete_edition.js index dd7e5b88..3d81c08c 100644 --- a/js/components/ascribe_forms/form_delete_edition.js +++ b/js/components/ascribe_forms/form_delete_edition.js @@ -11,7 +11,7 @@ let EditionDeleteForm = React.createClass({ mixins: [FormMixin], url() { - return fetch.prepareUrl(ApiUrls.edition_delete, {edition_id: this.props.editions[0].bitcoin_id}); + return fetch.prepareUrl(ApiUrls.edition_delete, {edition_id: this.getBitcoinIds().join()}); }, httpVerb(){ return 'delete'; diff --git a/js/components/ascribe_forms/form_remove_editions_from_collection.js b/js/components/ascribe_forms/form_remove_editions_from_collection.js new file mode 100644 index 00000000..0a268c10 --- /dev/null +++ b/js/components/ascribe_forms/form_remove_editions_from_collection.js @@ -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 ( +
+

Are you sure you would like to remove these editions from your collection?

+

This is an irrevocable action.

+
+ + +
+
+ ); + } +}); + + +export default EditionRemoveFromCollectionForm; \ No newline at end of file diff --git a/js/components/ascribe_forms/form_request_action.js b/js/components/ascribe_forms/form_request_action.js new file mode 100644 index 00000000..1bc08361 --- /dev/null +++ b/js/components/ascribe_forms/form_request_action.js @@ -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 = ( + + +
ACCEPT
+
+ +
REJECT
+
+
+ ); + if (this.state.submitted){ + buttons = ( + + + + ); + } + return ( + + { edition.owner } requests you { edition.request_action } this edition.   + {buttons} + + ); + } +}); + + +export default RequestActionForm; \ No newline at end of file diff --git a/js/components/ascribe_forms/form_unconsign.js b/js/components/ascribe_forms/form_unconsign.js index 810c70d8..c7d1cff2 100644 --- a/js/components/ascribe_forms/form_unconsign.js +++ b/js/components/ascribe_forms/form_unconsign.js @@ -17,19 +17,20 @@ let UnConsignForm = React.createClass({ getFormData() { return { - bitcoin_id: this.props.edition.bitcoin_id, + bitcoin_id: this.getBitcoinIds().join(), unconsign_message: this.refs.unconsign_message.state.value, password: this.refs.password.state.value }; }, renderForm() { - let title = this.props.edition.title; + let title = this.getTitlesString().join(''); let username = this.props.currentUser.username; let message = `Hi, -I un-consign \" ${title} \" from you. +I un-consign: +${title}from you. Truly yours, ${username}`; diff --git a/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js b/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js index d557a48d..d3e7f127 100644 --- a/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js +++ b/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js @@ -14,6 +14,9 @@ import PieceListBulkModalSelectedEditionsWidget from './piece_list_bulk_modal_se import AclButtonList from '../ascribe_buttons/acl_button_list'; +import { getAvailableAcls } from '../../utils/acl_utils'; + + let PieceListBulkModal = React.createClass({ propTypes: { className: React.PropTypes.string @@ -60,29 +63,6 @@ let PieceListBulkModal = React.createClass({ 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() { EditionListActions.clearAllEditionSelections(); }, @@ -96,8 +76,8 @@ let PieceListBulkModal = React.createClass({ }, render() { - let availableAcls = this.getAvailableAcls(); let selectedEditions = this.fetchSelectedEditionList(); + let availableAcls = getAvailableAcls(selectedEditions); if(availableAcls.length > 0) { return ( diff --git a/js/components/edition.js b/js/components/edition.js index fa721778..7d6a66f8 100644 --- a/js/components/edition.js +++ b/js/components/edition.js @@ -12,10 +12,10 @@ import Glyphicon from 'react-bootstrap/lib/Glyphicon'; 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 AclButtonList from './ascribe_buttons/acl_button_list'; -import DeleteButton from './ascribe_buttons/delete_button'; import GlobalNotificationModel from '../models/global_notification_model'; import GlobalNotificationActions from '../actions/global_notification_actions'; @@ -28,8 +28,6 @@ import classNames from 'classnames'; let Edition = React.createClass({ propTypes: { edition: React.PropTypes.object, - currentUser: React.PropTypes.object, - deleteEdition: React.PropTypes.func, loadEdition: React.PropTypes.func }, @@ -70,8 +68,7 @@ let Edition = React.createClass({ + edition={this.props.edition} /> @@ -93,6 +90,13 @@ let Edition = React.createClass({ history={this.props.edition.ownership_history} /> + 0}> + + + 0}> @@ -112,13 +116,6 @@ let Edition = React.createClass({ label="Owned by SPOOL address" value={ownerAddress} /> - - - - ); @@ -146,22 +143,31 @@ let EditionHeader = React.createClass({ let EditionSummary = React.createClass({ propTypes: { - edition: React.PropTypes.object, - currentUser: React.PropTypes.object + edition: React.PropTypes.object }, handleSuccess(){ EditionActions.fetchOne(this.props.edition.id); }, - + showNotification(response){ + this.handleSuccess(); + let notification = new GlobalNotificationModel(response.notification, 'success'); + GlobalNotificationActions.appendGlobalNotification(notification); + }, render() { - return ( -
- - - -
+ let status = null; + if (this.props.edition.status.length > 0){ + status = ; + } + let actions = null; + if (this.props.edition.request_action){ + actions = ( + ); + } + else { + actions = ( - + ); + } + + return ( +
+ + + + {status} +
+ {actions}
); @@ -273,8 +290,8 @@ let EditionDetailProperty = React.createClass({ getDefaultProps() { return { separator: ':', - labelClassName: 'col-xs-5 col-sm-5 col-md-5 col-lg-5', - valueClassName: 'col-xs-7 col-sm-7 col-md-7 col-lg-7' + labelClassName: 'col-xs-5 col-sm-4 col-md-3 col-lg-3', + valueClassName: 'col-xs-7 col-sm-8 col-md-9 col-lg-9' }; }, diff --git a/js/components/edition_container.js b/js/components/edition_container.js index 8f8af927..eaf94864 100644 --- a/js/components/edition_container.js +++ b/js/components/edition_container.js @@ -2,12 +2,8 @@ import React from 'react'; -import { mergeOptions } from '../utils/general_utils'; - import EditionActions from '../actions/edition_actions'; import EditionStore from '../stores/edition_store'; -import UserActions from '../actions/user_actions'; -import UserStore from '../stores/user_store'; import Edition from './edition'; @@ -16,7 +12,7 @@ import Edition from './edition'; */ let EditionContainer = React.createClass({ getInitialState() { - return mergeOptions(UserStore.getState(), EditionStore.getState()); + return EditionStore.getState(); }, onChange(state) { @@ -25,20 +21,13 @@ let EditionContainer = React.createClass({ componentDidMount() { EditionStore.listen(this.onChange); - UserStore.listen(this.onChange); - - UserActions.fetchCurrentUser(); EditionActions.fetchOne(this.props.params.editionId); }, componentWillUnmount() { EditionStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); }, - deleteEdition() { - // Delete Edition from server - }, loadEdition() { EditionActions.fetchOne(this.props.params.editionId); @@ -49,8 +38,6 @@ let EditionContainer = React.createClass({ return ( ); } else { diff --git a/js/constants/api_urls.js b/js/constants/api_urls.js index 39cf3af4..f8bffda4 100644 --- a/js/constants/api_urls.js +++ b/js/constants/api_urls.js @@ -3,8 +3,6 @@ import AppConstants from './application_constants'; let apiUrls = { - 'ownership_shares_mail': AppConstants.baseUrl + 'ownership/shares/mail/', - 'ownership_transfers': AppConstants.baseUrl + 'ownership/transfers/', 'user': AppConstants.baseUrl + 'users/', 'piece': AppConstants.baseUrl + 'pieces/${piece_id}', 'pieces_list': AppConstants.baseUrl + 'pieces/', @@ -12,10 +10,19 @@ let apiUrls = { 'edition': AppConstants.baseUrl + 'editions/${bitcoin_id}/', 'editions_list': AppConstants.baseUrl + 'pieces/${piece_id}/editions/', 'edition_delete': AppConstants.baseUrl + 'editions/${edition_id}/', - 'ownership_loans': AppConstants.baseUrl + 'ownership/loans/', + 'edition_remove_from_collection': AppConstants.baseUrl + 'ownership/shares/${edition_id}/', + 'ownership_shares_mail': AppConstants.baseUrl + 'ownership/shares/mail/', + 'ownership_transfers': AppConstants.baseUrl + 'ownership/transfers/', 'ownership_consigns': AppConstants.baseUrl + 'ownership/consigns/', + 'ownership_consigns_confirm': AppConstants.baseUrl + 'ownership/consigns/confirm/', + 'ownership_consigns_deny': AppConstants.baseUrl + 'ownership/consigns/deny/', 'ownership_unconsigns': AppConstants.baseUrl + 'ownership/unconsigns/', 'ownership_unconsigns_request': AppConstants.baseUrl + 'ownership/unconsigns/request/', + 'ownership_unconsigns_deny': AppConstants.baseUrl + 'ownership/unconsigns/deny/', + 'ownership_loans': AppConstants.baseUrl + 'ownership/loans/', + 'ownership_loans_confirm': AppConstants.baseUrl + 'ownership/loans/confirm/', + 'ownership_loans_deny': AppConstants.baseUrl + 'ownership/loans/deny/', + 'note_notes': AppConstants.baseUrl + 'note/notes/' }; diff --git a/js/constants/application_constants.js b/js/constants/application_constants.js index 778033b4..81eadc26 100644 --- a/js/constants/application_constants.js +++ b/js/constants/application_constants.js @@ -4,7 +4,8 @@ let constants = { 'baseUrl': 'http://localhost:8000/api/', //'baseUrl': 'http://staging.ascribe.io/api/', '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; diff --git a/js/mixins/form_mixin.js b/js/mixins/form_mixin.js index 8404b5b3..770083ec 100644 --- a/js/mixins/form_mixin.js +++ b/js/mixins/form_mixin.js @@ -25,19 +25,19 @@ export const FormMixin = { this.setState({submitted: true}); this.clearErrors(); let action = (this.httpVerb && this.httpVerb()) || 'post'; - this[action](); + this[action](e); }, - post(){ + post(e){ fetch - .post(this.url(), { body: this.getFormData() }) + .post(this.url(e), { body: this.getFormData() }) .then(this.handleSuccess) .catch(this.handleError); }, - delete(){ + delete(e){ fetch - .delete(this.url()) + .delete(this.url(e)) .then(this.handleSuccess) .catch(this.handleError); }, diff --git a/js/stores/edition_list_store.js b/js/stores/edition_list_store.js index 092e498e..8773b7dc 100644 --- a/js/stores/edition_list_store.js +++ b/js/stores/edition_list_store.js @@ -16,7 +16,9 @@ class EditionListStore { this.editionList[pieceId].forEach((edition, i) => { // 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! - 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; diff --git a/js/utils/acl_utils.js b/js/utils/acl_utils.js new file mode 100644 index 00000000..a29aa7bf --- /dev/null +++ b/js/utils/acl_utils.js @@ -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); +} \ No newline at end of file