Files
talk/client/coral-framework/services/plugins.js
T
2018-02-07 22:10:05 +01:00

215 lines
6.0 KiB
JavaScript

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 get from 'lodash/get';
import { getDisplayName } from 'coral-framework/helpers/hoc';
import camelize from '../helpers/camelize';
// 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) {
// Detect access from React DevTools and ignore those.
const error = new Error();
const accessFromDevTools = ['backend.js', 'dehydrate'].every(
keyword => error.stack && error.stack.includes(keyword)
);
// Only care about the components defined in the plugins.
if (component.talkPluginName && !accessFromDevTools) {
const warning = `'${getDisplayName(component)}' of '${
component.talkPluginName
}' accessed '${key}.${name}' but did not define fragments using the withFragment HOC`;
if (memoizedWarnings.indexOf(warning) === -1) {
console.warn(warning);
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;
});
});
});
}
class PluginsService {
constructor(plugins) {
this.plugins = plugins;
addMetaDataToSlotComponents(plugins);
}
isSlotEmpty(slot, reduxState, props = {}, queryData = {}) {
return (
this.getSlotElements(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 = get(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 = {}) {
const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig;
const isDisabled = component => {
if (
pluginConfig &&
pluginConfig[component.talkPluginName] &&
pluginConfig[component.talkPluginName].disable_components
) {
return true;
}
// Check if component is excluded.
if (component.isExcluded) {
let resolvedProps = this.getSlotComponentProps(
component,
reduxState,
props,
queryData
);
if (component.mapStateToProps) {
resolvedProps = {
...resolvedProps,
...component.mapStateToProps(reduxState),
};
}
return component.isExcluded(resolvedProps);
}
return false;
};
return flatten(
this.plugins
.filter(o => o.module.slots && o.module.slots[slot])
.map(o => o.module.slots[slot])
)
.map((component, i) => ({
component,
disabled: isDisabled(component),
key: i,
}))
.filter(o => !o.disabled)
.map(({ component, key }) =>
React.createElement(component, {
key,
...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 }))
);
}
async executeInit(context) {
const results = this.plugins
.map(o => o.module.init)
.filter(fn => fn)
.map(fn => fn(context));
await Promise.all(results);
}
}
/**
* createPluginsService returns a plugins service.
* @param {Array} plugins config as returned from importing `pluginsConfig`
* @return {Object} plugins service
*/
export function createPluginsService(pluginsConfig) {
return new PluginsService(pluginsConfig);
}