mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 01:21:14 +08:00
Remove global dependency on registry and plugins
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <WrappedComponent
|
||||
{...this.props}
|
||||
queueConfig={{...queueConfig, ...this.pluginsConfig}}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
return WithQueueConfig;
|
||||
});
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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) => (
|
||||
<Tab tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
|
||||
<PluginComponent
|
||||
{...getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData)}
|
||||
active={this.props.activeTab === PluginComponent.talkPluginName}
|
||||
/>
|
||||
</Tab>
|
||||
));
|
||||
const {plugins} = this.context;
|
||||
return this.getSlotComponents(props.tabSlot).map((PluginComponent) => {
|
||||
const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData);
|
||||
return (
|
||||
<Tab tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
|
||||
<PluginComponent
|
||||
{...pluginProps}
|
||||
active={this.props.activeTab === PluginComponent.talkPluginName}
|
||||
/>
|
||||
</Tab>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getPluginTabPaneElements(props = this.props) {
|
||||
return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => (
|
||||
<TabPane tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
|
||||
<PluginComponent
|
||||
{...getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData)}
|
||||
/>
|
||||
</TabPane>
|
||||
));
|
||||
const {plugins} = this.context;
|
||||
return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => {
|
||||
const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData);
|
||||
return (
|
||||
<TabPane tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
|
||||
<PluginComponent
|
||||
{...pluginProps}
|
||||
/>
|
||||
</TabPane>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 = <DefaultComponent {...getSlotComponentProps(DefaultComponent, reduxState, this.getSlotProps(this.props), queryData)} />;
|
||||
const props = plugins.getSlotComponentProps(DefaultComponent, reduxState, this.getSlotProps(this.props), queryData);
|
||||
children = <DefaultComponent {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+18
-4
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user