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

Merge with master

This commit is contained in:
Brett Sun 2015-11-30 11:43:33 +01:00
commit 3aab73379a
15 changed files with 358 additions and 30 deletions

View File

@ -0,0 +1,19 @@
'use strict';
import { alt } from '../alt';
class WebhookActions {
constructor() {
this.generateActions(
'fetchWebhooks',
'successFetchWebhooks',
'fetchWebhookEvents',
'successFetchWebhookEvents',
'removeWebhook',
'successRemoveWebhook'
);
}
}
export default alt.createActions(WebhookActions);

View File

@ -44,8 +44,7 @@ let Edition = React.createClass({
actionPanelButtonListType: React.PropTypes.func,
furtherDetailsType: React.PropTypes.func,
edition: React.PropTypes.object,
loadEdition: React.PropTypes.func,
location: React.PropTypes.object
loadEdition: React.PropTypes.func
},
mixins: [History],
@ -167,8 +166,7 @@ let Edition = React.createClass({
pieceId={this.props.edition.parent}
extraData={this.props.edition.extra_data}
otherData={this.props.edition.other_data}
handleSuccess={this.props.loadEdition}
location={this.props.location} />
handleSuccess={this.props.loadEdition} />
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('SPOOL Details')}>

View File

@ -19,8 +19,7 @@ let EditionContainer = React.createClass({
propTypes: {
actionPanelButtonListType: React.PropTypes.func,
furtherDetailsType: React.PropTypes.func,
params: React.PropTypes.object,
location: React.PropTypes.object
params: React.PropTypes.object
},
getInitialState() {
@ -69,7 +68,7 @@ let EditionContainer = React.createClass({
},
render() {
if(this.state.edition && this.state.edition.title) {
if(this.state.edition && this.state.edition.id) {
setDocumentTitle([this.state.edition.artist_name, this.state.edition.title].join(', '));
return (
@ -77,8 +76,7 @@ let EditionContainer = React.createClass({
actionPanelButtonListType={this.props.actionPanelButtonListType}
furtherDetailsType={this.props.furtherDetailsType}
edition={this.state.edition}
loadEdition={this.loadEdition}
location={this.props.location}/>
loadEdition={this.loadEdition} />
);
} else {
return (

View File

@ -23,8 +23,7 @@ let FurtherDetails = React.createClass({
pieceId: React.PropTypes.number,
extraData: React.PropTypes.object,
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
handleSuccess: React.PropTypes.func,
location: React.PropTypes.object
handleSuccess: React.PropTypes.func
},
getInitialState() {
@ -86,8 +85,7 @@ let FurtherDetails = React.createClass({
overrideForm={true}
pieceId={this.props.pieceId}
otherData={this.props.otherData}
multiple={true}
location={this.props.location}/>
multiple={true} />
</Form>
</Col>
</Row>

View File

@ -51,8 +51,7 @@ import { setDocumentTitle } from '../../utils/dom_utils';
let PieceContainer = React.createClass({
propTypes: {
furtherDetailsType: React.PropTypes.func,
params: React.PropTypes.object,
location: React.PropTypes.object
params: React.PropTypes.object
},
mixins: [History],
@ -233,7 +232,7 @@ let PieceContainer = React.createClass({
},
render() {
if(this.state.piece && this.state.piece.title) {
if (this.state.piece && this.state.piece.id) {
let FurtherDetailsType = this.props.furtherDetailsType;
setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', '));
@ -300,8 +299,7 @@ let PieceContainer = React.createClass({
pieceId={this.state.piece.id}
extraData={this.state.piece.extra_data}
otherData={this.state.piece.other_data}
handleSuccess={this.loadPiece}
location={this.props.location}/>
handleSuccess={this.loadPiece} />
</CollapsibleParagraph>
</Piece>

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import { History } from 'react-router';
import { History, RouteContext } from 'react-router';
import UserStore from '../../../stores/user_store';
import UserActions from '../../../actions/user_actions';
@ -37,7 +37,9 @@ export default function AuthProxyHandler({to, when}) {
location: object
},
mixins: [History],
// We need insert `RouteContext` here in order to be able
// to use the `Lifecycle` widget in further down nested components
mixins: [History, RouteContext],
getInitialState() {
return UserStore.getState();

View File

@ -11,6 +11,7 @@ import WhitelabelActions from '../../actions/whitelabel_actions';
import AccountSettings from './account_settings';
import BitcoinWalletSettings from './bitcoin_wallet_settings';
import APISettings from './api_settings';
import WebhookSettings from './webhook_settings';
import AclProxy from '../acl_proxy';
@ -70,6 +71,7 @@ let SettingsContainer = React.createClass({
aclName="acl_view_settings_api">
<APISettings />
</AclProxy>
<WebhookSettings />
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_view_settings_bitcoin">

View File

@ -0,0 +1,165 @@
'use strict';
import React from 'react';
import WebhookStore from '../../stores/webhook_store';
import WebhookActions from '../../actions/webhook_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import Form from '../ascribe_forms/form';
import Property from '../ascribe_forms/property';
import AclProxy from '../acl_proxy';
import ActionPanel from '../ascribe_panel/action_panel';
import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph';
import ApiUrls from '../../constants/api_urls';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
let WebhookSettings = React.createClass({
propTypes: {
defaultExpanded: React.PropTypes.bool
},
getInitialState() {
return WebhookStore.getState();
},
componentDidMount() {
WebhookStore.listen(this.onChange);
WebhookActions.fetchWebhooks();
WebhookActions.fetchWebhookEvents();
},
componentWillUnmount() {
WebhookStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
onRemoveWebhook(webhookId) {
return (event) => {
WebhookActions.removeWebhook(webhookId);
let notification = new GlobalNotificationModel(getLangText('Webhook deleted'), 'success', 2000);
GlobalNotificationActions.appendGlobalNotification(notification);
};
},
handleCreateSuccess() {
this.refs.webhookCreateForm.reset();
WebhookActions.fetchWebhooks(true);
let notification = new GlobalNotificationModel(getLangText('Webhook successfully created'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getWebhooks(){
let content = <AscribeSpinner color='dark-blue' size='lg'/>;
if (this.state.webhooks) {
content = this.state.webhooks.map(function(webhook, i) {
const event = webhook.event.split('.')[0];
return (
<ActionPanel
name={webhook.event}
key={i}
content={
<div>
<div className='ascribe-panel-title'>
{event.toUpperCase()}
</div>
<div className="ascribe-panel-subtitle">
{webhook.target}
</div>
</div>
}
buttons={
<div className="pull-right">
<div className="pull-right">
<button
className="pull-right btn btn-tertiary btn-sm"
onClick={this.onRemoveWebhook(webhook.id)}>
{getLangText('DELETE')}
</button>
</div>
</div>
}/>
);
}, this);
}
return content;
},
getEvents() {
if (this.state.webhookEvents && this.state.webhookEvents.length) {
return (
<Property
name='event'
label={getLangText('Select the event to trigger a webhook', '...')}>
<select name="events">
{this.state.webhookEvents.map((event, i) => {
return (
<option
name={i}
key={i}
value={ event + '.webhook' }>
{ event.toUpperCase() }
</option>
);
})}
</select>
</Property>);
}
return null;
},
render() {
return (
<CollapsibleParagraph
title={getLangText('Webhooks')}
defaultExpanded={this.props.defaultExpanded}>
<div>
<p>
Webhooks allow external services to receive notifications from ascribe.
Currently we support webhook notifications when someone transfers, consigns, loans or shares
(by email) a work to you.
</p>
<p>
To get started, simply choose the prefered action that you want to be notified upon and supply
a target url.
</p>
</div>
<AclProxy
show={this.state.webhookEvents && this.state.webhookEvents.length}>
<Form
ref="webhookCreateForm"
url={ApiUrls.webhooks}
handleSuccess={this.handleCreateSuccess}>
{ this.getEvents() }
<Property
name='target'
label={getLangText('Redirect Url')}>
<input
type="text"
placeholder={getLangText('Enter the url to be triggered')}
required/>
</Property>
<hr />
</Form>
</AclProxy>
{this.getWebhooks()}
</CollapsibleParagraph>
);
}
});
export default WebhookSettings;

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react/addons';
import { History } from 'react-router';
import { History, Lifecycle } from 'react-router';
import SlidesContainerBreadcrumbs from './slides_container_breadcrumbs';
@ -17,14 +17,16 @@ const SlidesContainer = React.createClass({
pending: string,
complete: string
}),
location: object
location: object,
pageExitWarning: string
},
mixins: [History],
mixins: [History, Lifecycle],
getInitialState() {
return {
containerWidth: 0
containerWidth: 0,
pageExitWarning: null
};
},
@ -41,6 +43,10 @@ const SlidesContainer = React.createClass({
window.removeEventListener('resize', this.handleContainerResize);
},
routerWillLeave() {
return this.props.pageExitWarning;
},
handleContainerResize() {
this.setState({
// +30 to get rid of the padding of the container which is 15px + 15px left and right

View File

@ -32,6 +32,7 @@ import ApiUrls from '../../../../../constants/api_urls';
import { mergeOptions } from '../../../../../utils/general_utils';
import { getLangText } from '../../../../../utils/lang_utils';
let IkonotvRegisterPiece = React.createClass({
propTypes: {
handleSuccess: React.PropTypes.func,
@ -47,7 +48,8 @@ let IkonotvRegisterPiece = React.createClass({
PieceListStore.getState(),
PieceStore.getState(),
{
step: 0
step: 0,
pageExitWarning: getLangText("If you leave this form now, your work will not be loaned to Ikono TV.")
});
},
@ -94,7 +96,6 @@ let IkonotvRegisterPiece = React.createClass({
handleRegisterSuccess(response){
this.refreshPieceList();
// also start loading the piece for the next step
@ -108,7 +109,6 @@ let IkonotvRegisterPiece = React.createClass({
this.incrementStep();
this.refs.slidesContainer.nextSlide();
}
},
handleAdditionalDataSuccess() {
@ -126,6 +126,8 @@ let IkonotvRegisterPiece = React.createClass({
},
handleLoanSuccess(response) {
this.setState({ pageExitWarning: null });
let notification = new GlobalNotificationModel(response.notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
@ -238,6 +240,8 @@ let IkonotvRegisterPiece = React.createClass({
},
render() {
const { pageExitWarning } = this.state;
return (
<SlidesContainer
ref="slidesContainer"
@ -246,7 +250,8 @@ let IkonotvRegisterPiece = React.createClass({
pending: 'glyphicon glyphicon-chevron-right',
completed: 'glyphicon glyphicon-lock'
}}
location={this.props.location}>
location={this.props.location}
pageExitWarning={pageExitWarning}>
<div data-slide-title={getLangText('Register work')}>
<Row className="no-margin">
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>

View File

@ -72,6 +72,9 @@ let ApiUrls = {
'users_username': AppConstants.apiEndpoint + 'users/username/',
'users_profile': AppConstants.apiEndpoint + 'users/profile/',
'wallet_settings': AppConstants.apiEndpoint + 'users/wallet_settings/',
'webhook': AppConstants.apiEndpoint + 'webhooks/${webhook_id}/',
'webhooks': AppConstants.apiEndpoint + 'webhooks/',
'webhooks_events': AppConstants.apiEndpoint + 'webhooks/events/',
'whitelabel_settings': AppConstants.apiEndpoint + 'whitelabel/settings/${subdomain}/',
'delete_s3_file': AppConstants.serverUrl + 's3/delete/',
'prize_list': AppConstants.apiEndpoint + 'prize/'

View File

@ -0,0 +1,46 @@
'use strict';
import requests from '../utils/requests';
import WebhookActions from '../actions/webhook_actions';
const WebhookSource = {
lookupWebhooks: {
remote() {
return requests.get('webhooks');
},
local(state) {
return state.webhooks && !Object.keys(state.webhooks).length ? state : {};
},
success: WebhookActions.successFetchWebhooks,
error: WebhookActions.errorWebhooks,
shouldFetch(state) {
return state.webhookMeta.invalidateCache || state.webhooks && !Object.keys(state.webhooks).length;
}
},
lookupWebhookEvents: {
remote() {
return requests.get('webhooks_events');
},
local(state) {
return state.webhookEvents && !Object.keys(state.webhookEvents).length ? state : {};
},
success: WebhookActions.successFetchWebhookEvents,
error: WebhookActions.errorWebhookEvents,
shouldFetch(state) {
return state.webhookEventsMeta.invalidateCache || state.webhookEvents && !Object.keys(state.webhookEvents).length;
}
},
performRemoveWebhook: {
remote(state) {
return requests.delete('webhook', {'webhook_id': state.webhookMeta.idToDelete });
},
success: WebhookActions.successRemoveWebhook,
error: WebhookActions.errorWebhooks
}
};
export default WebhookSource;

View File

@ -0,0 +1,88 @@
'use strict';
import { alt } from '../alt';
import WebhookActions from '../actions/webhook_actions';
import WebhookSource from '../sources/webhook_source';
class WebhookStore {
constructor() {
this.webhooks = [];
this.webhookEvents = [];
this.webhookMeta = {
invalidateCache: false,
err: null,
idToDelete: null
};
this.webhookEventsMeta = {
invalidateCache: false,
err: null
};
this.bindActions(WebhookActions);
this.registerAsync(WebhookSource);
}
onFetchWebhooks(invalidateCache) {
this.webhookMeta.invalidateCache = invalidateCache;
this.getInstance().lookupWebhooks();
}
onSuccessFetchWebhooks({ webhooks }) {
this.webhookMeta.invalidateCache = false;
this.webhookMeta.err = null;
this.webhooks = webhooks;
this.webhookEventsMeta.invalidateCache = true;
this.getInstance().lookupWebhookEvents();
}
onFetchWebhookEvents(invalidateCache) {
this.webhookEventsMeta.invalidateCache = invalidateCache;
this.getInstance().lookupWebhookEvents();
}
onSuccessFetchWebhookEvents({ events }) {
this.webhookEventsMeta.invalidateCache = false;
this.webhookEventsMeta.err = null;
// remove all events that have already been used.
const usedEvents = this.webhooks
.reduce((tempUsedEvents, webhook) => {
tempUsedEvents.push(webhook.event.split('.')[0]);
return tempUsedEvents;
}, []);
this.webhookEvents = events.filter((event) => {
return usedEvents.indexOf(event) === -1;
});
}
onRemoveWebhook(id) {
this.webhookMeta.invalidateCache = true;
this.webhookMeta.idToDelete = id;
if(!this.getInstance().isLoading()) {
this.getInstance().performRemoveWebhook();
}
}
onSuccessRemoveWebhook() {
this.webhookMeta.idToDelete = null;
if(!this.getInstance().isLoading()) {
this.getInstance().lookupWebhooks();
}
}
onErrorWebhooks(err) {
console.logGlobal(err);
this.webhookMeta.err = err;
}
onErrorWebhookEvents(err) {
console.logGlobal(err);
this.webhookEventsMeta.err = err;
}
}
export default alt.createStore(WebhookStore, 'WebhookStore');

View File

@ -66,7 +66,7 @@
"gulp-uglify": "^1.2.0",
"gulp-util": "^3.0.4",
"harmonize": "^1.4.2",
"history": "^1.11.1",
"history": "^1.13.1",
"invariant": "^2.1.1",
"isomorphic-fetch": "^2.0.2",
"jest-cli": "^0.4.0",
@ -80,7 +80,7 @@
"react": "0.13.2",
"react-bootstrap": "0.25.1",
"react-datepicker": "^0.12.0",
"react-router": "^1.0.0-rc3",
"react-router": "1.0.0",
"react-router-bootstrap": "^0.19.0",
"react-star-rating": "~1.3.2",
"react-textarea-autosize": "^2.5.2",