From 84e4091ea6eea48ccf0e38a0f53399ba65bf5b2b Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 21 Aug 2017 20:06:53 +0700 Subject: [PATCH 01/16] Implement TalkProvider --- client/coral-admin/src/index.js | 18 ++++++----- .../src/containers/Embed.js | 9 ++++-- client/coral-embed-stream/src/index.js | 18 ++++++----- .../components/ClickOutside.js | 15 ++++++++-- .../components/EventEmitterProvider.js | 18 ----------- .../components/TalkProvider.js | 30 +++++++++++++++++++ 6 files changed, 70 insertions(+), 38 deletions(-) delete mode 100644 client/coral-framework/components/EventEmitterProvider.js create mode 100644 client/coral-framework/components/TalkProvider.js diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index 14437c51b..ed9d537c0 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -1,9 +1,8 @@ import React from 'react'; import {render} from 'react-dom'; -import {ApolloProvider} from 'react-apollo'; import smoothscroll from 'smoothscroll-polyfill'; import EventEmitter from 'eventemitter2'; -import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider'; +import TalkProvider from 'coral-framework/components/TalkProvider'; import {getClient} from 'coral-framework/services/client'; import store from './services/store'; @@ -13,8 +12,10 @@ import App from './components/App'; import 'react-mdl/extra/material.js'; import './graphql'; import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins'; +import plugins from 'pluginsConfig'; const eventEmitter = new EventEmitter(); +const client = getClient(); // TODO: pass redux actions through the emitter. @@ -23,10 +24,13 @@ injectPluginsReducers(); smoothscroll.polyfill(); render( - - - - - + + + , document.querySelector('#root') ); diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index e21a683d1..6eb0d9f1c 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -10,20 +10,23 @@ import renderComponent from 'recompose/renderComponent'; import {Spinner} from 'coral-ui'; import * as authActions from 'coral-framework/actions/auth'; import * as assetActions from 'coral-framework/actions/asset'; -import pym from 'coral-framework/services/pym'; import {getDefinitionName} from 'coral-framework/utils'; import {withQuery} from 'coral-framework/hocs'; import Embed from '../components/Embed'; import Stream from './Stream'; import {addNotification} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; - +import PropTypes from 'prop-types'; import {setActiveTab} from '../actions/embed'; const {logout, checkLogin, focusSignInDialog, blurSignInDialog, hideSignInDialog} = authActions; const {fetchAssetSuccess} = assetActions; class EmbedContainer extends React.Component { + static contextTypes = { + pym: PropTypes.object, + }; + subscriptions = []; subscribeToUpdates(props = this.props) { @@ -95,7 +98,7 @@ class EmbedContainer extends React.Component { if (!get(prevProps, 'root.asset.comment') && get(this.props, 'root.asset.comment')) { // Scroll to a permalinked comment if one is in the URL once the page is done rendering. - setTimeout(() => pym.scrollParentToChildEl('talk-embed-stream-container'), 0); + setTimeout(() => this.context.pym.scrollParentToChildEl('talk-embed-stream-container'), 0); } } diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index b8b7804dc..e973ca0d4 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -1,6 +1,5 @@ import React from 'react'; import {render} from 'react-dom'; -import {ApolloProvider} from 'react-apollo'; import {checkLogin, handleAuthToken, logout} from 'coral-framework/actions/auth'; import './graphql'; @@ -13,7 +12,8 @@ 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'; +import TalkProvider from 'coral-framework/components/TalkProvider'; +import plugins from 'pluginsConfig'; const store = getStore(); const client = getClient(); @@ -68,10 +68,14 @@ if (!window.opener) { } render( - - - - - + + + , document.querySelector('#talk-embed-stream-container') ); diff --git a/client/coral-framework/components/ClickOutside.js b/client/coral-framework/components/ClickOutside.js index 5348a9af7..054a9e6fe 100644 --- a/client/coral-framework/components/ClickOutside.js +++ b/client/coral-framework/components/ClickOutside.js @@ -1,13 +1,16 @@ import {Component, cloneElement, Children} from 'react'; import PropTypes from 'prop-types'; import {findDOMNode} from 'react-dom'; -import pym from 'coral-framework/services/pym'; export default class ClickOutside extends Component { static propTypes = { onClickOutside: PropTypes.func.isRequired }; + static contextTypes = { + pym: PropTypes.object, + }; + domNode = null; handleClick = (e) => { @@ -18,14 +21,20 @@ export default class ClickOutside extends Component { }; componentDidMount() { + const {pym} = this.context; this.domNode = findDOMNode(this); document.addEventListener('click', this.handleClick, true); - pym.onMessage('click', this.handleClick); + if (pym) { + pym.onMessage('click', this.handleClick); + } } componentWillUnmount() { + const {pym} = this.context; document.removeEventListener('click', this.handleClick, true); - pym.messageHandlers.click = pym.messageHandlers.click.filter((h) => h !== this.handleClick); + if (pym) { + pym.messageHandlers.click = pym.messageHandlers.click.filter((h) => h !== this.handleClick); + } } render() { diff --git a/client/coral-framework/components/EventEmitterProvider.js b/client/coral-framework/components/EventEmitterProvider.js deleted file mode 100644 index 36c0bccdb..000000000 --- a/client/coral-framework/components/EventEmitterProvider.js +++ /dev/null @@ -1,18 +0,0 @@ -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; diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js new file mode 100644 index 000000000..275373c26 --- /dev/null +++ b/client/coral-framework/components/TalkProvider.js @@ -0,0 +1,30 @@ +import React from 'react'; +const PropTypes = require('prop-types'); +import {ApolloProvider} from 'react-apollo'; + +class TalkProvider extends React.Component { + getChildContext() { + return { + eventEmitter: this.props.eventEmitter, + pym: this.props.pym, + plugins: this.props.plugins, + }; + } + + render() { + const {children, client, store, plugins} = this.props; + return ( + + {children} + + ); + } +} + +TalkProvider.childContextTypes = { + pym: PropTypes.object, + eventEmitter: PropTypes.object, + plugins: PropTypes.array, +}; + +export default TalkProvider; From 9566f83eac7aee9031bc8ff197ec12af1582b3e3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 21 Aug 2017 21:31:54 +0700 Subject: [PATCH 02/16] Remove coral-framework reducers Some of them were only related to the embed stream. Auth code is too convoluted with the embed stream and can't be reused atm. --- client/coral-admin/src/actions/auth.js | 29 ++++++++++++++++++- client/coral-admin/src/containers/Layout.js | 3 +- .../containers/ConfigureStreamContainer.js | 2 +- .../src}/actions/asset.js | 4 +-- .../src}/actions/auth.js | 8 ++--- .../src}/constants/asset.js | 0 .../src}/constants/auth.js | 0 .../src/containers/Embed.js | 4 +-- .../src/containers/Stream.js | 2 +- client/coral-embed-stream/src/index.js | 2 +- .../src}/reducers/asset.js | 0 .../src}/reducers/auth.js | 0 .../coral-embed-stream/src/reducers/index.js | 6 ++++ .../coral-embed-stream/src/reducers/stream.js | 2 +- client/coral-framework/reducers/index.js | 9 ------ client/coral-framework/services/store.js | 2 -- .../containers/ProfileContainer.js | 5 +++- plugin-api/beta/client/hocs/withReaction.js | 4 ++- .../client/components/ChangeUsername.js | 2 +- .../client/components/SignInButton.js | 2 +- .../client/components/SignInContainer.js | 2 +- .../client/components/UserBox.js | 2 +- 22 files changed, 58 insertions(+), 32 deletions(-) rename client/{coral-framework => coral-embed-stream/src}/actions/asset.js (94%) rename client/{coral-framework => coral-embed-stream/src}/actions/auth.js (97%) rename client/{coral-framework => coral-embed-stream/src}/constants/asset.js (100%) rename client/{coral-framework => coral-embed-stream/src}/constants/auth.js (100%) rename client/{coral-framework => coral-embed-stream/src}/reducers/asset.js (100%) rename client/{coral-framework => coral-embed-stream/src}/reducers/auth.js (100%) delete mode 100644 client/coral-framework/reducers/index.js diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 0c1264e1f..8c84a95ec 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -2,9 +2,9 @@ import bowser from 'bowser'; import * as actions from '../constants/auth'; import coralApi from 'coral-framework/helpers/request'; import * as Storage from 'coral-framework/helpers/storage'; -import {handleAuthToken} from 'coral-framework/actions/auth'; import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; +import jwtDecode from 'jwt-decode'; //============================================================================== // SIGN IN @@ -136,3 +136,30 @@ export const checkLogin = () => (dispatch) => { dispatch(checkLoginFailure(errorMessage)); }); }; + +//============================================================================== +// LOGOUT +//============================================================================== + +export const logout = () => (dispatch) => { + return coralApi('/auth', {method: 'DELETE'}).then(() => { + Storage.removeItem('token'); + + // Reset the websocket. + resetWebsocket(); + + dispatch({type: actions.LOGOUT}); + }); +}; + +//============================================================================== +// AUTH TOKEN +//============================================================================== + +export const handleAuthToken = (token) => (dispatch) => { + Storage.setItem('exp', jwtDecode(token).exp); + Storage.setItem('token', token); + + dispatch({type: 'HANDLE_AUTH_TOKEN'}); +}; + diff --git a/client/coral-admin/src/containers/Layout.js b/client/coral-admin/src/containers/Layout.js index ae022f2ef..2c5e1621a 100644 --- a/client/coral-admin/src/containers/Layout.js +++ b/client/coral-admin/src/containers/Layout.js @@ -3,12 +3,11 @@ import {connect} from 'react-redux'; import Layout from '../components/ui/Layout'; import {fetchConfig} from '../actions/config'; import AdminLogin from '../components/AdminLogin'; -import {logout} from 'coral-framework/actions/auth'; import {FullLoading} from '../components/FullLoading'; import BanUserDialog from './BanUserDialog'; import SuspendUserDialog from './SuspendUserDialog'; import {toggleModal as toggleShortcutModal} from '../actions/moderation'; -import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth'; +import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth'; import {can} from 'coral-framework/services/perms'; import UserDetail from 'coral-admin/src/containers/UserDetail'; diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index 8489c809f..1699c3b45 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -2,7 +2,7 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; import {compose} from 'react-apollo'; -import {updateOpenStatus, updateConfiguration} from 'coral-framework/actions/asset'; +import {updateOpenStatus, updateConfiguration} from 'coral-embed-stream/src/actions/asset'; import CloseCommentsInfo from '../components/CloseCommentsInfo'; import ConfigureCommentStream from '../components/ConfigureCommentStream'; diff --git a/client/coral-framework/actions/asset.js b/client/coral-embed-stream/src/actions/asset.js similarity index 94% rename from client/coral-framework/actions/asset.js rename to client/coral-embed-stream/src/actions/asset.js index aab3caf3a..87b6fdd98 100644 --- a/client/coral-framework/actions/asset.js +++ b/client/coral-embed-stream/src/actions/asset.js @@ -1,6 +1,6 @@ import * as actions from '../constants/asset'; -import coralApi from '../helpers/request'; -import {addNotification} from '../actions/notification'; +import coralApi from 'coral-framework/helpers/request'; +import {addNotification} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; diff --git a/client/coral-framework/actions/auth.js b/client/coral-embed-stream/src/actions/auth.js similarity index 97% rename from client/coral-framework/actions/auth.js rename to client/coral-embed-stream/src/actions/auth.js index 7c6d3bf89..851889a9b 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-embed-stream/src/actions/auth.js @@ -1,10 +1,10 @@ import jwtDecode from 'jwt-decode'; import bowser from 'bowser'; import * as actions from '../constants/auth'; -import * as Storage from '../helpers/storage'; -import coralApi, {base} from '../helpers/request'; -import pym from '../services/pym'; -import {addNotification} from '../actions/notification'; +import * as Storage from 'coral-framework/helpers/storage'; +import coralApi, {base} from 'coral-framework/helpers/request'; +import pym from 'coral-framework/services/pym'; +import {addNotification} from 'coral-framework/actions/notification'; import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; diff --git a/client/coral-framework/constants/asset.js b/client/coral-embed-stream/src/constants/asset.js similarity index 100% rename from client/coral-framework/constants/asset.js rename to client/coral-embed-stream/src/constants/asset.js diff --git a/client/coral-framework/constants/auth.js b/client/coral-embed-stream/src/constants/auth.js similarity index 100% rename from client/coral-framework/constants/auth.js rename to client/coral-embed-stream/src/constants/auth.js diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 6eb0d9f1c..c7400a506 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -8,8 +8,8 @@ import branch from 'recompose/branch'; import renderComponent from 'recompose/renderComponent'; import {Spinner} from 'coral-ui'; -import * as authActions from 'coral-framework/actions/auth'; -import * as assetActions from 'coral-framework/actions/asset'; +import * as authActions from '../actions/auth'; +import * as assetActions from '../actions/asset'; import {getDefinitionName} from 'coral-framework/utils'; import {withQuery} from 'coral-framework/hocs'; import Embed from '../components/Embed'; diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 451b115a9..e0ee02d80 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -8,7 +8,7 @@ import { withDeleteAction, withIgnoreUser, withEditComment } from 'coral-framework/graphql/mutations'; -import * as authActions from 'coral-framework/actions/auth'; +import * as authActions from 'coral-embed-stream/src/actions/auth'; import * as notificationActions from 'coral-framework/actions/notification'; import {setActiveReplyBox, setActiveTab, viewAllComments} from '../actions/stream'; import Stream from '../components/Stream'; diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index e973ca0d4..a14ce8df8 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -1,7 +1,7 @@ import React from 'react'; import {render} from 'react-dom'; -import {checkLogin, handleAuthToken, logout} from 'coral-framework/actions/auth'; +import {checkLogin, handleAuthToken, logout} from 'coral-embed-stream/src/actions/auth'; import './graphql'; import {addExternalConfig} from 'coral-embed-stream/src/actions/config'; import {getStore, injectReducers, addListener} from 'coral-framework/services/store'; diff --git a/client/coral-framework/reducers/asset.js b/client/coral-embed-stream/src/reducers/asset.js similarity index 100% rename from client/coral-framework/reducers/asset.js rename to client/coral-embed-stream/src/reducers/asset.js diff --git a/client/coral-framework/reducers/auth.js b/client/coral-embed-stream/src/reducers/auth.js similarity index 100% rename from client/coral-framework/reducers/auth.js rename to client/coral-embed-stream/src/reducers/auth.js diff --git a/client/coral-embed-stream/src/reducers/index.js b/client/coral-embed-stream/src/reducers/index.js index 61fe950c9..5c553b04f 100644 --- a/client/coral-embed-stream/src/reducers/index.js +++ b/client/coral-embed-stream/src/reducers/index.js @@ -1,8 +1,14 @@ +import auth from './auth'; +import asset from './asset'; import embed from './embed'; import config from './config'; import stream from './stream'; +import {reducer as commentBox} from '../../../talk-plugin-commentbox'; export default { + auth, + asset, + commentBox, embed, config, stream, diff --git a/client/coral-embed-stream/src/reducers/stream.js b/client/coral-embed-stream/src/reducers/stream.js index 86d5301b7..020ca654f 100644 --- a/client/coral-embed-stream/src/reducers/stream.js +++ b/client/coral-embed-stream/src/reducers/stream.js @@ -1,5 +1,5 @@ import * as actions from '../constants/stream'; -import * as authActions from 'coral-framework/constants/auth'; +import * as authActions from '../constants/auth'; function getQueryVariable(variable) { let query = window.location.search.substring(1); diff --git a/client/coral-framework/reducers/index.js b/client/coral-framework/reducers/index.js deleted file mode 100644 index 6b9b730ca..000000000 --- a/client/coral-framework/reducers/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import auth from './auth'; -import asset from './asset'; -import {reducer as commentBox} from '../../talk-plugin-commentbox'; - -export default { - auth, - asset, - commentBox, -}; diff --git a/client/coral-framework/services/store.js b/client/coral-framework/services/store.js index f87a887a3..ec2479d15 100644 --- a/client/coral-framework/services/store.js +++ b/client/coral-framework/services/store.js @@ -1,6 +1,5 @@ import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; import thunk from 'redux-thunk'; -import mainReducer from '../reducers'; import {getClient} from './client'; let listeners = []; @@ -52,7 +51,6 @@ export function getStore() { } const coralReducers = { - ...mainReducer, apollo: getClient().reducer() }; diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index e33c0b6cd..00fd2b6e7 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -11,7 +11,10 @@ import NotLoggedIn from '../components/NotLoggedIn'; import IgnoredUsers from '../components/IgnoredUsers'; import {Spinner} from 'coral-ui'; import CommentHistory from 'talk-plugin-history/CommentHistory'; -import {showSignInDialog, checkLogin} from 'coral-framework/actions/auth'; + +// TODO: Auth logic needs refactoring. +import {showSignInDialog, checkLogin} from 'coral-embed-stream/src/actions/auth'; + import {insertCommentsSorted} from 'plugin-api/beta/client/utils'; import update from 'immutability-helper'; diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index 22c30d639..c06362725 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -7,12 +7,14 @@ import {getDisplayName} from 'coral-framework/helpers/hoc'; import {compose, gql} from 'react-apollo'; import withFragments from 'coral-framework/hocs/withFragments'; import withMutation from 'coral-framework/hocs/withMutation'; -import {showSignInDialog} from 'coral-framework/actions/auth'; import {addNotification} from 'coral-framework/actions/notification'; import {capitalize} from 'coral-framework/helpers/strings'; import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils'; import * as PropTypes from 'prop-types'; +// TODO: Auth logic needs refactoring. +import {showSignInDialog} from 'coral-embed-stream/src/actions/auth'; + export default (reaction) => (WrappedComponent) => { if (typeof reaction !== 'string') { console.error('Reaction must be a valid string'); diff --git a/plugins/talk-plugin-auth/client/components/ChangeUsername.js b/plugins/talk-plugin-auth/client/components/ChangeUsername.js index c1ad081b1..5c5b0891d 100644 --- a/plugins/talk-plugin-auth/client/components/ChangeUsername.js +++ b/plugins/talk-plugin-auth/client/components/ChangeUsername.js @@ -13,7 +13,7 @@ import { invalidForm, validForm, createUsername -} from 'coral-framework/actions/auth'; +} from 'coral-embed-stream/src/actions/auth'; class ChangeUsernameContainer extends React.Component { constructor(props) { diff --git a/plugins/talk-plugin-auth/client/components/SignInButton.js b/plugins/talk-plugin-auth/client/components/SignInButton.js index 9c8a04b11..6641b8dff 100644 --- a/plugins/talk-plugin-auth/client/components/SignInButton.js +++ b/plugins/talk-plugin-auth/client/components/SignInButton.js @@ -2,7 +2,7 @@ import React from 'react'; import {Button} from 'plugin-api/beta/client/components/ui'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {showSignInDialog} from 'coral-framework/actions/auth'; +import {showSignInDialog} from 'coral-embed-stream/src/actions/auth'; import t from 'coral-framework/services/i18n'; const SignInButton = ({loggedIn, showSignInDialog}) => ( diff --git a/plugins/talk-plugin-auth/client/components/SignInContainer.js b/plugins/talk-plugin-auth/client/components/SignInContainer.js index 188651186..9ef6b5988 100644 --- a/plugins/talk-plugin-auth/client/components/SignInContainer.js +++ b/plugins/talk-plugin-auth/client/components/SignInContainer.js @@ -18,7 +18,7 @@ import { facebookCallback, invalidForm, validForm, -} from 'coral-framework/actions/auth'; +} from 'coral-embed-stream/src/actions/auth'; class SignInContainer extends React.Component { constructor(props) { diff --git a/plugins/talk-plugin-auth/client/components/UserBox.js b/plugins/talk-plugin-auth/client/components/UserBox.js index 61540d3c5..eea8e7690 100644 --- a/plugins/talk-plugin-auth/client/components/UserBox.js +++ b/plugins/talk-plugin-auth/client/components/UserBox.js @@ -3,7 +3,7 @@ import styles from './styles.css'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import t from 'coral-framework/services/i18n'; -import {logout} from 'coral-framework/actions/auth'; +import {logout} from 'coral-embed-stream/src/actions/auth'; const UserBox = ({loggedIn, user, logout, onShowProfile}) => (
From d47287fbd500300ba359ac60c2c7e3d40dcfc19f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 21 Aug 2017 21:39:40 +0700 Subject: [PATCH 03/16] Remove unused import --- client/coral-embed-stream/src/containers/Embed.js | 1 - 1 file changed, 1 deletion(-) diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 01690f77e..a8f6c1b18 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -10,7 +10,6 @@ import renderComponent from 'recompose/renderComponent'; import {Spinner} from 'coral-ui'; import * as authActions from '../actions/auth'; import * as assetActions from '../actions/asset'; -import pym from 'coral-framework/services/pym'; import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import {withQuery} from 'coral-framework/hocs'; import Embed from '../components/Embed'; From 54ea0b1a3fbf9be4ca695cf3eb0d4dcc602713d3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 22 Aug 2017 21:59:41 +0700 Subject: [PATCH 04/16] Remove global dependency on store, rest, pym and apollo client --- client/coral-admin/src/actions/assets.js | 9 +- client/coral-admin/src/actions/auth.js | 24 +++-- client/coral-admin/src/actions/community.js | 13 ++- client/coral-admin/src/actions/install.js | 9 +- client/coral-admin/src/actions/settings.js | 9 +- client/coral-admin/src/actions/users.js | 13 ++- client/coral-admin/src/index.js | 21 +--- .../coral-embed-stream/src/actions/asset.js | 9 +- client/coral-embed-stream/src/actions/auth.js | 56 +++++------ .../coral-embed-stream/src/actions/stream.js | 9 +- client/coral-embed-stream/src/index.js | 30 +----- .../components/TalkProvider.js | 4 +- client/coral-framework/helpers/plugins.js | 13 +-- client/coral-framework/helpers/request.js | 84 ---------------- client/coral-framework/services/bootstrap.js | 92 +++++++++++++++++ client/coral-framework/services/client.js | 99 ++++++++++--------- client/coral-framework/services/events.js | 6 +- client/coral-framework/services/rest.js | 62 ++++++++++++ client/coral-framework/services/store.js | 61 ++---------- client/coral-framework/services/transport.js | 37 ------- 20 files changed, 304 insertions(+), 356 deletions(-) delete mode 100644 client/coral-framework/helpers/request.js create mode 100644 client/coral-framework/services/bootstrap.js create mode 100644 client/coral-framework/services/rest.js delete mode 100644 client/coral-framework/services/transport.js diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js index b9ebe0ada..1ce6ad6ac 100644 --- a/client/coral-admin/src/actions/assets.js +++ b/client/coral-admin/src/actions/assets.js @@ -8,7 +8,6 @@ import { UPDATE_ASSETS } from '../constants/assets'; -import coralApi from '../../../coral-framework/helpers/request'; import t from 'coral-framework/services/i18n'; /** @@ -17,9 +16,9 @@ import t from 'coral-framework/services/i18n'; // Fetch a page of assets // Get comments to fill each of the three lists on the mod queue -export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => { +export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch, _, {rest}) => { dispatch({type: FETCH_ASSETS_REQUEST}); - return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) + return rest(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) .then(({result, count}) => dispatch({type: FETCH_ASSETS_SUCCESS, assets: result, @@ -34,9 +33,9 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte // Update an asset state // Get comments to fill each of the three lists on the mod queue -export const updateAssetState = (id, closedAt) => (dispatch) => { +export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => { dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt}); - return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) + return rest(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}}) .then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS})) .catch((error) => { console.error(error); diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 8c84a95ec..3433fc95a 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -1,8 +1,6 @@ import bowser from 'bowser'; import * as actions from '../constants/auth'; -import coralApi from 'coral-framework/helpers/request'; import * as Storage from 'coral-framework/helpers/storage'; -import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; import jwtDecode from 'jwt-decode'; @@ -10,7 +8,7 @@ import jwtDecode from 'jwt-decode'; // SIGN IN //============================================================================== -export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => { +export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client}) => { dispatch({type: actions.LOGIN_REQUEST}); const params = { @@ -27,7 +25,7 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => }; } - return coralApi('/auth/local', params) + return rest('/auth/local', params) .then(({user, token}) => { if (!user) { @@ -38,7 +36,7 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => } dispatch(handleAuthToken(token)); - resetWebsocket(); + client.resetWebsocket(); dispatch(checkLoginSuccess(user)); }) .catch((error) => { @@ -84,11 +82,11 @@ const forgotPasswordFailure = (error) => ({ error, }); -export const requestPasswordReset = (email) => (dispatch) => { +export const requestPasswordReset = (email) => (dispatch, _, {rest}) => { dispatch(forgotPasswordRequest(email)); const redirectUri = location.href; - return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}}) + return rest('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}}) .then(() => dispatch(forgotPasswordSuccess())) .catch((error) => { console.error(error); @@ -116,9 +114,9 @@ const checkLoginFailure = (error) => ({ error }); -export const checkLogin = () => (dispatch) => { +export const checkLogin = () => (dispatch, _, {rest, client}) => { dispatch(checkLoginRequest()); - return coralApi('/auth') + return rest('/auth') .then(({user}) => { if (!user) { if (!bowser.safari && !bowser.ios) { @@ -127,7 +125,7 @@ export const checkLogin = () => (dispatch) => { return dispatch(checkLoginFailure('not logged in')); } - resetWebsocket(); + client.resetWebsocket(); dispatch(checkLoginSuccess(user)); }) .catch((error) => { @@ -141,12 +139,12 @@ export const checkLogin = () => (dispatch) => { // LOGOUT //============================================================================== -export const logout = () => (dispatch) => { - return coralApi('/auth', {method: 'DELETE'}).then(() => { +export const logout = () => (dispatch, _, {rest, client}) => { + return rest('/auth', {method: 'DELETE'}).then(() => { Storage.removeItem('token'); // Reset the websocket. - resetWebsocket(); + client.resetWebsocket(); dispatch({type: actions.LOGOUT}); }); diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 70c9b7592..402a32e5d 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -14,13 +14,12 @@ import { HIDE_REJECT_USERNAME_DIALOG } from '../constants/community'; -import coralApi from '../../../coral-framework/helpers/request'; import t from 'coral-framework/services/i18n'; -export const fetchAccounts = (query = {}) => (dispatch) => { +export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => { dispatch(requestFetchAccounts()); - coralApi(`/users?${qs.stringify(query)}`) + rest(`/users?${qs.stringify(query)}`) .then(({result, page, count, limit, totalPages}) =>{ dispatch({ type: FETCH_COMMENTERS_SUCCESS, @@ -51,15 +50,15 @@ export const newPage = () => ({ type: COMMENTERS_NEW_PAGE }); -export const setRole = (id, role) => (dispatch) => { - return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}}) +export const setRole = (id, role) => (dispatch, _, {rest}) => { + return rest(`/users/${id}/role`, {method: 'POST', body: {role}}) .then(() => { return dispatch({type: SET_ROLE, id, role}); }); }; -export const setCommenterStatus = (id, status) => (dispatch) => { - return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}}) +export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => { + return rest(`/users/${id}/status`, {method: 'POST', body: {status}}) .then(() => { return dispatch({type: SET_COMMENTER_STATUS, id, status}); }); diff --git a/client/coral-admin/src/actions/install.js b/client/coral-admin/src/actions/install.js index 6393c58dd..d7210782a 100644 --- a/client/coral-admin/src/actions/install.js +++ b/client/coral-admin/src/actions/install.js @@ -1,4 +1,3 @@ -import coralApi from 'coral-framework/helpers/request'; import * as actions from '../constants/install'; import validate from 'coral-framework/helpers/validate'; import errorMsj from 'coral-framework/helpers/error'; @@ -106,10 +105,10 @@ export const submitUser = () => (dispatch, getState) => { }); }; -export const finishInstall = () => (dispatch, getState) => { +export const finishInstall = () => (dispatch, getState, {rest}) => { const data = getState().install.data; dispatch(installRequest()); - return coralApi('/setup', {method: 'POST', body: data}) + return rest('/setup', {method: 'POST', body: data}) .then(() => { dispatch(installSuccess()); dispatch(nextStep()); @@ -129,9 +128,9 @@ const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST}); const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed}); const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error}); -export const checkInstall = (next) => (dispatch) => { +export const checkInstall = (next) => (dispatch, _, {rest}) => { dispatch(checkInstallRequest()); - coralApi('/setup') + rest('/setup') .then(({installed}) => { dispatch(checkInstallSuccess(installed)); if (installed) { diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js index ca9062b9e..35f2013c9 100644 --- a/client/coral-admin/src/actions/settings.js +++ b/client/coral-admin/src/actions/settings.js @@ -1,4 +1,3 @@ -import coralApi from '../../../coral-framework/helpers/request'; import t from 'coral-framework/services/i18n'; export const SETTINGS_LOADING = 'SETTINGS_LOADING'; @@ -14,9 +13,9 @@ export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED'; export const WORDLIST_UPDATED = 'WORDLIST_UPDATED'; export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED'; -export const fetchSettings = () => (dispatch) => { +export const fetchSettings = () => (dispatch, _, {rest}) => { dispatch({type: SETTINGS_LOADING}); - coralApi('/settings') + rest('/settings') .then((settings) => { dispatch({type: SETTINGS_RECEIVED, settings}); }) @@ -41,13 +40,13 @@ export const updateDomainlist = (listName, list) => { return {type: DOMAINLIST_UPDATED, listName, list}; }; -export const saveSettingsToServer = () => (dispatch, getState) => { +export const saveSettingsToServer = () => (dispatch, getState, {rest}) => { let settings = getState().settings; if (settings.charCount) { settings.charCount = parseInt(settings.charCount); } dispatch({type: SAVE_SETTINGS_LOADING}); - coralApi('/settings', {method: 'PUT', body: settings}) + rest('/settings', {method: 'PUT', body: settings}) .then(() => { dispatch({type: SAVE_SETTINGS_SUCCESS, settings}); }) diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js index 7ba051a90..994a79a79 100644 --- a/client/coral-admin/src/actions/users.js +++ b/client/coral-admin/src/actions/users.js @@ -1,4 +1,3 @@ -import coralApi from '../../../coral-framework/helpers/request'; import * as userTypes from '../constants/users'; import t from 'coral-framework/services/i18n'; @@ -7,9 +6,9 @@ import t from 'coral-framework/services/i18n'; */ // change status of a user export const userStatusUpdate = (status, userId, commentId) => { - return (dispatch) => { + return (dispatch, _, {rest}) => { dispatch({type: userTypes.UPDATE_STATUS_REQUEST}); - return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) + return rest(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}}) .then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res})) .catch((error) => { console.error(error); @@ -21,8 +20,8 @@ export const userStatusUpdate = (status, userId, commentId) => { // change status of a user export const sendNotificationEmail = (userId, subject, body) => { - return (dispatch) => { - return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}}) + return (dispatch, _, {rest}) => { + return rest(`/users/${userId}/email`, {method: 'POST', body: {subject, body}}) .catch((error) => { console.error(error); const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); @@ -33,8 +32,8 @@ export const sendNotificationEmail = (userId, subject, body) => { // let a user edit their username export const enableUsernameEdit = (userId) => { - return (dispatch) => { - return coralApi(`/users/${userId}/username-enable`, {method: 'POST'}) + return (dispatch, _, {rest}) => { + return rest(`/users/${userId}/username-enable`, {method: 'POST'}) .catch((error) => { console.error(error); const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index ed9d537c0..e85cbed8c 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -1,34 +1,21 @@ import React from 'react'; import {render} from 'react-dom'; import smoothscroll from 'smoothscroll-polyfill'; -import EventEmitter from 'eventemitter2'; import TalkProvider from 'coral-framework/components/TalkProvider'; - -import {getClient} from 'coral-framework/services/client'; -import store from './services/store'; - +import {createContext} from 'coral-framework/services/bootstrap'; +import reducers from './reducers'; import App from './components/App'; - import 'react-mdl/extra/material.js'; import './graphql'; -import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins'; import plugins from 'pluginsConfig'; -const eventEmitter = new EventEmitter(); -const client = getClient(); +const context = createContext(reducers, plugins); -// TODO: pass redux actions through the emitter. - -loadPluginsTranslations(); -injectPluginsReducers(); smoothscroll.polyfill(); render( diff --git a/client/coral-embed-stream/src/actions/asset.js b/client/coral-embed-stream/src/actions/asset.js index 87b6fdd98..6acebeafb 100644 --- a/client/coral-embed-stream/src/actions/asset.js +++ b/client/coral-embed-stream/src/actions/asset.js @@ -1,5 +1,4 @@ import * as actions from '../constants/asset'; -import coralApi from 'coral-framework/helpers/request'; import {addNotification} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; @@ -12,10 +11,10 @@ const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_R const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings}); const updateAssetSettingsFailure = (error) => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE, error}); -export const updateConfiguration = (newConfig) => (dispatch, getState) => { +export const updateConfiguration = (newConfig) => (dispatch, getState, {rest}) => { const assetId = getState().asset.id; dispatch(updateAssetSettingsRequest()); - coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig}) + rest(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig}) .then(() => { dispatch(addNotification('success', t('framework.success_update_settings'))); dispatch(updateAssetSettingsSuccess(newConfig)); @@ -26,10 +25,10 @@ export const updateConfiguration = (newConfig) => (dispatch, getState) => { }); }; -export const updateOpenStream = (closedBody) => (dispatch, getState) => { +export const updateOpenStream = (closedBody) => (dispatch, getState, {rest}) => { const assetId = getState().asset.id; dispatch(fetchAssetRequest()); - coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody}) + rest(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody}) .then(() => { dispatch(addNotification('success', t('framework.success_update_settings'))); dispatch(fetchAssetSuccess(closedBody)); diff --git a/client/coral-embed-stream/src/actions/auth.js b/client/coral-embed-stream/src/actions/auth.js index 851889a9b..55f7eb546 100644 --- a/client/coral-embed-stream/src/actions/auth.js +++ b/client/coral-embed-stream/src/actions/auth.js @@ -2,11 +2,8 @@ import jwtDecode from 'jwt-decode'; import bowser from 'bowser'; import * as actions from '../constants/auth'; import * as Storage from 'coral-framework/helpers/storage'; -import coralApi, {base} from 'coral-framework/helpers/request'; -import pym from 'coral-framework/services/pym'; import {addNotification} from 'coral-framework/actions/notification'; -import {resetWebsocket} from 'coral-framework/services/client'; import t from 'coral-framework/services/i18n'; export const showSignInDialog = () => ({ @@ -59,9 +56,9 @@ export const updateUsername = ({username}) => ({ username }); -export const createUsername = (userId, formData) => (dispatch) => { +export const createUsername = (userId, formData) => (dispatch, _, {rest}) => { dispatch(createUsernameRequest()); - coralApi('/account/username', {method: 'PUT', body: formData}) + rest('/account/username', {method: 'PUT', body: formData}) .then(() => { dispatch(createUsernameSuccess()); dispatch(hideCreateUsernameDialog()); @@ -123,10 +120,10 @@ export const handleAuthToken = (token) => (dispatch) => { //============================================================================== export const fetchSignIn = (formData) => { - return (dispatch) => { + return (dispatch, _, {rest}) => { dispatch(signInRequest()); - return coralApi('/auth/local', {method: 'POST', body: formData}) + return rest('/auth/local', {method: 'POST', body: formData}) .then(({token}) => { if (!bowser.safari && !bowser.ios) { dispatch(handleAuthToken(token)); @@ -171,10 +168,10 @@ const signInFacebookFailure = (error) => ({ error }); -export const fetchSignInFacebook = () => (dispatch) => { +export const fetchSignInFacebook = () => (dispatch, _, {rest}) => { dispatch(signInFacebookRequest()); window.open( - `${base}/auth/facebook`, + `${rest.uri}/auth/facebook`, 'Continue with Facebook', 'menubar=0,resizable=0,width=500,height=500,top=200,left=500' ); @@ -188,10 +185,10 @@ const signUpFacebookRequest = () => ({ type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST }); -export const fetchSignUpFacebook = () => (dispatch) => { +export const fetchSignUpFacebook = () => (dispatch, _, {rest}) => { dispatch(signUpFacebookRequest()); window.open( - `${base}/auth/facebook`, + `${rest.uri}/auth/facebook`, 'Continue with Facebook', 'menubar=0,resizable=0,width=500,height=500,top=200,left=500' ); @@ -220,11 +217,11 @@ const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST}); const signUpSuccess = (user) => ({type: actions.FETCH_SIGNUP_SUCCESS, user}); const signUpFailure = (error) => ({type: actions.FETCH_SIGNUP_FAILURE, error}); -export const fetchSignUp = (formData) => (dispatch, getState) => { +export const fetchSignUp = (formData) => (dispatch, getState, {rest}) => { const redirectUri = getState().auth.redirectUri; dispatch(signUpRequest()); - coralApi('/users', { + rest('/users', { method: 'POST', body: formData, headers: {'X-Pym-Url': redirectUri} @@ -256,10 +253,10 @@ const forgotPasswordFailure = (error) => ({ error, }); -export const fetchForgotPassword = (email) => (dispatch, getState) => { +export const fetchForgotPassword = (email) => (dispatch, getState, {rest}) => { dispatch(forgotPasswordRequest(email)); const redirectUri = getState().auth.redirectUri; - coralApi('/account/password/reset', { + rest('/account/password/reset', { method: 'POST', body: {email, loc: redirectUri} }) @@ -275,16 +272,15 @@ export const fetchForgotPassword = (email) => (dispatch, getState) => { // LOGOUT //============================================================================== -export const logout = () => (dispatch) => { - return coralApi('/auth', {method: 'DELETE'}).then(() => { - Storage.removeItem('token'); +export const logout = () => async (dispatch, _, {rest, client, pym}) => { + await rest('/auth', {method: 'DELETE'}); + Storage.removeItem('token'); - // Reset the websocket. - resetWebsocket(); + // Reset the websocket. + client.resetWebsocket(); - dispatch({type: actions.LOGOUT}); - pym.sendMessage('coral-auth-changed'); - }); + dispatch({type: actions.LOGOUT}); + pym.sendMessage('coral-auth-changed'); }; //============================================================================== @@ -300,9 +296,9 @@ const checkLoginSuccess = (user, isAdmin) => ({ isAdmin }); -export const checkLogin = () => (dispatch) => { +export const checkLogin = () => (dispatch, _, {rest, client, pym}) => { dispatch(checkLoginRequest()); - coralApi('/auth') + rest('/auth') .then((result) => { if (!result.user) { Storage.removeItem('token'); @@ -310,7 +306,7 @@ export const checkLogin = () => (dispatch) => { } // Reset the websocket. - resetWebsocket(); + client.resetWebsocket(); dispatch(checkLoginSuccess(result.user)); pym.sendMessage('coral-auth-changed', JSON.stringify(result.user)); @@ -351,10 +347,10 @@ const verifyEmailFailure = () => ({ type: actions.VERIFY_EMAIL_FAILURE }); -export const requestConfirmEmail = (email) => (dispatch, getState) => { +export const requestConfirmEmail = (email) => (dispatch, getState, {rest}) => { const redirectUri = getState().auth.redirectUri; dispatch(verifyEmailRequest()); - return coralApi('/users/resend-verify', { + return rest('/users/resend-verify', { method: 'POST', body: {email}, headers: {'X-Pym-Url': redirectUri} @@ -387,8 +383,8 @@ export const setRedirectUri = (uri) => ({ const editUsernameFailure = (error) => ({type: actions.EDIT_USERNAME_FAILURE, error}); const editUsernameSuccess = () => ({type: actions.EDIT_USERNAME_SUCCESS}); -export const editName = (username) => (dispatch) => { - return coralApi('/account/username', {method: 'PUT', body: {username}}) +export const editName = (username) => (dispatch, _, {rest}) => { + return rest('/account/username', {method: 'PUT', body: {username}}) .then(() => { dispatch(editUsernameSuccess()); dispatch(addNotification('success', t('framework.success_name_update'))); diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index c20760259..75f353b19 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -1,11 +1,10 @@ -import pym from 'coral-framework/services/pym'; import * as actions from '../constants/stream'; import {buildUrl} from 'coral-framework/utils/url'; import queryString from 'query-string'; export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id}); -export const viewAllComments = () => { +export const viewAllComments = () => (dispatch, _, {pym}) => { const search = queryString.stringify({ ...queryString.parse(location.search), @@ -24,10 +23,10 @@ export const viewAllComments = () => { pym.sendMessage('coral-view-all-comments'); } catch (e) { /* not sure if we're worried about old browsers */ } - return {type: actions.VIEW_ALL_COMMENTS}; + dispatch({type: actions.VIEW_ALL_COMMENTS}); }; -export const viewComment = (id) => { +export const viewComment = (id) => (dispatch, _, {pym}) => { const search = queryString.stringify({ ...queryString.parse(location.search), @@ -46,7 +45,7 @@ export const viewComment = (id) => { pym.sendMessage('coral-view-comment', id); } catch (e) { /* not sure if we're worried about old browsers */ } - return {type: actions.VIEW_COMMENT, id}; + dispatch({type: actions.VIEW_COMMENT, id}); }; export const addCommentClassName = (className) => ({ diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index a14ce8df8..ac38d2eab 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -4,24 +4,16 @@ import {render} from 'react-dom'; import {checkLogin, handleAuthToken, logout} from 'coral-embed-stream/src/actions/auth'; import './graphql'; import {addExternalConfig} from 'coral-embed-stream/src/actions/config'; -import {getStore, injectReducers, addListener} from 'coral-framework/services/store'; -import {getClient} from 'coral-framework/services/client'; -import {createReduxEmitter} from 'coral-framework/services/events'; -import pym from 'coral-framework/services/pym'; +import {createContext} from 'coral-framework/services/bootstrap'; import AppRouter from './AppRouter'; -import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins'; import reducers from './reducers'; -import EventEmitter from 'eventemitter2'; import TalkProvider from 'coral-framework/components/TalkProvider'; import plugins from 'pluginsConfig'; -const store = getStore(); -const client = getClient(); -const eventEmitter = new EventEmitter({wildcard: true}); +const context = createContext(reducers, plugins); -loadPluginsTranslations(); -injectPluginsReducers(); -injectReducers(reducers); +// TODO: move init code into `bootstrap` service after auth has been refactored. +const {store, pym} = context; function inIframe() { try { @@ -57,23 +49,11 @@ if (!window.opener) { } else { init(); } - - // Pass any events through our parent. - eventEmitter.onAny((eventName, value) => { - pym.sendMessage('event', JSON.stringify({eventName, value})); - }); - - // Add a redux listener to pass through all actions to our event emitter. - addListener(createReduxEmitter(eventEmitter)); } render( diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index 275373c26..0f44af43f 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -12,9 +12,9 @@ class TalkProvider extends React.Component { } render() { - const {children, client, store, plugins} = this.props; + const {children, client, store} = this.props; return ( - + {children} ); diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js index 3173115c2..b73c89c64 100644 --- a/client/coral-framework/helpers/plugins.js +++ b/client/coral-framework/helpers/plugins.js @@ -6,8 +6,6 @@ import flattenDeep from 'lodash/flattenDeep'; import isEmpty from 'lodash/isEmpty'; import flatten from 'lodash/flatten'; import mapValues from 'lodash/mapValues'; -import {loadTranslations} from 'coral-framework/services/i18n'; -import {injectReducers} from 'coral-framework/services/store'; import {getDisplayName} from 'coral-framework/helpers/hoc'; import camelize from './camelize'; import plugins from 'pluginsConfig'; @@ -133,23 +131,18 @@ export function getModQueueConfigs() { .filter((o) => o)); } -function getTranslations() { +export function getTranslations(plugins) { return plugins .map((o) => o.module.translations) .filter((o) => o); } -export function loadPluginsTranslations() { - getTranslations().forEach((t) => loadTranslations(t)); -} - -export function injectPluginsReducers() { - const reducers = merge( +export function getReducers(plugins) { + return merge( ...plugins .filter((o) => o.module.reducer) .map((o) => ({[camelize(o.name)] : o.module.reducer})) ); - injectReducers(reducers); } function addMetaDataToSlotComponents() { diff --git a/client/coral-framework/helpers/request.js b/client/coral-framework/helpers/request.js deleted file mode 100644 index ffddc9c5e..000000000 --- a/client/coral-framework/helpers/request.js +++ /dev/null @@ -1,84 +0,0 @@ -import bowser from 'bowser'; -import * as Storage from './storage'; -import merge from 'lodash/merge'; -import {getStore} from 'coral-framework/services/store'; -import {BASE_PATH} from 'coral-framework/constants/url'; - -/** - * getAuthToken returns the active auth token or null - * Note: this method does not have access to the cookie based token used by - * browsers that don't allow us to use cross domain iframe local storage. - * @return {string|null} - */ -export const getAuthToken = () => { - let state = getStore().getState(); - - if (state.config.auth_token) { - - // if an auth_token exists in config, use it. - return state.config.auth_token; - - } else if (!bowser.safari && !bowser.ios) { - - // Use local storage auth tokens where there's a stable api. - return Storage.getItem('token'); - } - - return null; -}; - -const buildOptions = (inputOptions = {}) => { - const defaultOptions = { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - credentials: 'same-origin' - }; - - let options = merge({}, defaultOptions, inputOptions); - - // Apply authToken header - let authToken = getAuthToken(); - if (authToken !== null) { - options.headers.Authorization = `Bearer ${authToken}`; - } - - if (options.method.toLowerCase() !== 'get') { - options.body = JSON.stringify(options.body); - } - - return options; -}; - -const handleResp = (res) => { - if (res.status > 399) { - return res.json().then((err) => { - let message = err.message || res.status; - const error = new Error(); - - if (err.error && err.error.metadata && err.error.metadata.message) { - error.metadata = err.error.metadata.message; - } - - if (err.error && err.error.translation_key) { - error.translation_key = err.error.translation_key; - } - - error.message = message; - error.status = res.status; - throw error; - }); - } else if (res.status === 204) { - return res.text(); - } else { - return res.json(); - } -}; - -export const base = `${BASE_PATH}api/v1`; - -export default (url, options) => { - return fetch(`${base}${url}`, buildOptions(options)).then(handleResp); -}; diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js new file mode 100644 index 000000000..6a18695f7 --- /dev/null +++ b/client/coral-framework/services/bootstrap.js @@ -0,0 +1,92 @@ +import {createStore} from './store'; +import {createClient, apolloErrorReporter} from './client'; +import pym from './pym'; +import EventEmitter from 'eventemitter2'; +import {createReduxEmitter} from './events'; +import {createRestClient} from './rest'; +import thunk from 'redux-thunk'; +import {loadTranslations} from './i18n'; +import {getTranslations, getReducers} from '../helpers/plugins'; +import bowser from 'bowser'; +import * as Storage from '../helpers/storage'; +import {BASE_PATH} from 'coral-framework/constants/url'; + +/** + * getAuthToken returns the active auth token or null + * Note: this method does not have access to the cookie based token used by + * browsers that don't allow us to use cross domain iframe local storage. + * @return {string|null} + */ +const getAuthToken = (store) => { + let state = store.getState(); + + if (state.config.auth_token) { + + // if an auth_token exists in config, use it. + return state.config.auth_token; + + } else if (!bowser.safari && !bowser.ios) { + + // Use local storage auth tokens where there's a stable api. + return Storage.getItem('token'); + } + + return null; +}; + +export function createContext(reducers, plugins) { + const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; + const eventEmitter = new EventEmitter({wildcard: true}); + let store = null; + const token = () => { + + // Try to get the token from localStorage. If it isn't here, it may + // be passed as a cookie. + + // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT + // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. + return getAuthToken(store); + }; + const rest = createRestClient({ + uri: `${BASE_PATH}api/v1`, + token, + }); + const client = createClient({ + uri: `${BASE_PATH}api/v1/graph/ql`, + liveUri: `${protocol}://${location.host}${BASE_PATH}api/v1/live`, + token, + }); + const context = { + client, + pym, + plugins, + eventEmitter, + rest, + }; + + // Load plugin translations. + getTranslations(plugins).forEach((t) => loadTranslations(t)); + + // Pass any events through our parent. + eventEmitter.onAny((eventName, value) => { + pym.sendMessage('event', JSON.stringify({eventName, value})); + }); + + const finalReducers = { + ...reducers, + ...getReducers(plugins), + apollo: client.reducer(), + }; + + store = createStore(finalReducers, [ + client.middleware(), + thunk.withExtraArgument(context), + apolloErrorReporter, + createReduxEmitter(eventEmitter), + ]); + + return { + ...context, + store, + }; +} diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index 6f044f621..a354d68bc 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -1,64 +1,55 @@ -import ApolloClient, {addTypename, IntrospectionFragmentMatcher} from 'apollo-client'; -import {networkInterface} from './transport'; +import ApolloClient, {addTypename, IntrospectionFragmentMatcher, createNetworkInterface} from 'apollo-client'; import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws'; import MessageTypes from 'subscriptions-transport-ws/dist/message-types'; -import {getAuthToken} from '../helpers/request'; import introspectionQueryResultData from '../graphql/introspection.json'; -import {BASE_PATH} from 'coral-framework/constants/url'; -let client, wsClient = null, wsClientToken = null; - -export function resetWebsocket() { - if (wsClient === null) { - - // Nothing to reset! - return; +// Redux middleware to report any errors to the console. +export const apolloErrorReporter = () => (next) => (action) => { + if (action.type === 'APOLLO_QUERY_ERROR') { + console.error(action.error); } + return next(action); +}; - // Close socket connection which will also unregister subscriptions on the server-side. - wsClient.close(); - - // Reconnect to the server. - wsClient.connect(); - - // Reregister all subscriptions (uses non public api). - // See: https://github.com/apollographql/subscriptions-transport-ws/issues/171 - Object.keys(wsClient.operations).forEach((id) => { - wsClient.sendMessage(id, MessageTypes.GQL_START, wsClient.operations[id].options); - }); -} - -export function getClient(options = {}) { - if (client) { - return client; - } - - const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; - wsClient = new SubscriptionClient(`${protocol}://${location.host}${BASE_PATH}api/v1/live`, { +export function createClient(options = {}) { + const {token, uri, liveUri, ...apolloOptions} = options; + const wsClient = new SubscriptionClient(liveUri, { reconnect: true, lazy: true, connectionParams: { - get token() { - - wsClientToken = getAuthToken(); - - // Try to get the token from localStorage. If it isn't here, it may - // be passed as a cookie. - - // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT - // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. - return wsClientToken; - } + token, } }); + const networkInterface = createNetworkInterface({ + uri, + opts: { + credentials: 'same-origin' + } + }); + + networkInterface.use([{ + applyMiddleware(req, next) { + if (!req.options.headers) { + req.options.headers = {}; // Create the header object if needed. + } + + let authToken = typeof token === 'function' ? token() : token; + if (authToken) { + req.options.headers['authorization'] = `Bearer ${authToken}`; + } + + next(); + } + }]); + const networkInterfaceWithSubscriptions = addGraphQLSubscriptions( networkInterface, wsClient, ); - client = new ApolloClient({ - ...options, + const client = new ApolloClient({ + ...apolloOptions, connectToDevTools: true, addTypename: true, fragmentMatcher: new IntrospectionFragmentMatcher({introspectionQueryResultData}), @@ -72,5 +63,25 @@ export function getClient(options = {}) { networkInterface: networkInterfaceWithSubscriptions, }); + client.resetWebsocket = () => { + if (wsClient === null) { + + // Nothing to reset! + return; + } + + // Close socket connection which will also unregister subscriptions on the server-side. + wsClient.close(); + + // Reconnect to the server. + wsClient.connect(); + + // Reregister all subscriptions (uses non public api). + // See: https://github.com/apollographql/subscriptions-transport-ws/issues/171 + Object.keys(wsClient.operations).forEach((id) => { + wsClient.sendMessage(id, MessageTypes.GQL_START, wsClient.operations[id].options); + }); + }; + return client; } diff --git a/client/coral-framework/services/events.js b/client/coral-framework/services/events.js index a1a2f4448..f6a62763d 100644 --- a/client/coral-framework/services/events.js +++ b/client/coral-framework/services/events.js @@ -1,5 +1,5 @@ export function createReduxEmitter(eventEmitter) { - return (action) => { + return () => (next) => (action) => { // Handle apollo actions. if (action.type.startsWith('APOLLO_')) { @@ -7,8 +7,10 @@ export function createReduxEmitter(eventEmitter) { const {operationName, variables, result: {data}} = action; eventEmitter.emit(`subscription.${operationName}.data`, {variables, data}); } - return; + return next(action); } eventEmitter.emit(`action.${action.type}`, {action}); + + return next(action); }; } diff --git a/client/coral-framework/services/rest.js b/client/coral-framework/services/rest.js new file mode 100644 index 000000000..f879659ca --- /dev/null +++ b/client/coral-framework/services/rest.js @@ -0,0 +1,62 @@ +import merge from 'lodash/merge'; + +const buildOptions = (inputOptions = {}) => { + const defaultOptions = { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + credentials: 'same-origin' + }; + + const options = merge({}, defaultOptions, inputOptions); + if (options.method.toLowerCase() !== 'get') { + options.body = JSON.stringify(options.body); + } + + return options; +}; + +const handleResp = (res) => { + if (res.status > 399) { + return res.json().then((err) => { + let message = err.message || res.status; + const error = new Error(); + + if (err.error && err.error.metadata && err.error.metadata.message) { + error.metadata = err.error.metadata.message; + } + + if (err.error && err.error.translation_key) { + error.translation_key = err.error.translation_key; + } + + error.message = message; + error.status = res.status; + throw error; + }); + } else if (res.status === 204) { + return res.text(); + } else { + return res.json(); + } +}; + +export function createRestClient(options) { + const {token, uri} = options; + const client = (path, options) => { + const authToken = typeof token === 'function' ? token() : token; + let opts = options; + if (authToken) { + opts = merge({}, options, { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }); + } + return fetch(`${uri}${path}`, buildOptions(opts)).then(handleResp); + }; + client.uri = uri; + return client; +} diff --git a/client/coral-framework/services/store.js b/client/coral-framework/services/store.js index ec2479d15..e5fc52503 100644 --- a/client/coral-framework/services/store.js +++ b/client/coral-framework/services/store.js @@ -1,66 +1,21 @@ -import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; -import thunk from 'redux-thunk'; -import {getClient} from './client'; +import {createStore as reduxCreateStore, combineReducers, applyMiddleware, compose} from 'redux'; -let listeners = []; - -export function injectReducers(reducers) { - const store = getStore(); - store.coralReducers = {...store.coralReducers, ...reducers}; - store.replaceReducer(combineReducers(store.coralReducers)); -} - -/** - * Add a action listener to the redux store. - * The action is passed as the first argument to the callback. - */ -export function addListener(cb) { - listeners.push(cb); -} - -export function getStore() { - if (window.coralStore) { - return window.coralStore; - } - - const apolloErrorReporter = () => (next) => (action) => { - if (action.type === 'APOLLO_QUERY_ERROR') { - console.error(action.error); - } - return next(action); - }; - - const customListener = () => (next) => (action) => { - listeners.forEach((cb) => {cb(action);}); - return next(action); - }; - - const middlewares = [ +export function createStore(reducers, middlewares = []) { + const enhancers = [ applyMiddleware( - getClient().middleware(), - thunk, - apolloErrorReporter, - customListener, + ...middlewares, ), ]; if (window.devToolsExtension) { // we can't have the last argument of compose() be undefined - middlewares.push(window.devToolsExtension()); + enhancers.push(window.devToolsExtension()); } - const coralReducers = { - apollo: getClient().reducer() - }; - - window.coralStore = createStore( - combineReducers(coralReducers), + return reduxCreateStore( + combineReducers(reducers), {}, - compose(...middlewares) + compose(...enhancers) ); - - window.coralStore.coralReducers = coralReducers; - - return window.coralStore; } diff --git a/client/coral-framework/services/transport.js b/client/coral-framework/services/transport.js deleted file mode 100644 index 060bfc34b..000000000 --- a/client/coral-framework/services/transport.js +++ /dev/null @@ -1,37 +0,0 @@ -import {createNetworkInterface} from 'apollo-client'; -import {getAuthToken} from '../helpers/request'; -import {BASE_PATH} from 'coral-framework/constants/url'; - -//============================================================================== -// NETWORK INTERFACE -//============================================================================== - -const networkInterface = createNetworkInterface({ - uri: `${BASE_PATH}api/v1/graph/ql`, - opts: { - credentials: 'same-origin' - } -}); - -//============================================================================== -// MIDDLEWARES -//============================================================================== - -networkInterface.use([{ - applyMiddleware(req, next) { - if (!req.options.headers) { - req.options.headers = {}; // Create the header object if needed. - } - - let authToken = getAuthToken(); - if (authToken) { - req.options.headers['authorization'] = `Bearer ${authToken}`; - } - - next(); - } -}]); - -export { - networkInterface -}; From fccbcce7a804f93058e7d35cac1c255ce8054aee Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 01:41:58 +0700 Subject: [PATCH 05/16] Remove global dependency on registry and plugins --- client/coral-admin/src/graphql/index.js | 5 +- client/coral-admin/src/index.js | 6 +- .../Moderation/containers/Moderation.js | 20 +- .../routes/Moderation/hoc/withQueueConfig.js | 26 + .../src/routes/Moderation/queueConfig.js | 2 - .../src/containers/StreamTabPanel.js | 45 +- .../coral-embed-stream/src/graphql/index.js | 4 +- client/coral-embed-stream/src/index.js | 6 +- .../components/IfSlotIsEmpty.js | 6 +- .../components/IfSlotIsNotEmpty.js | 6 +- client/coral-framework/components/Slot.js | 13 +- .../components/TalkProvider.js | 6 +- client/coral-framework/helpers/plugins.js | 166 ------ client/coral-framework/hocs/withFragments.js | 19 +- client/coral-framework/hocs/withMutation.js | 17 +- client/coral-framework/hocs/withQuery.js | 30 +- client/coral-framework/services/bootstrap.js | 22 +- client/coral-framework/services/client.js | 5 - .../services/graphqlRegistry.js | 494 +++++++++--------- client/coral-framework/services/plugins.js | 174 ++++++ plugin-api/beta/client/services/index.js | 6 - 21 files changed, 582 insertions(+), 496 deletions(-) create mode 100644 client/coral-admin/src/routes/Moderation/hoc/withQueueConfig.js delete mode 100644 client/coral-framework/helpers/plugins.js create mode 100644 client/coral-framework/services/plugins.js diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index 915fe2989..dea68c5e8 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -1,6 +1,4 @@ -import {add} from 'coral-framework/services/graphqlRegistry'; - -const extension = { +export default { mutations: { SetUserStatus: () => ({ refetchQueries: ['CoralAdmin_Community'], @@ -11,4 +9,3 @@ const extension = { }, }; -add(extension); diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index e85cbed8c..8b90f5acf 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -6,10 +6,10 @@ import {createContext} from 'coral-framework/services/bootstrap'; import reducers from './reducers'; import App from './components/App'; import 'react-mdl/extra/material.js'; -import './graphql'; -import plugins from 'pluginsConfig'; +import graphqlExtension from './graphql'; +import pluginsConfig from 'pluginsConfig'; -const context = createContext(reducers, plugins); +const context = createContext({reducers, graphqlExtension, pluginsConfig}); smoothscroll.polyfill(); diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index d5ca5c840..8df83da2a 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -26,25 +26,26 @@ import { storySearchChange, clearState } from 'actions/moderation'; +import withQueueConfig from '../hoc/withQueueConfig'; import {Spinner} from 'coral-ui'; import Moderation from '../components/Moderation'; import Comment from './Comment'; -import queueConfig from '../queueConfig'; +import baseQueueConfig from '../queueConfig'; function prepareNotificationText(text) { return truncate(text, {length: 50}).replace('\n', ' '); } function getAssetId(props) { - if (props.params.tabOrId && !(props.params.tabOrId in queueConfig)) { + if (props.params.tabOrId && !(props.params.tabOrId in props.queueConfig)) { return props.params.tabOrId; } return props.params.id || null; } function getTab(props) { - if (props.params.tabOrId && props.params.tabOrId in queueConfig) { + if (props.params.tabOrId && props.params.tabOrId in props.queueConfig) { return props.params.tabOrId; } return props.params.tab || null; @@ -54,7 +55,7 @@ class ModerationContainer extends Component { subscriptions = []; handleCommentChange = (root, comment, notify) => { - return handleCommentChange(root, comment, this.props.data.variables.sort, notify, queueConfig, this.activeTab); + return handleCommentChange(root, comment, this.props.data.variables.sort, notify, this.props.queueConfig, this.activeTab); }; get activeTab() { @@ -161,8 +162,8 @@ class ModerationContainer extends Component { cursor: this.props.root[tab].endCursor, sort: this.props.data.variables.sort, asset_id: this.props.data.variables.asset_id, - statuses: queueConfig[tab].statuses, - action_type: queueConfig[tab].action_type, + statuses: this.props.queueConfig[tab].statuses, + action_type: this.props.queueConfig[tab].action_type, }; return this.props.data.fetchMore({ query: LOAD_MORE_QUERY, @@ -206,7 +207,7 @@ class ModerationContainer extends Component { } const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation); - const currentQueueConfig = Object.assign({}, queueConfig); + const currentQueueConfig = Object.assign({}, this.props.queueConfig); if (premodEnabled) { delete currentQueueConfig.new; } else { @@ -304,7 +305,7 @@ const commentConnectionFragment = gql` ${Comment.fragments.comment} `; -const withModQueueQuery = withQuery(gql` +const withModQueueQuery = withQuery(({queueConfig}) => gql` query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER, $allAssets: Boolean!) { ${Object.keys(queueConfig).map((queue) => ` ${queue}: comments(query: { @@ -352,7 +353,7 @@ const withModQueueQuery = withQuery(gql` }, }); -const withQueueCountPolling = withQuery(gql` +const withQueueCountPolling = withQuery(({queueConfig}) => gql` query CoralAdmin_ModerationCountPoll($asset_id: ID) { ${Object.keys(queueConfig).map((queue) => ` ${queue}Count: commentCount(query: { @@ -398,6 +399,7 @@ const mapDispatchToProps = (dispatch) => ({ }); export default compose( + withQueueConfig(baseQueueConfig), connect(mapStateToProps, mapDispatchToProps), withSetCommentStatus, withQueueCountPolling, diff --git a/client/coral-admin/src/routes/Moderation/hoc/withQueueConfig.js b/client/coral-admin/src/routes/Moderation/hoc/withQueueConfig.js new file mode 100644 index 000000000..a56051810 --- /dev/null +++ b/client/coral-admin/src/routes/Moderation/hoc/withQueueConfig.js @@ -0,0 +1,26 @@ +import React from 'react'; +import hoistStatics from 'recompose/hoistStatics'; +import PropTypes from 'prop-types'; + +/** + * WithQueueConfig takes a `queueConfig` parameter that is + * passed down as a prop enriched with queue config data from plugins. + */ +export default (queueConfig) => hoistStatics((WrappedComponent) => { + class WithQueueConfig extends React.Component { + static contextTypes = { + plugins: PropTypes.object, + }; + + pluginsConfig = this.context.plugins.getModQueueConfigs(); + + render() { + return ; + } + } + + return WithQueueConfig; +}); diff --git a/client/coral-admin/src/routes/Moderation/queueConfig.js b/client/coral-admin/src/routes/Moderation/queueConfig.js index b9e9e5320..9a23ccc6c 100644 --- a/client/coral-admin/src/routes/Moderation/queueConfig.js +++ b/client/coral-admin/src/routes/Moderation/queueConfig.js @@ -1,5 +1,4 @@ import t from 'coral-framework/services/i18n'; -import {getModQueueConfigs} from 'coral-framework/helpers/plugins'; export default { premod: { @@ -33,5 +32,4 @@ export default { icon: 'question_answer', name: t('modqueue.all'), }, - ...getModQueueConfigs(), }; diff --git a/client/coral-embed-stream/src/containers/StreamTabPanel.js b/client/coral-embed-stream/src/containers/StreamTabPanel.js index 7b91d58ee..152f34e39 100644 --- a/client/coral-embed-stream/src/containers/StreamTabPanel.js +++ b/client/coral-embed-stream/src/containers/StreamTabPanel.js @@ -2,13 +2,15 @@ import React from 'react'; import StreamTabPanel from '../components/StreamTabPanel'; import {connect} from 'react-redux'; import omit from 'lodash/omit'; -import {getSlotComponents, getSlotComponentProps} from 'coral-framework/helpers/plugins'; import {Tab, TabPane} from 'coral-ui'; import {getShallowChanges} from 'coral-framework/utils'; import isEqual from 'lodash/isEqual'; import PropTypes from 'prop-types'; class StreamTabPanelContainer extends React.Component { + static contextTypes = { + plugins: PropTypes.object, + }; componentDidMount() { this.fallbackAllTab(); @@ -43,28 +45,37 @@ class StreamTabPanelContainer extends React.Component { } getSlotComponents(slot, props = this.props) { - return getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData); + const {plugins} = this.context; + return plugins.getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData); } getPluginTabElements(props = this.props) { - return this.getSlotComponents(props.tabSlot).map((PluginComponent) => ( - - - - )); + const {plugins} = this.context; + return this.getSlotComponents(props.tabSlot).map((PluginComponent) => { + const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData); + return ( + + + + ); + }); } getPluginTabPaneElements(props = this.props) { - return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => ( - - - - )); + const {plugins} = this.context; + return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => { + const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData); + return ( + + + + ); + }); } render() { diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index b4c5ce163..9ec9a2132 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -1,10 +1,9 @@ import {gql} from 'react-apollo'; -import {add} from 'coral-framework/services/graphqlRegistry'; import update from 'immutability-helper'; import uuid from 'uuid/v4'; import {insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} from './utils'; -const extension = { +export default { fragments: { EditCommentResponse: gql` fragment CoralEmbedStream_EditCommentResponse on EditCommentResponse { @@ -223,4 +222,3 @@ const extension = { }, }; -add(extension); diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index ac38d2eab..dca4e31b4 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -2,15 +2,15 @@ import React from 'react'; import {render} from 'react-dom'; import {checkLogin, handleAuthToken, logout} from 'coral-embed-stream/src/actions/auth'; -import './graphql'; +import graphqlExtension from './graphql'; import {addExternalConfig} from 'coral-embed-stream/src/actions/config'; import {createContext} from 'coral-framework/services/bootstrap'; import AppRouter from './AppRouter'; import reducers from './reducers'; import TalkProvider from 'coral-framework/components/TalkProvider'; -import plugins from 'pluginsConfig'; +import pluginsConfig from 'pluginsConfig'; -const context = createContext(reducers, plugins); +const context = createContext({reducers, graphqlExtension, pluginsConfig}); // TODO: move init code into `bootstrap` service after auth has been refactored. const {store, pym} = context; diff --git a/client/coral-framework/components/IfSlotIsEmpty.js b/client/coral-framework/components/IfSlotIsEmpty.js index e9211fc57..557ee3b3d 100644 --- a/client/coral-framework/components/IfSlotIsEmpty.js +++ b/client/coral-framework/components/IfSlotIsEmpty.js @@ -1,11 +1,13 @@ import React from 'react'; import {connect} from 'react-redux'; -import {isSlotEmpty} from 'coral-framework/helpers/plugins'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; import {getShallowChanges} from 'coral-framework/utils'; class IfSlotIsEmpty extends React.Component { + static contextTypes = { + plugins: PropTypes.object, + }; shouldComponentUpdate(next) { @@ -22,7 +24,7 @@ class IfSlotIsEmpty extends React.Component { isSlotEmpty(props = this.props) { const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props; - return isSlotEmpty(slot, reduxState, rest); + return this.context.plugins.isSlotEmpty(slot, reduxState, rest); } render() { diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js index e7a0e83ce..4e3abfae4 100644 --- a/client/coral-framework/components/IfSlotIsNotEmpty.js +++ b/client/coral-framework/components/IfSlotIsNotEmpty.js @@ -1,11 +1,13 @@ import React from 'react'; import {connect} from 'react-redux'; -import {isSlotEmpty} from 'coral-framework/helpers/plugins'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; import {getShallowChanges} from 'coral-framework/utils'; class IfSlotIsNotEmpty extends React.Component { + static contextTypes = { + plugins: PropTypes.object, + }; shouldComponentUpdate(next) { @@ -22,7 +24,7 @@ class IfSlotIsNotEmpty extends React.Component { isSlotEmpty(props = this.props) { const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props; - return isSlotEmpty(slot, reduxState, rest); + return this.context.plugins.isSlotEmpty(slot, reduxState, rest); } render() { diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index c0cca143c..dad5d5cf2 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -2,14 +2,18 @@ import React from 'react'; import cn from 'classnames'; import styles from './Slot.css'; import {connect} from 'react-redux'; -import {getSlotElements, getSlotComponentProps} from 'coral-framework/helpers/plugins'; import omit from 'lodash/omit'; +import PropTypes from 'prop-types'; import isEqual from 'lodash/isEqual'; import {getShallowChanges} from 'coral-framework/utils'; const emptyConfig = {}; class Slot extends React.Component { + static contextTypes = { + plugins: PropTypes.object, + }; + shouldComponentUpdate(next) { // Prevent Slot from rerendering when only reduxState has changed and @@ -30,15 +34,18 @@ class Slot extends React.Component { } getChildren(props = this.props) { - return getSlotElements(props.fill, props.reduxState, this.getSlotProps(props), props.queryData); + const {plugins} = this.context; + return plugins.getSlotElements(props.fill, props.reduxState, this.getSlotProps(props), props.queryData); } render() { + const {plugins} = this.context; const {inline = false, className, reduxState, defaultComponent: DefaultComponent, queryData} = this.props; let children = this.getChildren(); const pluginConfig = reduxState.config.pluginConfig || emptyConfig; if (children.length === 0 && DefaultComponent) { - children = ; + const props = plugins.getSlotComponentProps(DefaultComponent, reduxState, this.getSlotProps(this.props), queryData); + children = ; } return ( diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index 0f44af43f..1350244f6 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -8,6 +8,8 @@ class TalkProvider extends React.Component { eventEmitter: this.props.eventEmitter, pym: this.props.pym, plugins: this.props.plugins, + rest: this.props.rest, + graphqlRegistry: this.props.graphqlRegistry, }; } @@ -24,7 +26,9 @@ class TalkProvider extends React.Component { TalkProvider.childContextTypes = { pym: PropTypes.object, eventEmitter: PropTypes.object, - plugins: PropTypes.array, + plugins: PropTypes.object, + rest: PropTypes.func, + graphqlRegistry: PropTypes.object, }; export default TalkProvider; diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js deleted file mode 100644 index b73c89c64..000000000 --- a/client/coral-framework/helpers/plugins.js +++ /dev/null @@ -1,166 +0,0 @@ -import React from 'react'; -import uniq from 'lodash/uniq'; -import pick from 'lodash/pick'; -import merge from 'lodash/merge'; -import flattenDeep from 'lodash/flattenDeep'; -import isEmpty from 'lodash/isEmpty'; -import flatten from 'lodash/flatten'; -import mapValues from 'lodash/mapValues'; -import {getDisplayName} from 'coral-framework/helpers/hoc'; -import camelize from './camelize'; -import plugins from 'pluginsConfig'; -import uuid from 'uuid/v4'; - -// This is returned for pluginConfig when it is empty. -const emptyConfig = {}; - -export function getSlotComponents(slot, reduxState, props = {}, queryData = {}) { - const pluginConfig = reduxState.config.plugin_config || emptyConfig; - return flatten(plugins - - // Filter out components that have slots and have been disabled in `plugin_config` - .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components)) - - .filter((o) => o.module.slots[slot]) - .map((o) => o.module.slots[slot]) - ) - .filter((component) => { - if(!component.isExcluded) { - return true; - } - let resolvedProps = getSlotComponentProps(component, reduxState, props, queryData); - if (component.mapStateToProps) { - resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)}; - } - return !component.isExcluded(resolvedProps); - }); -} - -export function isSlotEmpty(slot, reduxState, props = {}, queryData = {}) { - return getSlotComponents(slot, reduxState, props, queryData).length === 0; -} - -// Memoize the warnings so we only show them once. -const memoizedWarnings = []; - -// withWarnings decorates the props of queryData with a proxy that -// prints a warning when accessing deeper props. -function withWarnings(component, queryData) { - if (process.env.NODE_ENV !== 'production' && window.Proxy) { - - // Show warnings when accessing queryData only when not in production. - return mapValues(queryData, (value, key) => { - - // Keep null values.. - if (!queryData[key]) { - return queryData[key]; - } - return new Proxy(queryData[key], { - get(target, name) { - - // Only care about the components defined in the plugins. - if (component.talkPluginName) { - const warning = `'${getDisplayName(component)}' of '${component.talkPluginName}' accessed '${key}.${name}' but did not define fragments using the withFragment HOC`; - if (memoizedWarnings.indexOf(warning) === -1) { - console.warn(warning); - memoizedWarnings.push(warning); - } - } - return queryData[key][name]; - } - }); - }); - } - - return queryData; -} - -/** - * getSlotComponentProps calculate the props we would pass to the slot component. - * query datas are only passed to the component if it is defined in `component.fragments`. - */ -export function getSlotComponentProps(component, reduxState, props, queryData) { - const pluginConfig = reduxState.config.plugin_config || emptyConfig; - return { - ...props, - config: pluginConfig, - ...( - component.fragments - ? pick(queryData, Object.keys(component.fragments)) - : withWarnings(component, queryData) - ) - }; -} - -/** - * Returns React Elements for given slot. - */ -export function getSlotElements(slot, reduxState, props = {}, queryData = {}) { - return getSlotComponents(slot, reduxState, props, queryData) - .map((component, i) => { - return React.createElement(component, {key: i, ...getSlotComponentProps(component, reduxState, props, queryData)}); - }); -} - -export function getSlotFragments(slot, part) { - const components = uniq(flattenDeep(plugins - .filter((o) => o.module.slots ? o.module.slots[slot] : false) - .map((o) => o.module.slots[slot]) - )); - - const documents = components - .map((c) => c.fragments) - .filter((fragments) => fragments && fragments[part]) - .reduce((res, fragments) => { - res.push(fragments[part]); - return res; - }, []); - - return documents; -} - -export function getGraphQLExtensions() { - return plugins - .map((o) => pick(o.module, ['mutations', 'queries', 'fragments'])) - .filter((o) => !isEmpty(o)); -} - -export function getModQueueConfigs() { - return merge(...plugins - .map((o) => o.module.modQueues) - .filter((o) => o)); -} - -export function getTranslations(plugins) { - return plugins - .map((o) => o.module.translations) - .filter((o) => o); -} - -export function getReducers(plugins) { - return merge( - ...plugins - .filter((o) => o.module.reducer) - .map((o) => ({[camelize(o.name)] : o.module.reducer})) - ); -} - -function addMetaDataToSlotComponents() { - - // Add talkPluginName to Slot Components. - plugins.forEach((plugin) => { - const slots = plugin.module.slots; - slots && Object.keys(slots).forEach((slot) => { - slots[slot].forEach((component) => { - - // Attach plugin name to the component - component.talkPluginName = plugin.name; - - // Attach uuid to the component - component.talkUuid = uuid(); - }); - }); - }); -} - -addMetaDataToSlotComponents(); diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js index 9bc06290e..be6d07fed 100644 --- a/client/coral-framework/hocs/withFragments.js +++ b/client/coral-framework/hocs/withFragments.js @@ -1,9 +1,9 @@ import React from 'react'; import graphql from 'graphql-anywhere'; -import {resolveFragments} from 'coral-framework/services/graphqlRegistry'; import mapValues from 'lodash/mapValues'; import hoistStatics from 'recompose/hoistStatics'; import {getShallowChanges} from 'coral-framework/utils'; +import PropTypes from 'prop-types'; import union from 'lodash/union'; // TODO: Should not depend on `props.data` @@ -63,7 +63,22 @@ function hasEqualLeaves(a, b, path = '') { export default (fragments) => hoistStatics((BaseComponent) => { class WithFragments extends React.Component { - fragments = mapValues(fragments, (val) => resolveFragments(val)); + static contextTypes = { + graphqlRegistry: PropTypes.object, + }; + + get graphqlRegistry() { + return this.context.graphqlRegistry; + } + + resolveDocument(documentOrCallback) { + const document = typeof documentOrCallback === 'function' + ? documentOrCallback(this.props, this.context) + : documentOrCallback; + return this.graphqlRegistry.resolveFragments(document); + } + + fragments = mapValues(fragments, (val) => this.resolveDocument(val)); fragmentKeys = Object.keys(fragments).sort(); // Cache variables between lifecycles to speed up render. diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js index 7c3dcfa1a..46ec19eb7 100644 --- a/client/coral-framework/hocs/withMutation.js +++ b/client/coral-framework/hocs/withMutation.js @@ -4,7 +4,6 @@ import merge from 'lodash/merge'; import uniq from 'lodash/uniq'; 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'; @@ -43,8 +42,20 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { static contextTypes = { eventEmitter: PropTypes.object, store: PropTypes.object, + graphqlRegistry: PropTypes.object, }; + get graphqlRegistry() { + return this.context.graphqlRegistry; + } + + resolveDocument(documentOrCallback) { + const document = typeof documentOrCallback === 'function' + ? documentOrCallback(this.props, this.context) + : documentOrCallback; + return this.graphqlRegistry.resolveFragments(document); + } + // Lazily resolve fragments from graphRegistry to support circular dependencies. memoized = null; @@ -56,7 +67,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { propsWrapper = (data) => { const name = getDefinitionName(document); - const callbacks = getMutationOptions(name); + const callbacks = this.graphqlRegistry.getMutationOptions(name); const mutate = (base) => { const variables = base.variables || config.options.variables; const configs = callbacks.map((cb) => cb({variables, state: this.context.store.getState()})); @@ -167,7 +178,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { getWrapped = () => { if (!this.memoized) { - this.memoized = graphql(resolveFragments(document), {...config, props: this.propsWrapper})(WrappedComponent); + this.memoized = graphql(this.resolveDocument(document), {...config, props: this.propsWrapper})(WrappedComponent); } return this.memoized; }; diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index f19c3209e..c0825f28b 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -1,6 +1,5 @@ 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'; import hoistStatics from 'recompose/hoistStatics'; @@ -37,17 +36,28 @@ function networkStatusToString(networkStatus) { * apply query options registered in the graphRegistry. */ export default (document, config = {}) => hoistStatics((WrappedComponent) => { - const name = getDefinitionName(document); - return class WithQuery extends React.Component { static contextTypes = { eventEmitter: PropTypes.object, + graphqlRegistry: PropTypes.object, }; // Lazily resolve fragments from graphRegistry to support circular dependencies. memoized = null; lastNetworkStatus = null; data = null; + name = ''; + + get graphqlRegistry() { + return this.context.graphqlRegistry; + } + + resolveDocument(documentOrCallback) { + const document = typeof documentOrCallback === 'function' + ? documentOrCallback(this.props, this.context) + : documentOrCallback; + return this.graphqlRegistry.resolveFragments(document); + } emitWhenNeeded(data) { const {variables, networkStatus} = data; @@ -93,11 +103,12 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { refetch: data.refetch, updateQuery: data.updateQuery, subscribeToMore: (stmArgs) => { + const resolvedDocument = this.resolveDocument(stmArgs.document); // Resolve document fragments before passing it to `apollo-client`. return data.subscribeToMore({ ...stmArgs, - document: resolveFragments(stmArgs.document), + document: resolvedDocument, onError: (err) => { if (stmArgs.onErr) { return stmArgs.onErr(err); @@ -107,7 +118,8 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { }); }, fetchMore: (lmArgs) => { - const fetchName = getDefinitionName(lmArgs.query); + const resolvedDocument = this.resolveDocument(lmArgs.query); + const fetchName = getDefinitionName(resolvedDocument); this.context.eventEmitter.emit( `query.${name}.fetchMore.${fetchName}.begin`, {variables: lmArgs.variables}); @@ -115,7 +127,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { // Resolve document fragments before passing it to `apollo-client`. return data.fetchMore({ ...lmArgs, - query: resolveFragments(lmArgs.query), + query: resolvedDocument, }) .then((res) => { this.context.eventEmitter.emit( @@ -156,7 +168,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { const base = (typeof this.wrappedConfig.options === 'function') ? this.wrappedConfig.options(data) : this.wrappedConfig.options; - const configs = getQueryOptions(name); + const configs = this.graphqlRegistry.getQueryOptions(name); const reducerCallbacks = [base.reducer || ((i) => i)] .concat(...configs.map((cfg) => cfg.reducer)) @@ -178,8 +190,10 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { getWrapped = () => { if (!this.memoized) { + const resolvedDocument = this.resolveDocument(document); + this.name = getDefinitionName(resolvedDocument); this.memoized = graphql( - resolveFragments(document), + resolvedDocument, {...this.wrappedConfig, options: this.wrappedOptions}, )(WrappedComponent); } diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index 6a18695f7..0aa2c4c35 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -6,10 +6,12 @@ import {createReduxEmitter} from './events'; import {createRestClient} from './rest'; import thunk from 'redux-thunk'; import {loadTranslations} from './i18n'; -import {getTranslations, getReducers} from '../helpers/plugins'; import bowser from 'bowser'; import * as Storage from '../helpers/storage'; import {BASE_PATH} from 'coral-framework/constants/url'; +import {createPluginsService} from './plugins'; +import {createGraphQLRegistry} from './graphqlRegistry'; +import globalFragments from 'coral-framework/graphql/fragments'; /** * getAuthToken returns the active auth token or null @@ -34,7 +36,7 @@ const getAuthToken = (store) => { return null; }; -export function createContext(reducers, plugins) { +export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}}) { const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); let store = null; @@ -56,16 +58,28 @@ export function createContext(reducers, plugins) { liveUri: `${protocol}://${location.host}${BASE_PATH}api/v1/live`, token, }); + const plugins = createPluginsService(pluginsConfig); + const graphqlRegistry = createGraphQLRegistry(plugins.getSlotFragments.bind(plugins)); const context = { client, pym, plugins, eventEmitter, rest, + graphqlRegistry, }; + // Load framework fragments. + Object.keys(globalFragments).forEach((key) => graphqlRegistry.addFragment(key, globalFragments[key])); + + // Register graphql extension + graphqlRegistry.add(graphqlExtension); + + // Register plugin graphql extensions. + plugins.getGraphQLExtensions().forEach((ext) => graphqlRegistry.add(ext)); + // Load plugin translations. - getTranslations(plugins).forEach((t) => loadTranslations(t)); + plugins.getTranslations().forEach((t) => loadTranslations(t)); // Pass any events through our parent. eventEmitter.onAny((eventName, value) => { @@ -74,7 +88,7 @@ export function createContext(reducers, plugins) { const finalReducers = { ...reducers, - ...getReducers(plugins), + ...plugins.getReducers(), apollo: client.reducer(), }; diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index a354d68bc..ebd248f0d 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -64,11 +64,6 @@ export function createClient(options = {}) { }); client.resetWebsocket = () => { - if (wsClient === null) { - - // Nothing to reset! - return; - } // Close socket connection which will also unregister subscriptions on the server-side. wsClient.close(); diff --git a/client/coral-framework/services/graphqlRegistry.js b/client/coral-framework/services/graphqlRegistry.js index 104ec41dd..5ca549183 100644 --- a/client/coral-framework/services/graphqlRegistry.js +++ b/client/coral-framework/services/graphqlRegistry.js @@ -1,6 +1,4 @@ import {getDefinitionName, mergeDocuments} from 'coral-framework/utils'; -import {getGraphQLExtensions, getSlotFragments} from 'coral-framework/helpers/plugins'; -import globalFragments from 'coral-framework/graphql/fragments'; import uniq from 'lodash/uniq'; import {gql} from 'react-apollo'; @@ -14,272 +12,262 @@ import {gql} from 'react-apollo'; */ gql.disableFragmentWarnings(); -const fragments = {}; -const mutationOptions = {}; -const queryOptions = {}; - const getTypeName = (ast) => ast.definitions[0].typeCondition.name.value; -/** - * Add fragment - * - * Example: - * addFragment('MyFragment', gql` - * fragment Plugin_MyFragment on Comment { - * body - * } - * `); - */ -export function addFragment(key, document) { - const type = getTypeName(document); - const name = getDefinitionName(document); - if (!(key in fragments)) { - fragments[key] = {type, names: [name], documents: [document]}; - } else { - if (type !== fragments[key].type) { - console.error(`Type mismatch ${type} !== ${fragments[key].type}`); +class GraphQLRegistry { + fragments = {}; + mutationOptions = {}; + queryOptions = {}; + + constructor(getSlotFragments) { + this.getSlotFragments = getSlotFragments; + } + + /** + * Add fragment + * + * Example: + * addFragment('MyFragment', gql` + * fragment Plugin_MyFragment on Comment { + * body + * } + * `); + */ + addFragment(key, document) { + const type = getTypeName(document); + const name = getDefinitionName(document); + if (!(key in this.fragments)) { + this.fragments[key] = {type, names: [name], documents: [document]}; + } else { + if (type !== this.fragments[key].type) { + console.error(`Type mismatch ${type} !== ${this.fragments[key].type}`); + } + this.fragments[key].names.push(name); + this.fragments[key].documents.push(document); } - fragments[key].names.push(name); - fragments[key].documents.push(document); - } -} - -/** - * Add mutation options. - * - * Example: - * // state is the current redux state, which is sometimes - * // necessary to fill the optimistic response. - * addMutationOptions('PostComment', ({variables, state}) => ({ - * optimisticResponse: { - * CreateComment: { - * extra: '', - * }, - * }, - * refetchQueries: [], - * updateQueries: { - * EmbedQuery: (previous, data) => { - * return previous; - * }, - * }, - * update: (proxy, result) => { - * }, - * }) - */ -export function addMutationOptions(key, config) { - if (!(key in mutationOptions)) { - mutationOptions[key] = [config]; - } else { - mutationOptions[key].push(config); - } -} - -/** - * Add query options. - * - * Example: - * addQueryOptions('EmbedQuery', { - * reducer: (previousResult, action, variables) => previousResult, - * }); - */ -export function addQueryOptions(key, config) { - if (!(key in queryOptions)) { - queryOptions[key] = [config]; - } else { - queryOptions[key].push(config); - } -} - -/** - * Add all fragments, mutation options, and query options defined in the object. - * - * Example: - * add({ - * fragments: { - * CreateCommentResponse: gql` - * fragment CoralRandomEmoji_CreateCommentResponse on CreateCommentResponse { - * [...] - * }`, - * }, - * mutations: { - * // state is the current redux state, which is sometimes - * // necessary to fill the optimistic response. - * PostComment: ({variables, state}) => ({ - * optimisticResponse: { - * [...] - * }, - * refetchQueries: [], - * updateQueries: { - * EmbedQuery: (previous, data) => { - * return previous; - * }, - * }, - * update: (proxy, result) => { - * }, - * }) - * }, - * queries: { - * EmbedQuery: { - * reducer: (previousResult, action, variables) => { - * return previousResult; - * }, - * }, - * }, - * }); - */ -export function add(extension) { - Object.keys(extension.fragments || []).forEach((key) => addFragment(key, extension.fragments[key])); - Object.keys(extension.mutations || []).forEach((key) => addMutationOptions(key, extension.mutations[key])); - Object.keys(extension.queries || []).forEach((key) => addQueryOptions(key, extension.queries[key])); -} - -/** - * Get a list of mutation options. - */ -export function getMutationOptions(key) { - init(); - return mutationOptions[key] || []; -} - -/** - * Get a list of query options. - */ -export function getQueryOptions(key) { - init(); - return queryOptions[key] || []; -} - -/** - * getSlotFragmentDocument handles `key`s in the form of TalkSlot_SlotName_Resource. - * It parses the slot name and the resource and usees the plugin API to assemble - * the fragment document. - */ -function getSlotFragmentDocument(key) { - const match = key.match(/TalkSlot_(.*)_(.*)/); - if (!match) { - return ''; } - const slot = match[1][0].toLowerCase() + match[1].substr(1); - const resource = match[2]; - const documents = getSlotFragments(slot, resource); - - if (documents.length === 0) { - return ''; - } - - const names = documents.map((d) => getDefinitionName(d)); - const typeName = getTypeName(documents[0]); - - // Assemble arguments for `gql` to call it directly without using template literals. - const main = ` - fragment ${key} on ${typeName} { - ...${names.join('\n...')}\n + /** + * Add mutation options. + * + * Example: + * // state is the current redux state, which is sometimes + * // necessary to fill the optimistic response. + * addMutationOptions('PostComment', ({variables, state}) => ({ + * optimisticResponse: { + * CreateComment: { + * extra: '', + * }, + * }, + * refetchQueries: [], + * updateQueries: { + * EmbedQuery: (previous, data) => { + * return previous; + * }, + * }, + * update: (proxy, result) => { + * }, + * }) + */ + addMutationOptions(key, config) { + if (!(key in this.mutationOptions)) { + this.mutationOptions[key] = [config]; + } else { + this.mutationOptions[key].push(config); } - `; - return mergeDocuments([main, ...documents]); -} - -/** - * getRegistryFragmentDocument assembles a fragment document using - * all registered fragment under given `key`. - */ -function getRegistryFragmentDocument(key) { - if (!(key in fragments)) { - return ''; } - let documents = fragments[key].documents; - let fields = `...${fragments[key].names.join('\n...')}\n`; - - // Assemble arguments for `gql` to call it directly without using template literals. - const main = ` - fragment ${key} on ${fragments[key].type} { - ${fields} + /** + * Add query options. + * + * Example: + * addQueryOptions('EmbedQuery', { + * reducer: (previousResult, action, variables) => previousResult, + * }); + */ + addQueryOptions(key, config) { + if (!(key in this.queryOptions)) { + this.queryOptions[key] = [config]; + } else { + this.queryOptions[key].push(config); } - `; - return mergeDocuments([main, ...documents]); -} + } -/** - * getFragmentDocument returns a fragment that assembles all registered - * fragments under given `key` or if `key` refers to Slot fragments it will - * return the slot fragments specified by this key. - */ -export function getFragmentDocument(key) { - init(); - return getRegistryFragmentDocument(key) || getSlotFragmentDocument(key); -} + /** + * Add all fragments, mutation options, and query options defined in the object. + * + * Example: + * add({ + * fragments: { + * CreateCommentResponse: gql` + * fragment CoralRandomEmoji_CreateCommentResponse on CreateCommentResponse { + * [...] + * }`, + * }, + * mutations: { + * // state is the current redux state, which is sometimes + * // necessary to fill the optimistic response. + * PostComment: ({variables, state}) => ({ + * optimisticResponse: { + * [...] + * }, + * refetchQueries: [], + * updateQueries: { + * EmbedQuery: (previous, data) => { + * return previous; + * }, + * }, + * update: (proxy, result) => { + * }, + * }) + * }, + * queries: { + * EmbedQuery: { + * reducer: (previousResult, action, variables) => { + * return previousResult; + * }, + * }, + * }, + * }); + */ + add(extension) { + Object.keys(extension.fragments || []).forEach((key) => this.addFragment(key, extension.fragments[key])); + Object.keys(extension.mutations || []).forEach((key) => this.addMutationOptions(key, extension.mutations[key])); + Object.keys(extension.queries || []).forEach((key) => this.addQueryOptions(key, extension.queries[key])); + } -// The fragments and configs are lazily loaded to allow circular dependencies to work. -// TODO: We might want to change this to an explicit add after we have lazy Queries and Mutations. -let initialized = false; + /** + * Get a list of mutation options. + */ + getMutationOptions(key) { + return this.mutationOptions[key] || []; + } -function init() { - if (initialized) { return; } - initialized = true; + /** + * Get a list of query options. + */ + getQueryOptions(key) { + return this.queryOptions[key] || []; + } - // Add fragments from framework. - [globalFragments].forEach((map) => - Object.keys(map).forEach((key) => addFragment(key, map[key])) - ); - - // Add configs from plugins. - getGraphQLExtensions().forEach((ext) => add(ext)); -} - -/** - * resolveFragments finds fragment spread names and attachs - * the related fragment document to the given root document. - */ -export function resolveFragments(document) { - if (document.loc.source) { - - // Remember keys that we have already resolved. - const resolvedKeys = []; - - // Spreads from slots that are empty and need to be removed. - // (works around the issue that we don't know the resource type - // if we don't have a fragment) - const spreadsToBeRemoved = []; - - // fragments to be attached. - const subFragments = []; - - // body contains the final result. - let body = document.loc.source.body; - - let done = false; - while (!done) { - done = true; - - const matchedSubFragments = body.match(/\.\.\.([_a-zA-Z][_a-zA-Z0-9]*)/g) || []; - uniq(matchedSubFragments.map((f) => f.replace('...', ''))) - .filter((key) => resolvedKeys.indexOf(key) === -1) - .forEach((key) => { - const doc = getFragmentDocument(key); - if (doc) { - subFragments.push(doc); - - // We found a new fragment, so we are not done yet. - done = false; - } else if(key.startsWith('TalkSlot_')) { - spreadsToBeRemoved.push(key); - } - resolvedKeys.push(key); - }); - - body = mergeDocuments([body, ...subFragments]).loc.source.body; + /** + * getSlotFragmentDocument handles `key`s in the form of TalkSlot_SlotName_Resource. + * It parses the slot name and the resource and usees the plugin API to assemble + * the fragment document. + */ + getSlotFragmentDocument(key) { + const match = key.match(/TalkSlot_(.*)_(.*)/); + if (!match) { + return ''; } - spreadsToBeRemoved.forEach((key) => { - const regex = new RegExp(`\\.\\.\\.${key}\n`, 'g'); - body = body.replace(regex, ''); - }); + const slot = match[1][0].toLowerCase() + match[1].substr(1); + const resource = match[2]; + const documents = this.getSlotFragments(slot, resource); - return gql`${body}`; - } else { - console.warn('Can only resolve fragments from documents definied using the gql tag.'); + if (documents.length === 0) { + return ''; + } + + const names = documents.map((d) => getDefinitionName(d)); + const typeName = getTypeName(documents[0]); + + // Assemble arguments for `gql` to call it directly without using template literals. + const main = ` + fragment ${key} on ${typeName} { + ...${names.join('\n...')}\n + } + `; + return mergeDocuments([main, ...documents]); + } + + /** + * getRegistryFragmentDocument assembles a fragment document using + * all registered fragment under given `key`. + */ + getRegistryFragmentDocument(key) { + if (!(key in this.fragments)) { + return ''; + } + + let documents = this.fragments[key].documents; + let fields = `...${this.fragments[key].names.join('\n...')}\n`; + + // Assemble arguments for `gql` to call it directly without using template literals. + const main = ` + fragment ${key} on ${this.fragments[key].type} { + ${fields} + } + `; + return mergeDocuments([main, ...documents]); + } + + /** + * getFragmentDocument returns a fragment that assembles all registered + * fragments under given `key` or if `key` refers to Slot fragments it will + * return the slot fragments specified by this key. + */ + getFragmentDocument(key) { + return this.getRegistryFragmentDocument(key) || this.getSlotFragmentDocument(key); + } + + /** + * resolveFragments finds fragment spread names and attachs + * the related fragment document to the given root document. + */ + resolveFragments(document) { + if (document.loc.source) { + + // Remember keys that we have already resolved. + const resolvedKeys = []; + + // Spreads from slots that are empty and need to be removed. + // (works around the issue that we don't know the resource type + // if we don't have a fragment) + const spreadsToBeRemoved = []; + + // fragments to be attached. + const subFragments = []; + + // body contains the final result. + let body = document.loc.source.body; + + let done = false; + while (!done) { + done = true; + + const matchedSubFragments = body.match(/\.\.\.([_a-zA-Z][_a-zA-Z0-9]*)/g) || []; + uniq(matchedSubFragments.map((f) => f.replace('...', ''))) + .filter((key) => resolvedKeys.indexOf(key) === -1) + .forEach((key) => { + const doc = this.getFragmentDocument(key); + if (doc) { + subFragments.push(doc); + + // We found a new fragment, so we are not done yet. + done = false; + } else if(key.startsWith('TalkSlot_')) { + spreadsToBeRemoved.push(key); + } + resolvedKeys.push(key); + }); + + body = mergeDocuments([body, ...subFragments]).loc.source.body; + } + + spreadsToBeRemoved.forEach((key) => { + const regex = new RegExp(`\\.\\.\\.${key}\n`, 'g'); + body = body.replace(regex, ''); + }); + + return gql`${body}`; + } else { + console.warn('Can only resolve fragments from documents definied using the gql tag.'); + } + return document; } - return document; +} + +export function createGraphQLRegistry(getSlotFragments) { + return new GraphQLRegistry(getSlotFragments); } diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js new file mode 100644 index 000000000..f51e33702 --- /dev/null +++ b/client/coral-framework/services/plugins.js @@ -0,0 +1,174 @@ +import React from 'react'; +import uniq from 'lodash/uniq'; +import pick from 'lodash/pick'; +import merge from 'lodash/merge'; +import flattenDeep from 'lodash/flattenDeep'; +import isEmpty from 'lodash/isEmpty'; +import flatten from 'lodash/flatten'; +import mapValues from 'lodash/mapValues'; +import {getDisplayName} from 'coral-framework/helpers/hoc'; +import camelize from '../helpers/camelize'; +import uuid from 'uuid/v4'; + +// This is returned for pluginConfig when it is empty. +const emptyConfig = {}; + +// Memoize the warnings so we only show them once. +const memoizedWarnings = []; + +// withWarnings decorates the props of queryData with a proxy that +// prints a warning when accessing deeper props. +function withWarnings(component, queryData) { + if (process.env.NODE_ENV !== 'production' && window.Proxy) { + + // Show warnings when accessing queryData only when not in production. + return mapValues(queryData, (value, key) => { + + // Keep null values.. + if (!queryData[key]) { + return queryData[key]; + } + return new Proxy(queryData[key], { + get(target, name) { + + // Only care about the components defined in the plugins. + if (component.talkPluginName) { + const warning = `'${getDisplayName(component)}' of '${component.talkPluginName}' accessed '${key}.${name}' but did not define fragments using the withFragment HOC`; + if (memoizedWarnings.indexOf(warning) === -1) { + console.warn(warning); + memoizedWarnings.push(warning); + } + } + return queryData[key][name]; + } + }); + }); + } + + return queryData; +} + +function addMetaDataToSlotComponents(plugins) { + + // Add talkPluginName to Slot Components. + plugins.forEach((plugin) => { + const slots = plugin.module.slots; + slots && Object.keys(slots).forEach((slot) => { + slots[slot].forEach((component) => { + + // Attach plugin name to the component + component.talkPluginName = plugin.name; + + // Attach uuid to the component + component.talkUuid = uuid(); + }); + }); + }); +} + +class PluginsService { + constructor(plugins) { + this.plugins = plugins; + addMetaDataToSlotComponents(plugins); + } + + getSlotComponents(slot, reduxState, props = {}, queryData = {}) { + const pluginConfig = reduxState.config.plugin_config || emptyConfig; + return flatten(this.plugins + + // Filter out components that have slots and have been disabled in `plugin_config` + .filter((o) => o.module.slots && (!pluginConfig || !pluginConfig[o.name] || !pluginConfig[o.name].disable_components)) + + .filter((o) => o.module.slots[slot]) + .map((o) => o.module.slots[slot]) + ) + .filter((component) => { + if(!component.isExcluded) { + return true; + } + let resolvedProps = this.getSlotComponentProps(component, reduxState, props, queryData); + if (component.mapStateToProps) { + resolvedProps = {...resolvedProps, ...component.mapStateToProps(reduxState)}; + } + return !component.isExcluded(resolvedProps); + }); + } + + isSlotEmpty(slot, reduxState, props = {}, queryData = {}) { + return this.getSlotComponents(slot, reduxState, props, queryData).length === 0; + } + + /** + * getSlotComponentProps calculate the props we would pass to the slot component. + * query datas are only passed to the component if it is defined in `component.fragments`. + */ + getSlotComponentProps(component, reduxState, props, queryData) { + const pluginConfig = reduxState.config.plugin_config || emptyConfig; + return { + ...props, + config: pluginConfig, + ...( + component.fragments + ? pick(queryData, Object.keys(component.fragments)) + : withWarnings(component, queryData) + ) + }; + } + + /** + * Returns React Elements for given slot. + */ + getSlotElements(slot, reduxState, props = {}, queryData = {}) { + return this.getSlotComponents(slot, reduxState, props, queryData) + .map((component, i) => { + return React.createElement(component, {key: i, ...this.getSlotComponentProps(component, reduxState, props, queryData)}); + }); + } + + getSlotFragments(slot, part) { + const components = uniq(flattenDeep(this.plugins + .filter((o) => o.module.slots ? o.module.slots[slot] : false) + .map((o) => o.module.slots[slot]) + )); + + const documents = components + .map((c) => c.fragments) + .filter((fragments) => fragments && fragments[part]) + .reduce((res, fragments) => { + res.push(fragments[part]); + return res; + }, []); + + return documents; + } + + getGraphQLExtensions() { + return this.plugins + .map((o) => pick(o.module, ['mutations', 'queries', 'fragments'])) + .filter((o) => !isEmpty(o)); + } + + getModQueueConfigs() { + return merge(...this.plugins + .map((o) => o.module.modQueues) + .filter((o) => o)); + } + + getTranslations() { + return this.plugins + .map((o) => o.module.translations) + .filter((o) => o); + } + + getReducers() { + return merge( + ...this.plugins + .filter((o) => o.module.reducer) + .map((o) => ({[camelize(o.name)] : o.module.reducer})) + ); + } +} + +export function createPluginsService(plugins) { + return new PluginsService(plugins); +} diff --git a/plugin-api/beta/client/services/index.js b/plugin-api/beta/client/services/index.js index 54ad90fe7..4d2281dc8 100644 --- a/plugin-api/beta/client/services/index.js +++ b/plugin-api/beta/client/services/index.js @@ -1,9 +1,3 @@ export {t, timeago} from 'coral-framework/services/i18n'; export {can} from 'coral-framework/services/perms'; -import {isSlotEmpty as ise} from 'coral-framework/helpers/plugins'; -// @TODO: Deprecated. -export function isSlotEmpty(...args) { - console.warn('A plugin is using `isSlotEmpty` which has been deprecated, please port to the new API using the `IfSlotIsEmpty` and `IfSlotIsNotEmpty` components.'); - return ise(...args); -} From a882b3650103785991f30d82512a0cc21f555253 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 02:20:43 +0700 Subject: [PATCH 06/16] Fix connection bug --- client/coral-framework/services/client.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index ebd248f0d..ce83d501f 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -11,13 +11,17 @@ export const apolloErrorReporter = () => (next) => (action) => { return next(action); }; +function resolveToken(token) { + return typeof token === 'function' ? token() : token; +} + export function createClient(options = {}) { const {token, uri, liveUri, ...apolloOptions} = options; const wsClient = new SubscriptionClient(liveUri, { reconnect: true, lazy: true, connectionParams: { - token, + get token() { return resolveToken(token); }, } }); @@ -34,7 +38,7 @@ export function createClient(options = {}) { req.options.headers = {}; // Create the header object if needed. } - let authToken = typeof token === 'function' ? token() : token; + let authToken = resolveToken(token); if (authToken) { req.options.headers['authorization'] = `Bearer ${authToken}`; } From a9347226722c9e48672479d9fe625d1c9843c14c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 02:23:18 +0700 Subject: [PATCH 07/16] Cleanup --- client/coral-admin/src/index.js | 8 +++----- client/coral-embed-stream/src/index.js | 4 +--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index 8b90f5acf..f433af4c2 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -9,14 +9,12 @@ import 'react-mdl/extra/material.js'; import graphqlExtension from './graphql'; import pluginsConfig from 'pluginsConfig'; -const context = createContext({reducers, graphqlExtension, pluginsConfig}); - smoothscroll.polyfill(); +const context = createContext({reducers, graphqlExtension, pluginsConfig}); + render( - + , document.querySelector('#root') diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index dca4e31b4..5d72ea3e7 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -52,9 +52,7 @@ if (!window.opener) { } render( - + , document.querySelector('#talk-embed-stream-container') From 1ee7c8167587f585807ca9eba75fbe99d64dd41d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 02:24:26 +0700 Subject: [PATCH 08/16] Remove admin store impl --- client/coral-admin/src/services/store.js | 31 ------------------------ 1 file changed, 31 deletions(-) delete mode 100644 client/coral-admin/src/services/store.js diff --git a/client/coral-admin/src/services/store.js b/client/coral-admin/src/services/store.js deleted file mode 100644 index ca29c15c8..000000000 --- a/client/coral-admin/src/services/store.js +++ /dev/null @@ -1,31 +0,0 @@ -import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; -import thunk from 'redux-thunk'; -import mainReducer from '../reducers'; -import {getClient} from 'coral-framework/services/client'; - -const middlewares = [ - applyMiddleware(getClient().middleware()), - applyMiddleware(thunk) -]; - -if (window.devToolsExtension) { - - // we can't have the last argument of compose() be undefined - middlewares.push(window.devToolsExtension()); -} - -const coralReducers = { - ...mainReducer, - apollo: getClient().reducer() -}; - -const store = createStore( - combineReducers(coralReducers), - {}, - compose(...middlewares) -); - -store.coralReducers = coralReducers; - -window.coralStore = store; -export default store; From f428bfa6c9cfc1aab7bfb46f4db6ac42d241170d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 03:16:19 +0700 Subject: [PATCH 09/16] WIP --- .../coral-framework/actions/notification.js | 24 ++++++++++++------- .../components/TalkProvider.js | 2 ++ .../coral-framework/constants/notification.js | 3 +-- client/coral-framework/services/bootstrap.js | 9 ++++++- .../coral-framework/services/notification.js | 13 ++++++++++ 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 client/coral-framework/services/notification.js diff --git a/client/coral-framework/actions/notification.js b/client/coral-framework/actions/notification.js index 57a7950f5..f940ab3b4 100644 --- a/client/coral-framework/actions/notification.js +++ b/client/coral-framework/actions/notification.js @@ -1,12 +1,18 @@ -import pym from '../services/pym'; import * as actions from '../constants/notification'; -export const addNotification = (notifType, text) => { - pym.sendMessage('coral-alert', `${notifType}|${text}`); - return {type: actions.ADD_NOTIFICATION, notifType, text}; -}; - -export const clearNotification = () => { - pym.sendMessage('coral-clear-notification'); - return {type: actions.CLEAR_NOTIFICATION}; +export const notify = (kind, msg) => (dispatch, _, {notification}) => { + switch (kind) { + case 'error': + notification.error(msg); + break; + case 'info': + notification.info(msg); + break; + case 'success': + notification.success(msg); + break; + default: + throw new Error(`Unknown notification kind ${kind}`); + } + dispatch({type: actions.NOTIFY, kind, msg}); }; diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index 1350244f6..b68fc7a1a 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -10,6 +10,7 @@ class TalkProvider extends React.Component { plugins: this.props.plugins, rest: this.props.rest, graphqlRegistry: this.props.graphqlRegistry, + notification: this.props.notification, }; } @@ -29,6 +30,7 @@ TalkProvider.childContextTypes = { plugins: PropTypes.object, rest: PropTypes.func, graphqlRegistry: PropTypes.object, + notification: PropTypes.object, }; export default TalkProvider; diff --git a/client/coral-framework/constants/notification.js b/client/coral-framework/constants/notification.js index a7334119a..af5828622 100644 --- a/client/coral-framework/constants/notification.js +++ b/client/coral-framework/constants/notification.js @@ -1,2 +1 @@ -export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'; -export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION'; +export const NOTIFY = 'NOTIFY'; diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index 0aa2c4c35..aa85e9d21 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -10,6 +10,7 @@ import bowser from 'bowser'; import * as Storage from '../helpers/storage'; import {BASE_PATH} from 'coral-framework/constants/url'; import {createPluginsService} from './plugins'; +import {createNotificationService} from './notification'; import {createGraphQLRegistry} from './graphqlRegistry'; import globalFragments from 'coral-framework/graphql/fragments'; @@ -36,7 +37,7 @@ const getAuthToken = (store) => { return null; }; -export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}}) { +export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification}) { const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); let store = null; @@ -60,6 +61,11 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi }); const plugins = createPluginsService(pluginsConfig); const graphqlRegistry = createGraphQLRegistry(plugins.getSlotFragments.bind(plugins)); + if (!notification) { + + // Use default notification service (pym based) + notification = createNotificationService(pym); + } const context = { client, pym, @@ -67,6 +73,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi eventEmitter, rest, graphqlRegistry, + notification, }; // Load framework fragments. diff --git a/client/coral-framework/services/notification.js b/client/coral-framework/services/notification.js new file mode 100644 index 000000000..fa0e61b3f --- /dev/null +++ b/client/coral-framework/services/notification.js @@ -0,0 +1,13 @@ +export function createNotificationService(pym) { + return { + success(msg) { + pym.sendMessage('coral-alert', `success|${msg}`); + }, + error(msg) { + pym.sendMessage('coral-alert', `error|${msg}`); + }, + info(msg) { + pym.sendMessage('coral-alert', `info|${msg}`); + }, + }; +} From 89037a2e051196b04c2f452f7b86aa50bf7574f6 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 03:22:56 +0700 Subject: [PATCH 10/16] Refactor notification --- .../coral-embed-stream/src/actions/asset.js | 6 ++-- client/coral-embed-stream/src/actions/auth.js | 4 +-- .../src/components/AllCommentsPane.js | 6 ++-- .../src/components/Comment.js | 20 ++++++------ .../src/components/EditableCommentContent.js | 10 +++--- .../src/components/Stream.js | 10 +++--- .../src/components/TopRightMenu.js | 6 ++-- .../src/containers/Embed.js | 10 +++--- .../src/containers/Stream.js | 4 +-- .../coral-framework/actions/notification.js | 32 +++++++++++-------- client/coral-framework/utils/index.js | 6 ++++ client/talk-plugin-commentbox/CommentBox.js | 16 +++++----- .../components/FlagButton.js | 2 +- client/talk-plugin-history/CommentHistory.js | 2 +- client/talk-plugin-replies/ReplyBox.js | 6 ++-- 15 files changed, 75 insertions(+), 65 deletions(-) diff --git a/client/coral-embed-stream/src/actions/asset.js b/client/coral-embed-stream/src/actions/asset.js index 6acebeafb..1fc9f09f7 100644 --- a/client/coral-embed-stream/src/actions/asset.js +++ b/client/coral-embed-stream/src/actions/asset.js @@ -1,5 +1,5 @@ import * as actions from '../constants/asset'; -import {addNotification} from 'coral-framework/actions/notification'; +import {notify} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; @@ -16,7 +16,7 @@ export const updateConfiguration = (newConfig) => (dispatch, getState, {rest}) = dispatch(updateAssetSettingsRequest()); rest(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig}) .then(() => { - dispatch(addNotification('success', t('framework.success_update_settings'))); + dispatch(notify('success', t('framework.success_update_settings'))); dispatch(updateAssetSettingsSuccess(newConfig)); }) .catch((error) => { @@ -30,7 +30,7 @@ export const updateOpenStream = (closedBody) => (dispatch, getState, {rest}) => dispatch(fetchAssetRequest()); rest(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody}) .then(() => { - dispatch(addNotification('success', t('framework.success_update_settings'))); + dispatch(notify('success', t('framework.success_update_settings'))); dispatch(fetchAssetSuccess(closedBody)); }) .catch((error) => { diff --git a/client/coral-embed-stream/src/actions/auth.js b/client/coral-embed-stream/src/actions/auth.js index 55f7eb546..bf1679b1c 100644 --- a/client/coral-embed-stream/src/actions/auth.js +++ b/client/coral-embed-stream/src/actions/auth.js @@ -2,7 +2,7 @@ import jwtDecode from 'jwt-decode'; import bowser from 'bowser'; import * as actions from '../constants/auth'; import * as Storage from 'coral-framework/helpers/storage'; -import {addNotification} from 'coral-framework/actions/notification'; +import {notify} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; @@ -387,7 +387,7 @@ export const editName = (username) => (dispatch, _, {rest}) => { return rest('/account/username', {method: 'PUT', body: {username}}) .then(() => { dispatch(editUsernameSuccess()); - dispatch(addNotification('success', t('framework.success_name_update'))); + dispatch(notify('success', t('framework.success_name_update'))); }) .catch((error) => { console.error(error); diff --git a/client/coral-embed-stream/src/components/AllCommentsPane.js b/client/coral-embed-stream/src/components/AllCommentsPane.js index c141f0618..cb7c3cbb5 100644 --- a/client/coral-embed-stream/src/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/components/AllCommentsPane.js @@ -87,7 +87,7 @@ class AllCommentsPane extends React.Component { }) .catch((error) => { this.setState({loadingState: 'error'}); - forEachError(error, ({msg}) => {this.props.addNotification('error', msg);}); + forEachError(error, ({msg}) => {this.props.notify('error', msg);}); }); } @@ -129,7 +129,7 @@ class AllCommentsPane extends React.Component { ignoreUser, setActiveReplyBox, activeReplyBox, - addNotification, + notify, disableReply, postComment, asset, @@ -166,7 +166,7 @@ class AllCommentsPane extends React.Component { disableReply={disableReply} setActiveReplyBox={setActiveReplyBox} activeReplyBox={activeReplyBox} - addNotification={addNotification} + notify={notify} depth={0} postComment={postComment} asset={asset} diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index bb6fee981..522e8f00b 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -152,7 +152,7 @@ export default class Comment extends React.Component { deleteAction: PropTypes.func.isRequired, parentId: PropTypes.string, highlighted: PropTypes.string, - addNotification: PropTypes.func.isRequired, + notify: PropTypes.func.isRequired, postComment: PropTypes.func.isRequired, depth: PropTypes.number.isRequired, liveUpdates: PropTypes.bool, @@ -205,7 +205,7 @@ export default class Comment extends React.Component { onClickEdit (e) { e.preventDefault(); if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) { - this.props.addNotification('error', t('error.NOT_AUTHORIZED')); + this.props.notify('error', t('error.NOT_AUTHORIZED')); return; } this.setState({isEditing: true}); @@ -235,7 +235,7 @@ export default class Comment extends React.Component { }) .catch((error) => { this.setState({loadingState: 'error'}); - forEachError(error, ({msg}) => {this.props.addNotification('error', msg);}); + forEachError(error, ({msg}) => {this.props.notify('error', msg);}); }); emit('ui.Comment.showMoreReplies', {id}); return; @@ -252,7 +252,7 @@ export default class Comment extends React.Component { if (can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) { this.props.setActiveReplyBox(this.props.comment.id); } else { - this.props.addNotification('error', t('error.NOT_AUTHORIZED')); + this.props.notify('error', t('error.NOT_AUTHORIZED')); } return; } @@ -331,7 +331,7 @@ export default class Comment extends React.Component { deleteAction, disableReply, maxCharCount, - addNotification, + notify, charCountEnable, showSignInDialog, liveUpdates, @@ -475,7 +475,7 @@ export default class Comment extends React.Component { + notify={notify} /> } { !isActive && @@ -487,7 +487,7 @@ export default class Comment extends React.Component { this.state.isEditing ? { if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) { - this.props.addNotification('error', t('error.NOT_AUTHORIZED')); + this.props.notify('error', t('error.NOT_AUTHORIZED')); return; } this.setState({loadingState: 'loading'}); - const {editComment, addNotification, stopEditing} = this.props; + const {editComment, notify, stopEditing} = this.props; if (typeof editComment !== 'function') {return;} let response; try { response = await editComment({body: this.state.body}); this.setState({loadingState: 'success'}); const status = response.data.editComment.comment.status; - notifyForNewCommentStatus(this.props.addNotification, status); + notifyForNewCommentStatus(this.props.notify, status); if (typeof stopEditing === 'function') { stopEditing(); } } catch (error) { this.setState({loadingState: 'error'}); - forEachError(error, ({msg}) => addNotification('error', msg)); + forEachError(error, ({msg}) => notify('error', msg)); } } diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index b37d232f7..a51b999d6 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -59,7 +59,7 @@ class Stream extends React.Component { commentClassNames, root: {asset, asset: {comment, comments, totalCommentCount}}, postComment, - addNotification, + notify, editComment, postFlag, postDontAgree, @@ -147,7 +147,7 @@ class Stream extends React.Component { />} {showCommentBox && this.setState({timesReset: this.state.timesReset + 1}); @@ -40,7 +40,7 @@ export class TopRightMenu extends React.Component { try { await ignoreUser({id}); } catch (error) { - addNotification('error', 'Failed to ignore user'); + notify('error', 'Failed to ignore user'); throw error; } }; diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index a8f6c1b18..71783e64d 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -14,7 +14,7 @@ import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils'; import {withQuery} from 'coral-framework/hocs'; import Embed from '../components/Embed'; import Stream from './Stream'; -import {addNotification} from 'coral-framework/actions/notification'; +import {notify} from 'coral-framework/actions/notification'; import t from 'coral-framework/services/i18n'; import PropTypes from 'prop-types'; import {setActiveTab} from '../actions/embed'; @@ -34,19 +34,19 @@ class EmbedContainer extends React.Component { const newSubscriptions = [{ document: USER_BANNED_SUBSCRIPTION, updateQuery: () => { - addNotification('info', t('your_account_has_been_banned')); + notify('info', t('your_account_has_been_banned')); }, }, { document: USER_SUSPENDED_SUBSCRIPTION, updateQuery: () => { - addNotification('info', t('your_account_has_been_suspended')); + notify('info', t('your_account_has_been_suspended')); }, }, { document: USERNAME_REJECTED_SUBSCRIPTION, updateQuery: () => { - addNotification('info', t('your_username_has_been_rejected')); + notify('info', t('your_username_has_been_rejected')); }, }]; @@ -193,7 +193,7 @@ const mapDispatchToProps = (dispatch) => checkLogin, setActiveTab, fetchAssetSuccess, - addNotification, + notify, focusSignInDialog, blurSignInDialog, hideSignInDialog, diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index c4a3ae736..7be3d09d5 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -26,7 +26,7 @@ import { } from '../graphql/utils'; const {showSignInDialog, editName} = authActions; -const {addNotification} = notificationActions; +const {notify} = notificationActions; class StreamContainer extends React.Component { subscriptions = []; @@ -303,7 +303,7 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => bindActionCreators({ showSignInDialog, - addNotification, + notify, setActiveReplyBox, editName, viewAllComments, diff --git a/client/coral-framework/actions/notification.js b/client/coral-framework/actions/notification.js index f940ab3b4..872936d15 100644 --- a/client/coral-framework/actions/notification.js +++ b/client/coral-framework/actions/notification.js @@ -1,18 +1,22 @@ import * as actions from '../constants/notification'; export const notify = (kind, msg) => (dispatch, _, {notification}) => { - switch (kind) { - case 'error': - notification.error(msg); - break; - case 'info': - notification.info(msg); - break; - case 'success': - notification.success(msg); - break; - default: - throw new Error(`Unknown notification kind ${kind}`); - } - dispatch({type: actions.NOTIFY, kind, msg}); + const messages = Array.isArray(msg) ? msg : [msg]; + + messages.forEach((message) => { + switch (kind) { + case 'error': + notification.error(message); + break; + case 'info': + notification.info(message); + break; + case 'success': + notification.success(message); + break; + default: + throw new Error(`Unknown notification kind ${kind}`); + } + dispatch({type: actions.NOTIFY, kind, message}); + }); }; diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 2b4b6378f..00e95243e 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -147,6 +147,12 @@ export function forEachError(error, callback) { }); } +export function getErrorMessages(error) { + const result = []; + forEachError(error, ({msg}) => result.push(msg)); + return result; +} + const ascending = (a, b) => { const dateA = new Date(a.created_at); const dateB = new Date(b.created_at); diff --git a/client/talk-plugin-commentbox/CommentBox.js b/client/talk-plugin-commentbox/CommentBox.js index 084e67fc4..d4cc44f22 100644 --- a/client/talk-plugin-commentbox/CommentBox.js +++ b/client/talk-plugin-commentbox/CommentBox.js @@ -12,11 +12,11 @@ export const name = 'talk-plugin-commentbox'; // Given a newly posted comment's status, show a notification to the user // if needed -export const notifyForNewCommentStatus = (addNotification, status) => { +export const notifyForNewCommentStatus = (notify, status) => { if (status === 'REJECTED') { - addNotification('error', t('comment_box.comment_post_banned_word')); + notify('error', t('comment_box.comment_post_banned_word')); } else if (status === 'PREMOD') { - addNotification('success', t('comment_box.comment_post_notif_premod')); + notify('success', t('comment_box.comment_post_notif_premod')); } }; @@ -45,12 +45,12 @@ class CommentBox extends React.Component { postComment, assetId, parentId, - addNotification, + notify, currentUser, } = this.props; if (!can(currentUser, 'INTERACT_WITH_COMMUNITY')) { - addNotification('error', t('error.NOT_AUTHORIZED')); + notify('error', t('error.NOT_AUTHORIZED')); return; } @@ -73,7 +73,7 @@ class CommentBox extends React.Component { // Execute postSubmit Hooks this.state.hooks.postSubmit.forEach((hook) => hook(data)); - notifyForNewCommentStatus(addNotification, postedComment.status); + notifyForNewCommentStatus(notify, postedComment.status); if (commentPostedHandler) { commentPostedHandler(); @@ -81,7 +81,7 @@ class CommentBox extends React.Component { }) .catch((err) => { this.setState({loadingState: 'error'}); - forEachError(err, ({msg}) => addNotification('error', msg)); + forEachError(err, ({msg}) => notify('error', msg)); }); } @@ -186,7 +186,7 @@ CommentBox.propTypes = { currentUser: PropTypes.object.isRequired, isReply: PropTypes.bool.isRequired, canPost: PropTypes.bool, - addNotification: PropTypes.func.isRequired, + notify: PropTypes.func.isRequired, }; const mapStateToProps = ({commentBox}) => ({commentBox}); diff --git a/client/talk-plugin-flags/components/FlagButton.js b/client/talk-plugin-flags/components/FlagButton.js index 1d8438ba0..bf3da6f01 100644 --- a/client/talk-plugin-flags/components/FlagButton.js +++ b/client/talk-plugin-flags/components/FlagButton.js @@ -42,7 +42,7 @@ export default class FlagButton extends Component { this.setState({showMenu: true}); } } else { - this.props.addNotification('error', t('error.NOT_AUTHORIZED')); + this.props.notify('error', t('error.NOT_AUTHORIZED')); } } diff --git a/client/talk-plugin-history/CommentHistory.js b/client/talk-plugin-history/CommentHistory.js index 43ae93f1d..91883cdc8 100644 --- a/client/talk-plugin-history/CommentHistory.js +++ b/client/talk-plugin-history/CommentHistory.js @@ -17,7 +17,7 @@ class CommentHistory extends React.Component { }) .catch((error) => { this.setState({loadingState: 'error'}); - forEachError(error, ({msg}) => {this.props.addNotification('error', msg);}); + forEachError(error, ({msg}) => {this.props.notify('error', msg);}); }); } diff --git a/client/talk-plugin-replies/ReplyBox.js b/client/talk-plugin-replies/ReplyBox.js index 58cb0110d..50bd9a538 100644 --- a/client/talk-plugin-replies/ReplyBox.js +++ b/client/talk-plugin-replies/ReplyBox.js @@ -19,7 +19,7 @@ class ReplyBox extends Component { postComment, assetId, currentUser, - addNotification, + notify, parentId, commentPostedHandler, maxCharCount, @@ -32,7 +32,7 @@ class ReplyBox extends Component { commentPostedHandler={commentPostedHandler} parentId={parentId} onCancel={this.cancelReply} - addNotification={addNotification} + notify={notify} currentUser={currentUser} assetId={assetId} postComment={postComment} @@ -47,7 +47,7 @@ ReplyBox.propTypes = { setActiveReplyBox: PropTypes.func.isRequired, commentPostedHandler: PropTypes.func, parentId: PropTypes.string, - addNotification: PropTypes.func.isRequired, + notify: PropTypes.func.isRequired, postComment: PropTypes.func.isRequired, assetId: PropTypes.string.isRequired }; From a23bad49c693cd621582b49f4b4a25dd14ae1e75 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 03:48:20 +0700 Subject: [PATCH 11/16] Remove global dependency on notification on admin --- .../src/containers/SuspendUserDialog.js | 11 ++++-- client/coral-admin/src/index.js | 5 ++- .../Moderation/containers/Moderation.js | 31 ++++++++++------ .../src/routes/Moderation/graphql.js | 14 +++---- .../coral-admin/src/services/notification.js | 37 ++++++------------- .../client/containers/ModSubscription.js | 4 +- 6 files changed, 50 insertions(+), 52 deletions(-) diff --git a/client/coral-admin/src/containers/SuspendUserDialog.js b/client/coral-admin/src/containers/SuspendUserDialog.js index bd95328a3..f60014832 100644 --- a/client/coral-admin/src/containers/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/SuspendUserDialog.js @@ -5,22 +5,24 @@ import SuspendUserDialog from '../components/SuspendUserDialog'; import {hideSuspendUserDialog} from '../actions/suspendUserDialog'; import {withSetCommentStatus, withSuspendUser} from 'coral-framework/graphql/mutations'; import {compose, gql} from 'react-apollo'; -import * as notification from 'coral-admin/src/services/notification'; import t, {timeago} from 'coral-framework/services/i18n'; import withQuery from 'coral-framework/hocs/withQuery'; +import {getErrorMessages} from 'coral-framework/utils'; import get from 'lodash/get'; +import {notify} from 'coral-framework/actions/notification'; class SuspendUserDialogContainer extends Component { suspendUser = async ({message, until}) => { - const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser} = this.props; + const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props; hideSuspendUserDialog(); try { const result = await suspendUser({id: userId, message, until}); if (result.data.suspendUser.errors) { throw result.data.suspendUser.errors; } - notification.success( + notify( + 'success', t('suspenduser.notify_suspend_until', username, timeago(until)), ); if (commentId && commentStatus && commentStatus !== 'REJECTED') { @@ -33,7 +35,7 @@ class SuspendUserDialogContainer extends Component { } } catch(err) { - notification.showMutationErrors(err); + notify('error', getErrorMessages(err)); } }; @@ -69,6 +71,7 @@ const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId, const mapDispatchToProps = (dispatch) => ({ ...bindActionCreators({ hideSuspendUserDialog, + notify, }, dispatch), }); diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index f433af4c2..2b0a982b4 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -8,10 +8,13 @@ import App from './components/App'; import 'react-mdl/extra/material.js'; import graphqlExtension from './graphql'; import pluginsConfig from 'pluginsConfig'; +import {toast} from 'react-toastify'; +import {createNotificationService} from './services/notification'; smoothscroll.polyfill(); -const context = createContext({reducers, graphqlExtension, pluginsConfig}); +const notification = createNotificationService(toast); +const context = createContext({reducers, graphqlExtension, pluginsConfig, notification}); render( diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index 8df83da2a..0b9fc2f38 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -27,6 +27,7 @@ import { clearState } from 'actions/moderation'; import withQueueConfig from '../hoc/withQueueConfig'; +import {notify} from 'coral-framework/actions/notification'; import {Spinner} from 'coral-ui'; import Moderation from '../components/Moderation'; @@ -54,8 +55,15 @@ function getTab(props) { class ModerationContainer extends Component { subscriptions = []; - handleCommentChange = (root, comment, notify) => { - return handleCommentChange(root, comment, this.props.data.variables.sort, notify, this.props.queueConfig, this.activeTab); + handleCommentChange = (root, comment, notifyText) => { + return handleCommentChange( + root, + comment, + this.props.data.variables.sort, + () => notifyText && this.props.notify('info', notifyText), + this.props.queueConfig, + this.activeTab + ); }; get activeTab() { @@ -79,10 +87,10 @@ class ModerationContainer extends Component { variables, updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => { const user = comment.status_history[comment.status_history.length - 1].assigned_by; - const notify = this.props.auth.user.id === user.id + const notifyText = this.props.auth.user.id === user.id ? '' : t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notify); + return this.handleCommentChange(prev, comment, notifyText); }, }); @@ -91,10 +99,10 @@ class ModerationContainer extends Component { variables, updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => { const user = comment.status_history[comment.status_history.length - 1].assigned_by; - const notify = this.props.auth.user.id === user.id + const notifyText = this.props.auth.user.id === user.id ? '' : t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notify); + return this.handleCommentChange(prev, comment, notifyText); }, }); @@ -102,8 +110,8 @@ class ModerationContainer extends Component { document: COMMENT_EDITED_SUBSCRIPTION, variables, updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => { - const notify = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notify); + const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notifyText); }, }); @@ -112,8 +120,8 @@ class ModerationContainer extends Component { variables, updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => { const user = comment.actions[comment.actions.length - 1].user; - const notify = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notify); + const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notifyText); }, }); @@ -394,7 +402,8 @@ const mapDispatchToProps = (dispatch) => ({ viewUserDetail, setSortOrder, storySearchChange, - clearState + clearState, + notify, }, dispatch), }); diff --git a/client/coral-admin/src/routes/Moderation/graphql.js b/client/coral-admin/src/routes/Moderation/graphql.js index 0d4991f72..2039fb0e5 100644 --- a/client/coral-admin/src/routes/Moderation/graphql.js +++ b/client/coral-admin/src/routes/Moderation/graphql.js @@ -1,5 +1,4 @@ import update from 'immutability-helper'; -import * as notification from 'coral-admin/src/services/notification'; const limit = 10; @@ -92,10 +91,7 @@ function getCommentQueues(comment, queueConfig) { * @param {Object} root current state of the store * @param {Object} comment comment that was changed * @param {string} sort current sort order of the queues - * @param {Object} [notify] show know notifications if set - * @param {string} notify.activeQueue current active queue - * @param {string} notify.text notification text to show - * @param {bool} notify.anyQueue if true show the notification when the comment is shown + * @param {string} notify callback to show notification * in the current active queue besides the 'all' queue. * @param {Object} queueConfig queue configuration * @return {Object} next state of the store @@ -110,7 +106,7 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac if (notificationShown) { return; } - notification.info(notify); + notify(); notificationShown = true; }; @@ -119,13 +115,13 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac if (!queueHasComment(next, queue, comment.id)) { next = addCommentToQueue(next, queue, comment, sort); if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) { - showNotificationOnce(comment); + showNotificationOnce(); } } } else if(queueHasComment(next, queue, comment.id)){ next = removeCommentFromQueue(next, queue, comment.id); if (notify && activeQueue === queue) { - showNotificationOnce(comment); + showNotificationOnce(); } } @@ -134,7 +130,7 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac && queueHasComment(next, queue, comment.id) && activeQueue === queue ) { - showNotificationOnce(comment); + showNotificationOnce(); } }); return next; diff --git a/client/coral-admin/src/services/notification.js b/client/coral-admin/src/services/notification.js index 5e8673060..7ad85692b 100644 --- a/client/coral-admin/src/services/notification.js +++ b/client/coral-admin/src/services/notification.js @@ -1,26 +1,13 @@ -import t from 'coral-framework/services/i18n'; -import {toast} from 'react-toastify'; - -export function success(msg) { - return toast(msg, {type: 'success'}); -} - -export function error(msg) { - return toast(msg, {type: 'error'}); -} - -export function info(msg) { - return toast(msg, {type: 'info'}); -} - -export function showMutationErrors(error) { - console.error(error); - if (error.errors) { - error.errors.forEach((err) => { - toast( - err.translation_key ? t(`error.${err.translation_key}`) : err, - {type: 'error'} - ); - }); - } +export function createNotificationService(toast) { + return { + success(msg) { + toast(msg, {type: 'success'}); + }, + error(msg) { + toast(msg, {type: 'error'}); + }, + info(msg) { + toast(msg, {type: 'info'}); + }, + }; } diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js index 82f323530..0e9fc8c89 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js +++ b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js @@ -21,14 +21,14 @@ class ModSubscription extends React.Component { assetId: this.props.data.variables.asset_id, }, updateQuery: (prev, {subscriptionData: {data: {commentFeatured: {user, comment}}}}) => { - const notify = this.props.user.id === user.id + const notifyText = this.props.user.id === user.id ? '' : t( 'talk-plugin-featured-comments.notify_featured', user.username, prepareNotificationText(comment.body), ); - return this.props.handleCommentChange(prev, comment, notify); + return this.props.handleCommentChange(prev, comment, notifyText); }, }, { From a9e3115bd9901e88a45ddb6e1bd2d1d610fc598e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 03:53:37 +0700 Subject: [PATCH 12/16] Expose `getErrorMessages` in plugin-api --- plugin-api/beta/client/utils/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js index e0ff635e0..eecfe0305 100644 --- a/plugin-api/beta/client/utils/index.js +++ b/plugin-api/beta/client/utils/index.js @@ -3,5 +3,6 @@ export { insertCommentsSorted, getSlotFragmentSpreads, forEachError, + getErrorMessages, getDefinitionName, } from 'coral-framework/utils'; From 0811c9302d375762f0691ae682cdf1b4146296e2 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 17:30:46 +0700 Subject: [PATCH 13/16] Ignore access from react dev tools --- client/coral-framework/services/plugins.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js index f51e33702..5474e919f 100644 --- a/client/coral-framework/services/plugins.js +++ b/client/coral-framework/services/plugins.js @@ -31,8 +31,13 @@ function withWarnings(component, queryData) { return new Proxy(queryData[key], { get(target, name) { + // Detect access from React DevTools and ignore those. + const error = new Error(); + const accessFromDevTools = ['backend.js', 'dehydrate'] + .every((keyword) => error.stack && error.stack.includes(keyword)); + // Only care about the components defined in the plugins. - if (component.talkPluginName) { + if (component.talkPluginName && !accessFromDevTools) { const warning = `'${getDisplayName(component)}' of '${component.talkPluginName}' accessed '${key}.${name}' but did not define fragments using the withFragment HOC`; if (memoizedWarnings.indexOf(warning) === -1) { console.warn(warning); From bf89409f60228fef8c59cc5f7f5f3d197a90d535 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 19:53:06 +0700 Subject: [PATCH 14/16] Remove global dependency on Storage --- client/coral-admin/src/actions/auth.js | 28 +++--- client/coral-admin/src/actions/moderation.js | 8 +- client/coral-admin/src/index.js | 10 ++ client/coral-admin/src/reducers/moderation.js | 7 +- .../Dashboard/components/CountdownTimer.js | 25 +++-- client/coral-embed-stream/src/actions/auth.js | 26 ++++-- .../components/TalkProvider.js | 2 + client/coral-framework/helpers/storage.js | 92 ------------------- client/coral-framework/services/bootstrap.js | 12 ++- client/coral-framework/services/storage.js | 39 ++++++++ 10 files changed, 117 insertions(+), 132 deletions(-) delete mode 100644 client/coral-framework/helpers/storage.js create mode 100644 client/coral-framework/services/storage.js diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 3433fc95a..422258f50 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -1,6 +1,5 @@ import bowser from 'bowser'; import * as actions from '../constants/auth'; -import * as Storage from 'coral-framework/helpers/storage'; import t from 'coral-framework/services/i18n'; import jwtDecode from 'jwt-decode'; @@ -8,7 +7,7 @@ import jwtDecode from 'jwt-decode'; // SIGN IN //============================================================================== -export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client}) => { +export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client, storage}) => { dispatch({type: actions.LOGIN_REQUEST}); const params = { @@ -29,8 +28,8 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, .then(({user, token}) => { if (!user) { - if (!bowser.safari && !bowser.ios) { - Storage.removeItem('token'); + if (!bowser.safari && !bowser.ios && storage) { + storage.removeItem('token'); } return dispatch(checkLoginFailure('not logged in')); } @@ -114,13 +113,13 @@ const checkLoginFailure = (error) => ({ error }); -export const checkLogin = () => (dispatch, _, {rest, client}) => { +export const checkLogin = () => (dispatch, _, {rest, client, storage}) => { dispatch(checkLoginRequest()); return rest('/auth') .then(({user}) => { if (!user) { - if (!bowser.safari && !bowser.ios) { - Storage.removeItem('token'); + if (!bowser.safari && !bowser.ios && storage) { + storage.removeItem('token'); } return dispatch(checkLoginFailure('not logged in')); } @@ -139,9 +138,11 @@ export const checkLogin = () => (dispatch, _, {rest, client}) => { // LOGOUT //============================================================================== -export const logout = () => (dispatch, _, {rest, client}) => { +export const logout = () => (dispatch, _, {rest, client, storage}) => { return rest('/auth', {method: 'DELETE'}).then(() => { - Storage.removeItem('token'); + if (storage) { + storage.removeItem('token'); + } // Reset the websocket. client.resetWebsocket(); @@ -154,10 +155,11 @@ export const logout = () => (dispatch, _, {rest, client}) => { // AUTH TOKEN //============================================================================== -export const handleAuthToken = (token) => (dispatch) => { - Storage.setItem('exp', jwtDecode(token).exp); - Storage.setItem('token', token); - +export const handleAuthToken = (token) => (dispatch, _, {storage}) => { + if (storage) { + storage.setItem('exp', jwtDecode(token).exp); + storage.setItem('token', token); + } dispatch({type: 'HANDLE_AUTH_TOKEN'}); }; diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index 5136db4c3..a3ff2ca3e 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -4,15 +4,17 @@ export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open}); export const singleView = () => ({type: actions.SINGLE_VIEW}); // hide shortcuts note -export const hideShortcutsNote = () => { +export const hideShortcutsNote = () => (dispatch, _, {storage}) => { try { - window.localStorage.setItem('coral:shortcutsNote', 'hide'); + if (storage) { + storage.setItem('coral:shortcutsNote', 'hide'); + } } catch (e) { // above will fail in Safari private mode } - return {type: actions.HIDE_SHORTCUTS_NOTE}; + dispatch({type: actions.HIDE_SHORTCUTS_NOTE}); }; export const setSortOrder = (order) => ({ diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index 2b0a982b4..5df2ce391 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -10,12 +10,22 @@ import graphqlExtension from './graphql'; import pluginsConfig from 'pluginsConfig'; import {toast} from 'react-toastify'; import {createNotificationService} from './services/notification'; +import {hideShortcutsNote} from './actions/moderation'; + +function hidrateStore({store, storage}) { + if (storage && storage.getItem('coral:shortcutsNote') === 'hide') { + store.dispatch(hideShortcutsNote()); + } +} smoothscroll.polyfill(); const notification = createNotificationService(toast); const context = createContext({reducers, graphqlExtension, pluginsConfig, notification}); +// hidrate Store with external data. +hidrateStore(context); + render( diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index e0a608488..3a4a3f5e9 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -5,14 +5,17 @@ const initialState = { modalOpen: false, storySearchVisible: false, storySearchString: '', - shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show', + shortcutsNoteVisible: 'show', sortOrder: 'REVERSE_CHRONOLOGICAL', }; export default function moderation (state = initialState, action) { switch (action.type) { case actions.MODERATION_CLEAR_STATE: - return initialState; + return { + ...initialState, + shortcutsNoteVisible: state.shortcutsNoteVisible, + }; case actions.TOGGLE_MODAL: return { ...state, diff --git a/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js b/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js index 9e1995b7f..6864acb6b 100644 --- a/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js +++ b/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js @@ -5,17 +5,24 @@ import {Icon} from 'coral-ui'; import t from 'coral-framework/services/i18n'; const refreshIntervalSeconds = 60 * 5; +// TODO: refactor out storage code into redux. + class CountdownTimer extends React.Component { + static contextTypes = { + storage: PropTypes.object, + }; + static propTypes = { handleTimeout: PropTypes.func.isRequired } - constructor (props) { - super(props); + constructor (props, context) { + super(props, context); + const {storage} = context; try { - if (window.localStorage.getItem('coral:dashboardNote') === null) { - window.localStorage.setItem('coral:dashboardNote', 'show'); + if (storage && storage.getItem('coral:dashboardNote') === null) { + storage.setItem('coral:dashboardNote', 'show'); } } catch (e) { @@ -24,7 +31,7 @@ class CountdownTimer extends React.Component { this.state = { secondsUntilRefresh: refreshIntervalSeconds, - dashboardNote: window.localStorage.getItem('coral:dashboardNote') || 'show' + dashboardNote: (storage && storage.getItem('coral:dashboardNote')) || 'show' }; } @@ -54,8 +61,11 @@ class CountdownTimer extends React.Component { } dismissNote = () => { + const {storage} = this.context; try { - window.localStorage.setItem('coral:dashboardNote', 'hide'); + if (storage) { + storage.setItem('coral:dashboardNote', 'hide'); + } } catch (e) { // when setItem fails in Safari Private mode @@ -64,7 +74,8 @@ class CountdownTimer extends React.Component { } render () { - const hideReloadNote = window.localStorage.getItem('coral:dashboardNote') === 'hide' || + const {storage} = this.context; + const hideReloadNote = (storage && storage.getItem('coral:dashboardNote') === 'hide') || this.state.dashboardNote === 'hide'; // for Safari Incognito return (

({ // AUTH TOKEN //============================================================================== -export const handleAuthToken = (token) => (dispatch) => { - Storage.setItem('exp', jwtDecode(token).exp); - Storage.setItem('token', token); +export const handleAuthToken = (token) => (dispatch, _, {storage}) => { + if (storage) { + storage.setItem('exp', jwtDecode(token).exp); + storage.setItem('token', token); + } dispatch({type: 'HANDLE_AUTH_TOKEN'}); }; @@ -272,9 +273,12 @@ export const fetchForgotPassword = (email) => (dispatch, getState, {rest}) => { // LOGOUT //============================================================================== -export const logout = () => async (dispatch, _, {rest, client, pym}) => { +export const logout = () => async (dispatch, _, {rest, client, pym, storage}) => { await rest('/auth', {method: 'DELETE'}); - Storage.removeItem('token'); + + if (storage) { + storage.removeItem('token'); + } // Reset the websocket. client.resetWebsocket(); @@ -296,12 +300,14 @@ const checkLoginSuccess = (user, isAdmin) => ({ isAdmin }); -export const checkLogin = () => (dispatch, _, {rest, client, pym}) => { +export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => { dispatch(checkLoginRequest()); rest('/auth') .then((result) => { if (!result.user) { - Storage.removeItem('token'); + if (storage) { + storage.removeItem('token'); + } throw new Error('Not logged in'); } @@ -318,10 +324,10 @@ export const checkLogin = () => (dispatch, _, {rest, client, pym}) => { }) .catch((error) => { console.error(error); - if (error.status && error.status === 401) { + if (error.status && error.status === 401 && storage) { // Unauthorized. - Storage.removeItem('token'); + storage.removeItem('token'); } const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); dispatch(checkLoginFailure(errorMessage)); diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index b68fc7a1a..b553cff45 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -11,6 +11,7 @@ class TalkProvider extends React.Component { rest: this.props.rest, graphqlRegistry: this.props.graphqlRegistry, notification: this.props.notification, + storage: this.props.storage, }; } @@ -31,6 +32,7 @@ TalkProvider.childContextTypes = { rest: PropTypes.func, graphqlRegistry: PropTypes.object, notification: PropTypes.object, + storage: PropTypes.object, }; export default TalkProvider; diff --git a/client/coral-framework/helpers/storage.js b/client/coral-framework/helpers/storage.js deleted file mode 100644 index ec83c3fb3..000000000 --- a/client/coral-framework/helpers/storage.js +++ /dev/null @@ -1,92 +0,0 @@ -let available, error; - -function storageAvailable(type) { - let storage = window[type], x = '__storage_test__'; - try { - storage.setItem(x, x); - storage.removeItem(x); - return true; - } catch (e) { - error = e; - return ( - e instanceof DOMException && - - // everything except Firefox - (e.code === 22 || - - // Firefox - - e.code === 1014 || - - // test name field too, because code might not be present - - // everything except Firefox - e.name === 'QuotaExceededError' || - - // Firefox - e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && - - // acknowledge QuotaExceededError only if there's something already stored - storage.length !== 0 - ); - } -} - -function lazyCheckStorage() { - if (typeof available === 'undefined') { - available = storageAvailable('localStorage'); - } -} - -export function getItem(item = '') { - lazyCheckStorage(); - - if (available) { - return localStorage.getItem(item); - } else { - console.error( - `Cannot get from localStorage. localStorage is not available. ${error}` - ); - } -} - -export function setItem(item = '', value) { - lazyCheckStorage(); - - if (available) { - return localStorage.setItem(item, value); - } else { - console.error( - `Cannot set localStorage. localStorage is not available. ${error}` - ); - } -} - -export function removeItem(item = '') { - lazyCheckStorage(); - - if (available) { - return localStorage.removeItem(item); - } else { - console.error( - `Cannot remove item from localStorage. localStorage is not available. ${error}` - ); - } -} - -export function clear() { - lazyCheckStorage(); - - if (available) { - return localStorage.clear(); - } else { - console.error( - `Cannot clear localStorage. localStorage is not available. ${error}` - ); - } -} - -// window.addEventListener('storage', function(e) { -// const msg = `${e.key} " was changed in page ${e.url} from ${e.oldValue} to ${e.newValue}`; -// console.log(msg); -// }); diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index aa85e9d21..a1b8aec12 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -7,12 +7,12 @@ import {createRestClient} from './rest'; import thunk from 'redux-thunk'; import {loadTranslations} from './i18n'; import bowser from 'bowser'; -import * as Storage from '../helpers/storage'; import {BASE_PATH} from 'coral-framework/constants/url'; import {createPluginsService} from './plugins'; import {createNotificationService} from './notification'; import {createGraphQLRegistry} from './graphqlRegistry'; import globalFragments from 'coral-framework/graphql/fragments'; +import {createStorage} from 'coral-framework/services/storage'; /** * getAuthToken returns the active auth token or null @@ -20,7 +20,7 @@ import globalFragments from 'coral-framework/graphql/fragments'; * browsers that don't allow us to use cross domain iframe local storage. * @return {string|null} */ -const getAuthToken = (store) => { +const getAuthToken = (store, storage) => { let state = store.getState(); if (state.config.auth_token) { @@ -28,10 +28,10 @@ const getAuthToken = (store) => { // if an auth_token exists in config, use it. return state.config.auth_token; - } else if (!bowser.safari && !bowser.ios) { + } else if (!bowser.safari && !bowser.ios && storage) { // Use local storage auth tokens where there's a stable api. - return Storage.getItem('token'); + return storage.getItem('token'); } return null; @@ -40,6 +40,7 @@ const getAuthToken = (store) => { export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification}) { const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); + const storage = createStorage(); let store = null; const token = () => { @@ -48,7 +49,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. - return getAuthToken(store); + return getAuthToken(store, storage); }; const rest = createRestClient({ uri: `${BASE_PATH}api/v1`, @@ -74,6 +75,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi rest, graphqlRegistry, notification, + storage, }; // Load framework fragments. diff --git a/client/coral-framework/services/storage.js b/client/coral-framework/services/storage.js new file mode 100644 index 000000000..1454d2ac5 --- /dev/null +++ b/client/coral-framework/services/storage.js @@ -0,0 +1,39 @@ + +function getStorage(type) { + let storage = window[type], x = '__storage_test__'; + try { + storage.setItem(x, x); + storage.removeItem(x); + } catch (e) { + const ignore = ( + e instanceof DOMException && + + // everything except Firefox + (e.code === 22 || + + // Firefox + + e.code === 1014 || + + // test name field too, because code might not be present + + // everything except Firefox + e.name === 'QuotaExceededError' || + + // Firefox + e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && + + // acknowledge QuotaExceededError only if there's something already stored + storage.length !== 0 + ); + if (!ignore) { + console.warning(e); // eslint-disable-line + return null; + } + } + return storage; +} + +export function createStorage() { + return getStorage('localStorage'); +} From c2b0a60d8af3c55443386a1e7b27d364347889eb Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 20:02:36 +0700 Subject: [PATCH 15/16] Remove global dependency on router history --- client/coral-admin/src/AppRouter.js | 12 ++++++++++-- client/coral-embed-stream/src/AppRouter.js | 12 ++++++++++-- client/coral-framework/components/TalkProvider.js | 2 ++ client/coral-framework/helpers/router.js | 7 ------- client/coral-framework/services/bootstrap.js | 3 +++ client/coral-framework/services/history.js | 12 ++++++++++++ 6 files changed, 37 insertions(+), 11 deletions(-) delete mode 100644 client/coral-framework/helpers/router.js create mode 100644 client/coral-framework/services/history.js diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 9772ebf17..dcd660c25 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -1,6 +1,6 @@ import React from 'react'; import {Router, Route, IndexRedirect, IndexRoute} from 'react-router'; -import {history} from 'coral-framework/helpers/router'; +import PropTypes from 'prop-types'; import Configure from 'routes/Configure'; import Dashboard from 'routes/Dashboard'; @@ -47,6 +47,14 @@ const routes = (

); -const AppRouter = () => ; +class AppRouter extends React.Component { + static contextTypes = { + history: PropTypes.object, + }; + + render() { + return ; + } +} export default AppRouter; diff --git a/client/coral-embed-stream/src/AppRouter.js b/client/coral-embed-stream/src/AppRouter.js index c2e7b2c5d..38093abd8 100644 --- a/client/coral-embed-stream/src/AppRouter.js +++ b/client/coral-embed-stream/src/AppRouter.js @@ -1,6 +1,6 @@ import React from 'react'; import {Router, Route} from 'react-router'; -import {history} from 'coral-framework/helpers/router'; +import PropTypes from 'prop-types'; import Embed from './containers/Embed'; import {LoginContainer} from 'coral-sign-in/containers/LoginContainer'; @@ -12,6 +12,14 @@ const routes = ( ); -const AppRouter = () => ; +class AppRouter extends React.Component { + static contextTypes = { + history: PropTypes.object, + }; + + render() { + return ; + } +} export default AppRouter; diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index b553cff45..418bb3760 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -12,6 +12,7 @@ class TalkProvider extends React.Component { graphqlRegistry: this.props.graphqlRegistry, notification: this.props.notification, storage: this.props.storage, + history: this.props.history, }; } @@ -33,6 +34,7 @@ TalkProvider.childContextTypes = { graphqlRegistry: PropTypes.object, notification: PropTypes.object, storage: PropTypes.object, + history: PropTypes.object, }; export default TalkProvider; diff --git a/client/coral-framework/helpers/router.js b/client/coral-framework/helpers/router.js deleted file mode 100644 index e6277e9d8..000000000 --- a/client/coral-framework/helpers/router.js +++ /dev/null @@ -1,7 +0,0 @@ -import {useBasename} from 'history'; -import {browserHistory} from 'react-router'; -import {BASE_PATH} from 'coral-framework/constants/url'; - -export const history = useBasename(() => browserHistory)({ - basename: BASE_PATH -}); diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index a1b8aec12..70fb9fa50 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -13,6 +13,7 @@ import {createNotificationService} from './notification'; import {createGraphQLRegistry} from './graphqlRegistry'; import globalFragments from 'coral-framework/graphql/fragments'; import {createStorage} from 'coral-framework/services/storage'; +import {createHistory} from 'coral-framework/services/history'; /** * getAuthToken returns the active auth token or null @@ -41,6 +42,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); const storage = createStorage(); + const history = createHistory(BASE_PATH); let store = null; const token = () => { @@ -76,6 +78,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi graphqlRegistry, notification, storage, + history, }; // Load framework fragments. diff --git a/client/coral-framework/services/history.js b/client/coral-framework/services/history.js new file mode 100644 index 000000000..f64bb8fb1 --- /dev/null +++ b/client/coral-framework/services/history.js @@ -0,0 +1,12 @@ +import {browserHistory} from 'react-router'; +import {useBasename} from 'history'; + +export function createHistory(basename) { + if (!basename) { + return browserHistory; + } + + return useBasename(() => browserHistory)({ + basename + }); +} From 645aaa09ae6c7bc050eceaa62fd76f0858abdd3c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 24 Aug 2017 18:40:38 +0700 Subject: [PATCH 16/16] Add some docs --- client/coral-admin/src/services/notification.js | 5 +++++ client/coral-framework/services/bootstrap.js | 12 +++++++++++- client/coral-framework/services/client.js | 11 +++++++++-- client/coral-framework/services/events.js | 6 ++++++ client/coral-framework/services/graphqlRegistry.js | 5 +++++ client/coral-framework/services/history.js | 5 +++++ client/coral-framework/services/notification.js | 5 +++++ client/coral-framework/services/plugins.js | 9 +++++++-- client/coral-framework/services/rest.js | 7 +++++++ client/coral-framework/services/storage.js | 4 ++++ client/coral-framework/services/store.js | 6 ++++++ 11 files changed, 70 insertions(+), 5 deletions(-) diff --git a/client/coral-admin/src/services/notification.js b/client/coral-admin/src/services/notification.js index 7ad85692b..aca5bdc80 100644 --- a/client/coral-admin/src/services/notification.js +++ b/client/coral-admin/src/services/notification.js @@ -1,3 +1,8 @@ +/** + * createNotificationService returns a notification services based on toast. + * @param {Object} toast + * @return {Object} notification service + */ export function createNotificationService(toast) { return { success(msg) { diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index 70fb9fa50..ac94d8b29 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -38,7 +38,17 @@ const getAuthToken = (store, storage) => { return null; }; -export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification}) { +/** + * createContext setups and returns Talk dependencies that should be + * passed to `TalkProvider`. + * @param {Object} [config] configuration + * @param {Object} [config.reducers] extra reducers to add to redux + * @param {Array} [config.pluginsConfig] plugin configuration as returned by importing pluginsConfig + * @param {Object} [config.graphqlExtensions] additional extension to the graphql framework + * @param {Object} [config.notification] replace default notification service + * @return {Object} context + */ +export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification} = {}) { const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); const storage = createStorage(); diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index ce83d501f..5907488e3 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -15,8 +15,16 @@ function resolveToken(token) { return typeof token === 'function' ? token() : token; } +/** + * createClient setups and returns an Apollo GraphQL Client + * @param {Object} [options] configuration + * @param {string|function} [options.token] auth token + * @param {string} [options.uri] uri of the graphql server + * @param {string} [options.liveUri] uri of the graphql subscription server + * @return {Object} apollo client + */ export function createClient(options = {}) { - const {token, uri, liveUri, ...apolloOptions} = options; + const {token, uri, liveUri} = options; const wsClient = new SubscriptionClient(liveUri, { reconnect: true, lazy: true, @@ -53,7 +61,6 @@ export function createClient(options = {}) { ); const client = new ApolloClient({ - ...apolloOptions, connectToDevTools: true, addTypename: true, fragmentMatcher: new IntrospectionFragmentMatcher({introspectionQueryResultData}), diff --git a/client/coral-framework/services/events.js b/client/coral-framework/services/events.js index f6a62763d..e1b13afd9 100644 --- a/client/coral-framework/services/events.js +++ b/client/coral-framework/services/events.js @@ -1,3 +1,9 @@ +/** + * createReduxEmitter returns a redux middleware proxying redux actions to + * the event emitter + * @param {Object} eventEmitter + * @return {function} redux middleware + */ export function createReduxEmitter(eventEmitter) { return () => (next) => (action) => { diff --git a/client/coral-framework/services/graphqlRegistry.js b/client/coral-framework/services/graphqlRegistry.js index 5ca549183..938bf7f78 100644 --- a/client/coral-framework/services/graphqlRegistry.js +++ b/client/coral-framework/services/graphqlRegistry.js @@ -268,6 +268,11 @@ class GraphQLRegistry { } } +/** + * createGraphQLRegistry + * @param {Function} getSlotFragments A callback with signature `(slot, part) => [documents]` to retrieve slot fragments. + * @return {Object} graphql registry + */ export function createGraphQLRegistry(getSlotFragments) { return new GraphQLRegistry(getSlotFragments); } diff --git a/client/coral-framework/services/history.js b/client/coral-framework/services/history.js index f64bb8fb1..c80d4b920 100644 --- a/client/coral-framework/services/history.js +++ b/client/coral-framework/services/history.js @@ -1,6 +1,11 @@ import {browserHistory} from 'react-router'; import {useBasename} from 'history'; +/** + * createHistory returns the history service for react router + * @param {string} basename base path of the url + * @return {Object} histor service + */ export function createHistory(basename) { if (!basename) { return browserHistory; diff --git a/client/coral-framework/services/notification.js b/client/coral-framework/services/notification.js index fa0e61b3f..5730200ad 100644 --- a/client/coral-framework/services/notification.js +++ b/client/coral-framework/services/notification.js @@ -1,3 +1,8 @@ +/** + * createNotificationService returns a notification services based on pym. + * @param {Object} pym + * @return {Object} notification service + */ export function createNotificationService(pym) { return { success(msg) { diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js index 5474e919f..1aae0c662 100644 --- a/client/coral-framework/services/plugins.js +++ b/client/coral-framework/services/plugins.js @@ -174,6 +174,11 @@ class PluginsService { } } -export function createPluginsService(plugins) { - return new PluginsService(plugins); +/** + * createPluginsService returns a plugins service. + * @param {Array} plugins config as returned from importing `pluginsConfig` + * @return {Object} plugins service + */ +export function createPluginsService(pluginsConfig) { + return new PluginsService(pluginsConfig); } diff --git a/client/coral-framework/services/rest.js b/client/coral-framework/services/rest.js index f879659ca..e5f9e5e09 100644 --- a/client/coral-framework/services/rest.js +++ b/client/coral-framework/services/rest.js @@ -43,6 +43,13 @@ const handleResp = (res) => { } }; +/** + * createRestClient setups and returns a Rest Client + * @param {Object} options configuration + * @param {string} options.uri uri of the rest server + * @param {string|function} [options.token] auth token + * @return {Object} rest client + */ export function createRestClient(options) { const {token, uri} = options; const client = (path, options) => { diff --git a/client/coral-framework/services/storage.js b/client/coral-framework/services/storage.js index 1454d2ac5..9f06595fa 100644 --- a/client/coral-framework/services/storage.js +++ b/client/coral-framework/services/storage.js @@ -34,6 +34,10 @@ function getStorage(type) { return storage; } +/** + * createStorage returns a localStorage wrapper if available + * @return {Object} localStorage wrapper + */ export function createStorage() { return getStorage('localStorage'); } diff --git a/client/coral-framework/services/store.js b/client/coral-framework/services/store.js index e5fc52503..3ccfe9e2f 100644 --- a/client/coral-framework/services/store.js +++ b/client/coral-framework/services/store.js @@ -1,5 +1,11 @@ import {createStore as reduxCreateStore, combineReducers, applyMiddleware, compose} from 'redux'; +/** + * createStore creates a Redux Store + * @param {Object} reducers addtional reducers + * @param {Array} [middlewares] additional middlewares + * @return {Object} redux store + */ export function createStore(reducers, middlewares = []) { const enhancers = [ applyMiddleware(