diff --git a/.gitignore~ b/.gitignore~ new file mode 100644 index 00000000..41b217c2 --- /dev/null +++ b/.gitignore~ @@ -0,0 +1,24 @@ +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz +*.sublime-project +.idea +spool-project.sublime-project +*.sublime-workspace +*.sublime-workspace +webapp-dependencies.txt + +pids +logs +results +README.md~ +node_modules/* + +build + +.DS_Store diff --git a/js/actions/contract_actions.js b/js/actions/contract_actions.js index e14dbf83..c6055ffc 100644 --- a/js/actions/contract_actions.js +++ b/js/actions/contract_actions.js @@ -43,6 +43,7 @@ class ContractActions { /* No email was entered - Ignore and keep going*/ } } + } export default alt.createActions(ContractActions); diff --git a/js/actions/contract_list_actions.js b/js/actions/contract_list_actions.js index f4257d37..b019ecc0 100644 --- a/js/actions/contract_list_actions.js +++ b/js/actions/contract_list_actions.js @@ -3,7 +3,6 @@ import alt from '../alt'; import OwnershipFetcher from '../fetchers/ownership_fetcher'; - class ContractListActions { constructor() { this.generateActions( @@ -15,13 +14,23 @@ class ContractListActions { fetchContractList() { OwnershipFetcher.fetchContractList() .then((contracts) => { - this.actions.updateContractList(contracts); + this.actions.updateContractList(contracts.results); }) .catch((err) => { console.logGlobal(err); this.actions.updateContractList([]); }); } + + makeContractPublic(contract){ + OwnershipFetcher.makeContractPublic(contract) + .then((res) =>{ + return res; + }) + .catch((err)=>{ + console.logGlobal(err); + }); + } } export default alt.createActions(ContractListActions); diff --git a/js/actions/piece_list_actions.js b/js/actions/piece_list_actions.js index 2fd15c04..ae5ac090 100644 --- a/js/actions/piece_list_actions.js +++ b/js/actions/piece_list_actions.js @@ -57,7 +57,7 @@ class PieceListActions { PieceListFetcher .fetchRequestActions() .then((res) => { - this.actions.updatePieceListRequestActions(res.piece_ids); + this.actions.updatePieceListRequestActions(res); }) .catch((err) => console.logGlobal(err)); } diff --git a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js index f7bca334..178a7db4 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -61,12 +61,13 @@ let AccordionListItemWallet = React.createClass({ }, getGlyphicon(){ - if (this.props.content.requestAction && this.props.content.requestAction.length > 0) { + if ((this.props.content.request_action && this.props.content.request_action.length > 0) || + (this.props.content.request_action_editions)){ return ( {getLangText('You have actions pending in one of your editions')}}> + overlay={{getLangText('You have actions pending')}}> ); } diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 696e5057..49175851 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -305,7 +305,6 @@ let EditionSummary = React.createClass({
); - } }); diff --git a/js/components/ascribe_detail/edition_container.js b/js/components/ascribe_detail/edition_container.js index 15086434..78b1e477 100644 --- a/js/components/ascribe_detail/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -9,6 +9,8 @@ import Edition from './edition'; import AppConstants from '../../constants/application_constants'; + + /** * This is the component that implements resource/data specific functionality */ @@ -34,6 +36,15 @@ let EditionContainer = React.createClass({ EditionActions.fetchOne(this.props.params.editionId); }, + // This is done to update the container when the user clicks on the prev or next + // button to update the URL parameter (and therefore to switch pieces) + componentWillReceiveProps(nextProps) { + if(this.props.params.editionId !== nextProps.params.editionId) { + EditionActions.updateEdition({}); + EditionActions.fetchOne(nextProps.params.editionId); + } + }, + componentWillUnmount() { // Every time we're leaving the edition detail page, // just reset the edition that is saved in the edition store @@ -50,6 +61,7 @@ let EditionContainer = React.createClass({ }, render() { + console.log(this.state); if('title' in this.state.edition) { return ( 0) { + if (this.state.contractList && this.state.contractList.count > 0) { + let contractList = this.state.contractList.results; return ( {getLangText('Learn more')} }> - - + handleSuccess={this.handleSuccess} /> ); } else if(this.props.requestAction === 'loan_request') { return ( @@ -110,7 +118,7 @@ let RequestActionForm = React.createClass({ buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px' pieceOrEditions={this.props.pieceOrEditions} currentUser={this.props.currentUser} - handleSuccess={this.props.handleSuccess} /> + handleSuccess={this.handleSuccess} /> ); } else { return ( diff --git a/js/components/ascribe_panel/action_panel.js b/js/components/ascribe_panel/action_panel.js index 148ea03d..3146f1ba 100644 --- a/js/components/ascribe_panel/action_panel.js +++ b/js/components/ascribe_panel/action_panel.js @@ -14,7 +14,6 @@ let ActionPanel = React.createClass({ onClick: React.PropTypes.func, ignoreFocus: React.PropTypes.bool }, - getInitialState() { return { isFocused: false diff --git a/js/components/ascribe_settings/contract_settings.js b/js/components/ascribe_settings/contract_settings.js index b98e9fe0..6b4c624e 100644 --- a/js/components/ascribe_settings/contract_settings.js +++ b/js/components/ascribe_settings/contract_settings.js @@ -5,22 +5,99 @@ import React from 'react'; import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph'; import CreateContractForm from '../ascribe_forms/form_create_contract'; -import { getLangText } from '../../utils/lang_utils'; +import ContractListStore from '../../stores/contract_list_store'; +import ContractListActions from '../../actions/contract_list_actions'; +import ActionPanel from '../ascribe_panel/action_panel'; + +import { getLangText } from '../../utils/lang_utils'; let ContractSettings = React.createClass({ propTypes: { defaultExpanded: React.PropTypes.bool }, - + getInitialState(){ + return ContractListStore.getState(); + }, + componentDidMount() { + ContractListStore.listen(this.onChange); + ContractListActions.fetchContractList(); + }, + componentWillUnmount() { + ContractListStore.unlisten(this.onChange); + }, + onChange(state) { + this.setState(state); + }, + makeContractPublic(contract){ + console.log(contract); + ContractListActions.makeContractPublic(contract) + .then(( ) => ContractListActions.fetchContractList()) + .catch((error)=>{console.log("Error ", error)}) + }, + getPublicContracts(){ + return this.state.contractList.filter((contract) => contract.public); + }, + getPrivateContracts(){ + return this.state.contractList.filter((contract) => !contract.public); + }, + getblobEndName(contract){ + return contract.blob.match(/.*\/(.*)/)[1]; + }, render() { + let publicContracts = this.getPublicContracts(); + let privateContracts = this.getPrivateContracts(); + console.log(this.state.contractList); return ( + defaultExpanded={false}> {/* this should be this.props.defaultExpanded */} - + + {
+

Public Contracts

+ {(publicContracts.length > 0) ? + publicContracts.map( + (contract) => { + return( + + + + } + />) + } + ) : null } +
} + + {
+

Private Contracts

+ {(privateContracts.length>0) ? + privateContracts.map( + (contract) => { + return( + + + } + />) + } + ) : null} +
} +
+ + +
); } diff --git a/js/components/global_action.js b/js/components/global_action.js new file mode 100644 index 00000000..80df0c75 --- /dev/null +++ b/js/components/global_action.js @@ -0,0 +1,43 @@ +'use strict'; + +import React from 'react'; + +let GlobalAction = React.createClass({ + propTypes: { + requestActions: React.PropTypes.object + }, + + render() { + let pieceActions = null; + if (this.props.requestActions && this.props.requestActions.pieces){ + pieceActions = this.props.requestActions.pieces.map((item) => { + return ( +
+ {item} +
); + }); + } + let editionActions = null; + if (this.props.requestActions && this.props.requestActions.editions){ + editionActions = Object.keys(this.props.requestActions.editions).map((pieceId) => { + return this.props.requestActions.editions[pieceId].map((item) => { + return ( +
+ {item} +
); + }); + }); + } + + if (pieceActions || editionActions) { + return ( +
+ {pieceActions} + {editionActions} +
); + } + return null; + } +}); + +export default GlobalAction; \ No newline at end of file diff --git a/js/components/header.js b/js/components/header.js index 0863624f..dcf3c475 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -3,12 +3,6 @@ import React from 'react'; import Router from 'react-router'; -import UserActions from '../actions/user_actions'; -import UserStore from '../stores/user_store'; - -import WhitelabelActions from '../actions/whitelabel_actions'; -import WhitelabelStore from '../stores/whitelabel_store'; -import EventActions from '../actions/event_actions'; import Nav from 'react-bootstrap/lib/Nav'; import Navbar from 'react-bootstrap/lib/Navbar'; @@ -18,6 +12,15 @@ import MenuItem from 'react-bootstrap/lib/MenuItem'; import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink'; import NavItemLink from 'react-router-bootstrap/lib/NavItemLink'; +import UserActions from '../actions/user_actions'; +import UserStore from '../stores/user_store'; + +import WhitelabelActions from '../actions/whitelabel_actions'; +import WhitelabelStore from '../stores/whitelabel_store'; +import EventActions from '../actions/event_actions'; + +import HeaderNotifications from './header_notification'; + import HeaderNotificationDebug from './header_notification_debug'; import NavRoutesLinks from './nav_routes_links'; @@ -41,7 +44,10 @@ let Header = React.createClass({ }, getInitialState() { - return mergeOptions(WhitelabelStore.getState(), UserStore.getState()); + return mergeOptions( + WhitelabelStore.getState(), + UserStore.getState() + ); }, componentDidMount() { @@ -96,11 +102,13 @@ let Header = React.createClass({ let navRoutesLinks; if (this.state.currentUser.username){ account = ( - + {getLangText('Account Settings')} {getLangText('Log out')} - + ); navRoutesLinks = ; } @@ -126,6 +134,7 @@ let Header = React.createClass({ {account} {signup} + {navRoutesLinks} diff --git a/js/components/header_notification.js b/js/components/header_notification.js new file mode 100644 index 00000000..9222c0c4 --- /dev/null +++ b/js/components/header_notification.js @@ -0,0 +1,144 @@ +'use strict'; + +import React from 'react'; +import Router from 'react-router'; +import DropdownButton from 'react-bootstrap/lib/DropdownButton'; +import Glyphicon from 'react-bootstrap/lib/Glyphicon'; +import MenuItem from 'react-bootstrap/lib/MenuItem'; + +import Nav from 'react-bootstrap/lib/Nav'; + +import PieceListStore from '../stores/piece_list_store'; + +import { mergeOptions } from '../utils/general_utils'; +import { getLangText } from '../utils/lang_utils'; + +let Link = Router.Link; + + +let HeaderNotifications = React.createClass({ + + getInitialState() { + return mergeOptions( + PieceListStore.getState() + ); + }, + + componentDidMount() { + PieceListStore.listen(this.onChange); + }, + + componentWillUnmount() { + PieceListStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + onSelected(event) { + /* + This is a hack to make the dropdown close after clicking on an item + The function just need to be defined + + from https://github.com/react-bootstrap/react-bootstrap/issues/368: + + @jvillasante - Have you tried to use onSelect with the DropdownButton? + I don't have a working example that is exactly like yours, + but I just noticed that the Dropdown closes when I've attached an event handler to OnSelect: + + + + onSelected: function(e) { + // doesn't need to have functionality (necessarily) ... just wired up + } + Internally, a call to DropdownButton.setDropDownState(false) is made which will hide the dropdown menu. + So, you should be able to call that directly on the DropdownButton instance as well if needed. + */ + }, + + render() { + if (this.state.requestActions && this.state.requestActions.length > 0) { + return ( + + ); + } + return null; + } +}); + +let NotificationListItem = React.createClass({ + propTypes: { + pieceOrEdition: React.PropTypes.object + }, + + getLinkData() { + + if(this.props.pieceOrEdition && this.props.pieceOrEdition.parent) { + return { + to: 'edition', + params: { + editionId: this.props.pieceOrEdition.bitcoin_id + } + }; + } else { + return { + to: 'piece', + params: { + pieceId: this.props.pieceOrEdition.id + } + }; + } + + }, + + render() { + if (this.props.pieceOrEdition) { + return ( + +
+
+
+ +
+
+
+

{this.props.pieceOrEdition.title}

+
by {this.props.pieceOrEdition.artist_name}
+
+ { + this.props.pieceOrEdition.request_action.map((requestAction) => { + return 'Pending ' + requestAction.action + ' request'; + }) + } +
+
+
+ ); + } + return null; + } +}); + +export default HeaderNotifications; diff --git a/js/components/piece_list.js b/js/components/piece_list.js index 3ecdd135..d841f7af 100644 --- a/js/components/piece_list.js +++ b/js/components/piece_list.js @@ -15,6 +15,7 @@ import AccordionListItemTableEditions from './ascribe_accordion_list/accordion_l import Pagination from './ascribe_pagination/pagination'; +import GlobalAction from './global_action'; import PieceListBulkModal from './ascribe_piece_list_bulk_modal/piece_list_bulk_modal'; import PieceListToolbar from './ascribe_piece_list_toolbar/piece_list_toolbar'; @@ -148,6 +149,10 @@ let PieceList = React.createClass({ render() { let loadingElement = (); let AccordionListItemType = this.props.accordionListItemType; + + // + + return (
+ ); } }); diff --git a/js/constants/api_urls.js b/js/constants/api_urls.js index b056ee03..bad04be5 100644 --- a/js/constants/api_urls.js +++ b/js/constants/api_urls.js @@ -27,6 +27,7 @@ let ApiUrls = { 'note_private_piece': AppConstants.apiEndpoint + 'note/private/pieces/', 'note_public_edition': AppConstants.apiEndpoint + 'note/public/editions/', 'note_public_piece': AppConstants.apiEndpoint + 'note/public/pieces/', + 'ownership_contract_agreements': AppConstants.apiEndpoint + 'ownership/contract_agreements/', 'ownership_consigns': AppConstants.apiEndpoint + 'ownership/consigns/', 'ownership_consigns_confirm': AppConstants.apiEndpoint + 'ownership/consigns/confirm/', 'ownership_consigns_deny': AppConstants.apiEndpoint + 'ownership/consigns/deny/', @@ -46,7 +47,8 @@ let ApiUrls = { 'ownership_unconsigns': AppConstants.apiEndpoint + 'ownership/unconsigns/', 'ownership_unconsigns_deny': AppConstants.apiEndpoint + 'ownership/unconsigns/deny/', 'ownership_unconsigns_request': AppConstants.apiEndpoint + 'ownership/unconsigns/request/', - 'ownership_contract': AppConstants.apiEndpoint + 'ownership/contracts/', + 'ownership_contract':AppConstants.apiEndpoint + 'ownership/contracts/${contract_id}', + "ownership_contract_list": AppConstants.apiEndpoint + 'ownership/contracts/', 'piece': AppConstants.apiEndpoint + 'pieces/${piece_id}/', 'piece_extradata': AppConstants.apiEndpoint + 'pieces/${piece_id}/extradata/', 'piece_first_edition_id': AppConstants.apiEndpoint + 'pieces/${piece_id}/edition_index/', diff --git a/js/fetchers/ownership_fetcher.js b/js/fetchers/ownership_fetcher.js index 051f2d08..8f97add5 100644 --- a/js/fetchers/ownership_fetcher.js +++ b/js/fetchers/ownership_fetcher.js @@ -16,11 +16,16 @@ let OwnershipFetcher = { * Fetch the contracts of the logged-in user from the API. */ fetchContractList(){ - return requests.get(ApiUrls.ownership_contract); + return requests.get(ApiUrls.ownership_contract_list); }, fetchLoanPieceRequestList(){ return requests.get(ApiUrls.ownership_loans_pieces_request); + }, + + makeContractPublic(contractObj){ + console.log(contractObj); + return requests.put('ownership_contract_list',{ body: contractObj, contract_id:contractObj.id }); } }; diff --git a/js/stores/piece_list_store.js b/js/stores/piece_list_store.js index bc2bba24..8b4254ac 100644 --- a/js/stores/piece_list_store.js +++ b/js/stores/piece_list_store.js @@ -28,6 +28,7 @@ class PieceListStore { this.orderBy = 'artist_name'; this.orderAsc = true; this.filterBy = {}; + this.requestActions = {}; this.bindActions(PieceListActions); } @@ -71,10 +72,8 @@ class PieceListStore { this.pieceList = pieceList; } - onUpdatePieceListRequestActions(requestActions) { - this.pieceList.forEach((piece) => { - piece.requestAction = requestActions.indexOf(piece.id) > -1; - }); + onUpdatePieceListRequestActions(res) { + this.requestActions = res.actions; } onUpdatePropertyForPiece({pieceId, key, value}) { diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index 15b0e85f..673a5509 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -52,6 +52,19 @@ export function sumNumList(l) { return sum; } +export function excludePropFromObject(obj, propList){ + let clonedObj = mergeOptions({},obj); + for (let item in propList){ + console.log(item); + if (clonedObj[propList[item]]){ + console.log('deleting... '); + delete clonedObj[propList[item]]; + } + } + console.log(clonedObj); + return clonedObj; +} + /* Taken from http://stackoverflow.com/a/4795914/1263876 Behaves like C's format string function diff --git a/js/utils/requests.js b/js/utils/requests.js index 793e1f21..3cf84690 100644 --- a/js/utils/requests.js +++ b/js/utils/requests.js @@ -6,6 +6,7 @@ import { argsToQueryParams, getCookie } from '../utils/fetch_api_utils'; import AppConstants from '../constants/application_constants'; +import {excludePropFromObject} from '../utils/general_utils'; class Requests { _merge(defaults, options) { @@ -137,15 +138,25 @@ class Requests { return this.request('delete', newUrl); } - post(url, params) { - let paramsCopy = this._merge(params); - let newUrl = this.prepareUrl(url, paramsCopy); + _putOrPost(url,paramsAndBody,method){ + let paramsCopy = this._merge(paramsAndBody); + let params = excludePropFromObject(paramsAndBody,['body']); + let newUrl = this.prepareUrl(url, params); let body = null; - if (paramsCopy && paramsCopy.body) { + console.log(paramsCopy.body); body = JSON.stringify(paramsCopy.body); } - return this.request('post', newUrl, { body }); + return this.request(method, newUrl, { body }); + } + + post(url, params) { + return this._putOrPost(url,params,'post') + } + + put(url, params){ + console.log(params); + return this._putOrPost(url,params,'put') } defaults(options) { diff --git a/sass/ascribe_accordion_list.scss b/sass/ascribe_accordion_list.scss index 4b4186eb..6412a598 100644 --- a/sass/ascribe_accordion_list.scss +++ b/sass/ascribe_accordion_list.scss @@ -166,7 +166,7 @@ $ascribe-accordion-list-item-height: 8em; .request-action-badge { color: $ascribe-color-green; font-size: 1.2em; - padding: .3em; + padding: .8em; position: absolute; right: 0; top: 0; diff --git a/sass/ascribe_global_action.scss b/sass/ascribe_global_action.scss new file mode 100644 index 00000000..af4eca4a --- /dev/null +++ b/sass/ascribe_global_action.scss @@ -0,0 +1,25 @@ +$break-small: 764px; +$break-medium: 991px; +$break-medium: 1200px; + +.ascribe-global-action-wrapper { + position: fixed; + width: 100%; + max-width: 500px; + height:3.5em; + left:0; + right: 0; + top:0; + z-index: 2000; + display:table; + margin: 1px auto; +} + +.ascribe-global-action { + text-align: center; + padding: 1em; + color: black; + border: 1px solid #cccccc; + background-color: white; + margin-top: 1px; +} \ No newline at end of file diff --git a/sass/ascribe_notification_list.scss b/sass/ascribe_notification_list.scss new file mode 100644 index 00000000..bd3c0b20 --- /dev/null +++ b/sass/ascribe_notification_list.scss @@ -0,0 +1,65 @@ +$break-small: 764px; +$break-medium: 991px; +$break-medium: 1200px; + +.notification-wrapper { + width: 350px; + height:8em; + padding: 0.3em; + border-bottom: 1px solid #cccccc; + margin: -3px -20px; + + // ToDo: Include media queries for thumbnail + .thumbnail-wrapper { + width: 7.4em; + height: 7.4em; + padding:0; + cursor: pointer; + text-align: center; + img { + max-width: 100%; + max-height: 100%; + } + &::before { + content: ' '; + display: inline-block; + vertical-align: middle; /* vertical alignment of the inline element */ + height: 100%; + } + } + h1 { + margin-top: 0.3em; + margin-bottom: 0.15em; + font-size: 1.8em; + } + .sub-header{ + margin-bottom: 1em; + } + .notification-action{ + color: $ascribe-color-green; + } +} + +.notification-menu { + .dropdown-menu { + padding: 0 !important; + li a { + padding-top: 0; + } + } +} + +.notification-amount { + padding: 0.3em; + font-size: 1.2em; + +} + +.ascribe-global-action { + text-align: center; + padding: 1em; + color: black; + border: 1px solid #cccccc; + background-color: white; + margin-top: 1px; +} \ No newline at end of file diff --git a/sass/main.scss b/sass/main.scss index 6ec1d37d..fb71ff2a 100644 --- a/sass/main.scss +++ b/sass/main.scss @@ -22,7 +22,9 @@ $BASE_URL: '<%= BASE_URL %>'; @import 'ascribe_media_player'; @import 'ascribe_uploader'; @import 'ascribe_footer'; +@import 'ascribe_global_action'; @import 'ascribe_global_notification'; +@import 'ascribe_notification_list'; @import 'ascribe_piece_register'; @import 'offset_right'; @import 'ascribe_settings';