Implement support for events

This commit is contained in:
Chi Vinh Le
2017-07-21 22:54:45 +07:00
parent 0f82fa8bc5
commit cb95e9a308
11 changed files with 239 additions and 70 deletions
+25 -4
View File
@@ -5,15 +5,18 @@ import {ApolloProvider} from 'react-apollo';
import {checkLogin} from 'coral-framework/actions/auth';
import './graphql';
import {addExternalConfig} from 'coral-embed-stream/src/actions/config';
import {getStore, injectReducers} from 'coral-framework/services/store';
import {getStore, injectReducers, addListener} from 'coral-framework/services/store';
import {getClient} from 'coral-framework/services/client';
import pym from 'coral-framework/services/pym';
import AppRouter from './AppRouter';
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
import reducers from './reducers';
import EventEmitter from 'eventemitter2';
import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider';
const store = getStore();
const client = getClient();
const eventEmitter = new EventEmitter();
loadPluginsTranslations();
injectPluginsReducers();
@@ -42,11 +45,29 @@ if (!window.opener) {
} else {
init();
}
// Pass any events through our parent.
eventEmitter.onAny((eventName, value) => {
pym.sendMessage('eventEmitter', JSON.stringify({eventName, value}));
});
// Add a redux listener to pass through all actions to our event emitter.
addListener((action) => {
// Ignore apollo actions.
if (action.type.startsWith('APOLLO_')) {
return;
}
eventEmitter.emit(`redux.action.${action.type}`, {action});
});
}
render(
<ApolloProvider client={client} store={store}>
<AppRouter />
</ApolloProvider>
<EventEmitterProvider eventEmitter={eventEmitter}>
<ApolloProvider client={client} store={store}>
<AppRouter />
</ApolloProvider>
</EventEmitterProvider>
, document.querySelector('#talk-embed-stream-container')
);
+12
View File
@@ -3,6 +3,9 @@ import URLSearchParams from 'url-search-params';
import {buildUrl} from 'coral-framework/utils';
import queryString from 'query-string';
import EventEmitter from 'eventemitter2';
const eventEmitter = new EventEmitter();
// TODO: Styles should live in a separate file
const snackbarStyles = {
@@ -57,6 +60,10 @@ function configurePymParent(pymParent, opts) {
pymParent.sendMessage('config', JSON.stringify(config));
}
if (opts.events) {
opts.events(eventEmitter);
}
pymParent.onMessage('coral-auth-changed', function(message) {
if (opts.onAuthChanged) {
opts.onAuthChanged(message ? JSON.parse(message) : null);
@@ -167,6 +174,11 @@ function configurePymParent(pymParent, opts) {
window.open(url, '_blank').focus();
});
pymParent.onMessage('eventEmitter', (raw) => {
const {eventName, value} = JSON.parse(raw);
eventEmitter.emit(eventName, value);
});
// get dimensions of viewport
const viewport = () => {
let e = window, a = 'inner';
@@ -0,0 +1,18 @@
import React from 'react';
const PropTypes = require('prop-types');
class EventEmitterProvider extends React.Component {
getChildContext() {
return {eventEmitter: this.props.eventEmitter};
}
render() {
return this.props.children;
}
}
EventEmitterProvider.childContextTypes = {
eventEmitter: PropTypes.object,
};
export default EventEmitterProvider;
+3
View File
@@ -2,3 +2,6 @@ export {default as withFragments} from './withFragments';
export {default as withMutation} from './withMutation';
export {default as withQuery} from './withQuery';
export {default as withCopyToClipboard} from './withCopyToClipboard';
export {default as withEmit} from './withEmit';
export {default as excludeIf} from './excludeIf';
export {default as connect} from './connect';
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
const PropTypes = require('prop-types');
export default (WrappedComponent) => {
class WithCopyToClipboard extends React.Component {
static contextTypes = {
eventEmitter: PropTypes.object,
};
emit = (eventName, value, context) => {
this.context.eventEmitter.emit(eventName, value, context);
};
render() {
return <WrappedComponent
{...this.props}
emit={this.emit}
/>;
}
}
return WithCopyToClipboard;
};
+9 -5
View File
@@ -6,8 +6,8 @@ import flatten from 'lodash/flatten';
import isEmpty from 'lodash/isEmpty';
import {getMutationOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry';
import {getDefinitionName, getResponseErrors} from '../utils';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
const PropTypes = require('prop-types');
class ResponseErrors extends Error {
constructor(errors) {
@@ -43,10 +43,6 @@ export default (document, config = {}) => (WrappedComponent) => {
store: PropTypes.object,
};
emit = (eventName, value, context) => {
this.context.eventEmitter.emit(eventName, value, context);
};
// Lazily resolve fragments from graphRegistry to support circular dependencies.
memoized = null;
@@ -119,13 +115,21 @@ export default (document, config = {}) => (WrappedComponent) => {
if (isEmpty(wrappedConfig.optimisticResponse)) {
delete wrappedConfig.optimisticResponse;
}
this.context.eventEmitter.emit(`mutation.${name}.begin`, {variables});
return data.mutate(wrappedConfig)
.then((res) => {
const errors = getResponseErrors(res);
if (errors) {
throw new ResponseErrors(errors);
}
this.context.eventEmitter.emit(`mutation.${name}.success`, {variables});
return Promise.resolve(res);
})
.catch((error) => {
this.context.eventEmitter.emit(`mutation.${name}.error`, {variables, error});
throw new error;
});
};
return config.props({...data, mutate});
+127 -61
View File
@@ -2,6 +2,7 @@ import * as React from 'react';
import {graphql} from 'react-apollo';
import {getQueryOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry';
import {getDefinitionName, separateDataAndRoot, getResponseErrors} from '../utils';
import PropTypes from 'prop-types';
const withSkipOnErrors = (reducer) => (prev, action, ...rest) => {
if (action.type === 'APOLLO_MUTATION_RESULT' && getResponseErrors(action.result)) {
@@ -10,76 +11,141 @@ const withSkipOnErrors = (reducer) => (prev, action, ...rest) => {
return reducer(prev, action, ...rest);
};
function networkStatusToString(networkStatus) {
switch(networkStatus) {
case 1:
return 'loading';
case 2:
return 'setVariables';
case 3:
return 'fetchMore';
case 4:
return 'refetch';
case 6:
return 'poll';
case 7:
return 'ready';
case 8:
return 'error';
}
throw new Error(`Unknown network status ${networkStatus}`);
}
/**
* Exports a HOC with the same signature as `graphql`, that will
* apply query options registered in the graphRegistry.
*/
export default (document, config = {}) => (WrappedComponent) => {
const wrappedConfig = {
...config,
options: config.options || {},
props: (args) => {
const wrappedArgs = {
...args,
data: {
...args.data,
subscribeToMore(stmArgs) {
const name = getDefinitionName(document);
// Resolve document fragments before passing it to `apollo-client`.
return args.data.subscribeToMore({
...stmArgs,
document: resolveFragments(stmArgs.document),
});
},
fetchMore(lmArgs) {
// Resolve document fragments before passing it to `apollo-client`.
return args.data.fetchMore({
...lmArgs,
query: resolveFragments(lmArgs.query),
});
},
},
};
return config.props
? config.props(wrappedArgs)
: separateDataAndRoot(wrappedArgs.data);
},
};
const wrappedOptions = (data) => {
const base = (typeof wrappedConfig.options === 'function')
? wrappedConfig.options(data)
: wrappedConfig.options;
const name = getDefinitionName(document);
const configs = getQueryOptions(name);
const reducerCallbacks =
[base.reducer || ((i) => i)]
.concat(...configs.map((cfg) => cfg.reducer))
.filter((i) => i);
const reducer = withSkipOnErrors(
reducerCallbacks.reduce(
(a, b) => (prev, ...rest) =>
b(a(prev, ...rest), ...rest),
));
return {
...base,
reducer,
return class WithQuery extends React.Component {
static contextTypes = {
eventEmitter: PropTypes.object,
};
};
let memoized = null;
const getWrapped = () => {
if (!memoized) {
memoized = graphql(resolveFragments(document), {...wrappedConfig, options: wrappedOptions})(WrappedComponent);
// Lazily resolve fragments from graphRegistry to support circular dependencies.
memoized = null;
lastNetworkStatus = null;
emitWhenNeeded(data) {
const {variables, networkStatus} = data;
if (this.lastNetworkStatus === networkStatus) {
return;
}
this.lastNetworkStatus = networkStatus;
const status = networkStatusToString(networkStatus);
this.context.eventEmitter.emit(`query.${name}.${status}`, {variables, networkStatus});
}
return memoized;
};
return (props) => {
const Wrapped = getWrapped();
return <Wrapped {...props} />;
wrappedConfig = {
...config,
options: config.options || {},
props: (args) => {
this.emitWhenNeeded(args.data);
const wrappedArgs = {
...args,
data: {
...args.data,
subscribeToMore: (stmArgs) => {
const subscrName = getDefinitionName(stmArgs.document);
this.context.eventEmitter.emit(
`query.${name}.subscribeToMore.${subscrName}`,
{variables: stmArgs.variables});
// Resolve document fragments before passing it to `apollo-client`.
return args.data.subscribeToMore({
...stmArgs,
document: resolveFragments(stmArgs.document),
onError: (err) => {
this.context.eventEmitter.emit(
`query.${name}.subscribeToMore.${subscrName}.error`,
{variables: stmArgs.variables});
if (stmArgs.onErr) {
return stmArgs.onErr(err);
}
throw err;
},
});
},
fetchMore: (lmArgs) => {
const fetchName = getDefinitionName(lmArgs.query);
this.context.eventEmitter.emit(
`query.${name}.fetchMore.${fetchName}`,
{variables: lmArgs.variables});
// Resolve document fragments before passing it to `apollo-client`.
return args.data.fetchMore({
...lmArgs,
query: resolveFragments(lmArgs.query),
});
},
},
};
return config.props
? config.props(wrappedArgs)
: separateDataAndRoot(wrappedArgs.data);
},
};
wrappedOptions = (data) => {
const base = (typeof this.wrappedConfig.options === 'function')
? this.wrappedConfig.options(data)
: this.wrappedConfig.options;
const configs = getQueryOptions(name);
const reducerCallbacks =
[base.reducer || ((i) => i)]
.concat(...configs.map((cfg) => cfg.reducer))
.filter((i) => i);
const reducer = withSkipOnErrors(
reducerCallbacks.reduce(
(a, b) => (prev, ...rest) =>
b(a(prev, ...rest), ...rest),
));
return {
...base,
reducer,
};
};
getWrapped = () => {
if (!this.memoized) {
this.memoized = graphql(
resolveFragments(document),
{...this.wrappedConfig, options: this.wrappedOptions},
)(WrappedComponent);
}
return this.memoized;
};
render() {
const Wrapped = this.getWrapped();
return <Wrapped {...this.props} />;
}
};
};
+12
View File
@@ -3,12 +3,18 @@ import thunk from 'redux-thunk';
import mainReducer from '../reducers';
import {getClient} from './client';
let listeners = [];
export function injectReducers(reducers) {
const store = getStore();
store.coralReducers = {...store.coralReducers, ...reducers};
store.replaceReducer(combineReducers(store.coralReducers));
}
export function addListener(cb) {
listeners.push(cb);
}
export function getStore() {
if (window.coralStore) {
return window.coralStore;
@@ -21,11 +27,17 @@ export function getStore() {
return next(action);
};
const customListener = () => (next) => (action) => {
listeners.forEach((cb) => {cb(action);});
return next(action);
};
const middlewares = [
applyMiddleware(
getClient().middleware(),
thunk,
apolloErrorReporter,
customListener,
),
];
+1
View File
@@ -75,6 +75,7 @@
"dotenv": "^4.0.0",
"ejs": "^2.5.6",
"env-rewrite": "^1.0.2",
"eventemitter2": "^4.1.2",
"express": "^4.15.2",
"express-session": "^1.15.1",
"file-loader": "^0.11.2",
+5
View File
@@ -30,6 +30,11 @@
asset_url: '<%= asset_url ? asset_url : '' %>',
asset_id: '<%= asset_id ? asset_id : '' %>',
auth_token: '',
events: function(events) {
events.onAny(function(eventName, data) {
console.log(eventName, data);
});
},
plugin_config: {
/**
* You can disable rendering slot components of a plugin by doing:
+4
View File
@@ -2961,6 +2961,10 @@ event-stream@~3.3.0:
stream-combiner "~0.0.4"
through "~2.3.1"
eventemitter2@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-4.1.2.tgz#0e1a8477af821a6ef3995b311bf74c23a5247f15"
eventemitter3@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"