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);
-}