Skip to content
Snippets Groups Projects
Commit fd0d0c8d authored by Mathias Maaß's avatar Mathias Maaß
Browse files

Mechanism for negotiating the hierarchy of plugins. Closes #2188 #2185

parent 3ef817df
No related branches found
No related tags found
No related merge requests found
import { pluginStore } from '../../../src/extend';
describe('storePlugins()', () => {
it('should run without throw error when Array is passed', () => {
expect(() => pluginStore.storePlugins([])).not.toThrow();
});
it('should run without throw error when nothing is passed', () => {
expect(() => pluginStore.storePlugins()).not.toThrow();
});
});
describe('getPlugins', () => {
it('with no plugins should return undefined', () => {
pluginStore.storePlugins();
expect(pluginStore.getPlugins('target')).not.toBeDefined();
});
it('returns { mode -> plugin } mapping for target', () => {
const plugins = [
{ target: 'Window', mode: 'delete' },
{ target: 'Window', mode: 'delete' },
{ target: 'Window', mode: 'replace' },
{ target: 'Window', mode: 'replace' },
{ target: 'Window', mode: 'wrap' },
{ target: 'Window', mode: 'wrap' },
{ target: 'Window', mode: 'add' },
{ target: 'Window', mode: 'add' },
{ target: 'TopBar', mode: 'delete' },
{ target: 'TopBar', mode: 'delete' },
{ target: 'TopBar', mode: 'replace' },
{ target: 'TopBar', mode: 'replace' },
{ target: 'TopBar', mode: 'wrap' },
{ target: 'TopBar', mode: 'wrap' },
{ target: 'TopBar', mode: 'add' },
{ target: 'TopBar', mode: 'add' },
];
pluginStore.storePlugins(plugins);
expect(pluginStore.getPlugins('Window')).toEqual({
delete: [
{ target: 'Window', mode: 'delete' },
{ target: 'Window', mode: 'delete' },
],
replace: [
{ target: 'Window', mode: 'replace' },
{ target: 'Window', mode: 'replace' },
],
wrap: [
{ target: 'Window', mode: 'wrap' },
{ target: 'Window', mode: 'wrap' },
],
add: [
{ target: 'Window', mode: 'add' },
{ target: 'Window', mode: 'add' },
],
});
expect(pluginStore.getPlugins('TopBar')).toEqual({
delete: [
{ target: 'TopBar', mode: 'delete' },
{ target: 'TopBar', mode: 'delete' },
],
replace: [
{ target: 'TopBar', mode: 'replace' },
{ target: 'TopBar', mode: 'replace' },
],
wrap: [
{ target: 'TopBar', mode: 'wrap' },
{ target: 'TopBar', mode: 'wrap' },
],
add: [
{ target: 'TopBar', mode: 'add' },
{ target: 'TopBar', mode: 'add' },
],
});
});
});
import React from 'react';
import { shallow } from 'enzyme';
import { withPlugins } from '../../../src/extend';
import { pluginStore } from '../../../src/extend/pluginStore';
jest.mock('../../../src/extend/pluginStore');
/** */
const Target = props => <div>Hello</div>;
/** create wrapper */
function createPluginHoc(plugins) {
pluginStore.getPlugins = () => plugins;
const props = { foo: 1, bar: 2 };
const PluginHoc = withPlugins('Target', Target);
return shallow(<PluginHoc {...props} />);
}
describe('withPlugins', () => {
it('should return a function (normal function call)', () => {
expect(withPlugins('Target', Target)).toBeInstanceOf(Function);
});
it('should return a function (curry function call)', () => {
expect(withPlugins('Target')(Target)).toBeInstanceOf(Function);
});
it('displayName prop of returned function is based on target name argument', () => {
expect(withPlugins('Bubu', Target).displayName)
.toBe('WithPlugins(Bubu)');
});
});
describe('PluginHoc: if no plugin exists for the target', () => {
it('renders the target component', () => {
const hoc = createPluginHoc([]);
expect(hoc.find(Target).length).toBe(1);
expect(hoc.find(Target).props().foo).toBe(1);
expect(hoc.find(Target).props().bar).toBe(2);
});
});
describe('PluginHoc: if a delete-plugin exists for the target', () => {
it('renders nothing', () => {
const plugins = {
delete: [
{ target: 'Target', mode: 'delete' },
],
replace: [
{ target: 'Target', mode: 'replace' },
],
wrap: [
{ target: 'Target', mode: 'wrap' },
],
add: [
{ target: 'Target', mode: 'add' },
],
};
const hoc = createPluginHoc(plugins);
expect(hoc.find('*').length).toBe(0);
});
});
describe('PluginHoc: if replace-plugins exists but no delete-plugin', () => {
it('renders the first replace-plugin component', () => {
/** */ const ReplacePluginComponentA = props => <div>look i am a plugin</div>;
/** */ const ReplacePluginComponentB = props => <div>look i am a plugin</div>;
/** */ const WrapPluginComponent = props => <div>look i am a plugin</div>;
/** */ const AddPluginComponent = props => <div>look i am a plugin</div>;
const plugins = {
replace: [
{ target: 'Target', mode: 'replace', component: ReplacePluginComponentA },
{ target: 'Target', mode: 'replace', component: ReplacePluginComponentB },
],
wrap: [
{ target: 'Target', mode: 'wrap', component: WrapPluginComponent },
],
add: [
{ target: 'Target', mode: 'add', component: AddPluginComponent },
],
};
const hoc = createPluginHoc(plugins);
const selector = 'Connect(ReplacePluginComponentA)';
expect(hoc.find(selector).length).toBe(1);
expect(hoc.find(selector).props().foo).toBe(1);
expect(hoc.find(selector).props().bar).toBe(2);
});
});
describe('PluginHoc: if wrap-plugins exists but no delete-plugin, no replace-plugin and no add-plugins', () => {
it('renders the first wrap-plugin and passes the target component as prop', () => {
/** */ const WrapPluginComponentA = props => <div>look i am a plugin</div>;
/** */ const WrapPluginComponentB = props => <div>look i am a plugin</div>;
const plugins = {
wrap: [
{ target: 'Target', mode: 'wrap', component: WrapPluginComponentA },
{ target: 'Target', mode: 'wrap', component: WrapPluginComponentB },
],
};
const hoc = createPluginHoc(plugins);
const selector = 'Connect(WrapPluginComponentA)';
expect(hoc.find(selector).length).toBe(1);
expect(hoc.find(selector).props().foo).toBe(1);
expect(hoc.find(selector).props().bar).toBe(2);
expect(hoc.find(selector).props().TargetComponent).toBe(Target);
});
});
describe('PluginHoc: if add-plugins exist but no delete-plugin, no replace-plugin and no wrap-plugin', () => {
it('renders the target component and passes all add-plugin components as a prop', () => {
/** */ const AddPluginComponentA = props => <div>look i am a plugin</div>;
/** */ const AddPluginComponentB = props => <div>look i am a plugin</div>;
const plugins = {
add: [
{ target: 'Target', mode: 'add', component: AddPluginComponentA },
{ target: 'Target', mode: 'add', component: AddPluginComponentB },
],
};
const hoc = createPluginHoc(plugins);
const selector = Target;
expect(hoc.find(selector).length).toBe(1);
expect(hoc.find(selector).props().foo).toBe(1);
expect(hoc.find(selector).props().bar).toBe(2);
expect(hoc.find(selector).props().PluginComponents[0].displayName)
.toBe('Connect(AddPluginComponentA)');
expect(hoc.find(selector).props().PluginComponents[1].displayName)
.toBe('Connect(AddPluginComponentB)');
});
});
describe('PluginHoc: if add-plugins AND wrap-plugins exist but no delete-plugin and no replace-plugin', () => {
it('passes all add-plugin components to target component and renders the first wrap-plugin with the target component as prop', () => {
/** */ const WrapPluginComponentA = props => <div>look i am a plugin</div>;
/** */ const WrapPluginComponentB = props => <div>look i am a plugin</div>;
/** */ const AddPluginComponentA = props => <div>look i am a plugin</div>;
/** */ const AddPluginComponentB = props => <div>look i am a plugin</div>;
const plugins = {
add: [
{ target: 'Target', mode: 'add', component: AddPluginComponentA },
{ target: 'Target', mode: 'add', component: AddPluginComponentB },
],
wrap: [
{ target: 'Target', mode: 'wrap', component: WrapPluginComponentA },
{ target: 'Target', mode: 'wrap', component: WrapPluginComponentB },
],
};
const hoc = createPluginHoc(plugins);
const selector = 'Connect(WrapPluginComponentA)';
expect(hoc.find(selector).length).toBe(1);
expect(hoc.find(selector).props().foo).toBe(1);
expect(hoc.find(selector).props().bar).toBe(2);
const { TargetComponent } = hoc.find(selector).props();
const target = shallow(<TargetComponent bubu={10} kiki={20} />);
expect(target.props().bubu).toBe(10);
expect(target.props().kiki).toBe(20);
expect(target.props().PluginComponents[0].displayName)
.toBe('Connect(AddPluginComponentA)');
expect(target.props().PluginComponents[1].displayName)
.toBe('Connect(AddPluginComponentB)');
});
});
......@@ -5,8 +5,13 @@ import WorkspaceMenuButton from '../containers/WorkspaceMenuButton';
/** Renders plugins */
const PluginHook = (props) => {
const { PluginComponent } = props; // eslint-disable-line react/prop-types
return PluginComponent ? <PluginComponent {...props} /> : null;
const { PluginComponents } = props; // eslint-disable-line react/prop-types
if (PluginComponents) {
return PluginComponents.map((PluginComponent, index) => (
<PluginComponent {...props} key={index} /> // eslint-disable-line react/no-array-index-key
));
}
return null;
};
/**
......
import update from 'lodash/update';
export const pluginStore = {
/** */
getPlugins() {
return this.plugins || [];
/**
* Get plugins for target
*
* @param {String} targetName
* @return {Object | undefined } - looks like:
*
* {
* delete: [plugin1, ...],
* replace: [plugin2, ...],
* wrap: [plugin3, ...],
* add: [plugin4, ...],
* }
*/
getPlugins(target) {
return this.pluginMap[target];
},
/** */
/**
* Store Plugins
*
* @param {Array} plugins
*/
storePlugins(plugins) {
this.plugins = plugins || [];
this.pluginMap = mapPlugins(plugins || []);
},
};
/**
* Returns a mapping from plugins to targets and modes
*
* @param {Array} plugins
* @return {Object} - looks like:
*
*
* {
* 'WorkspacePanel': {
* delete: [plugin1, ...],
* replace: [plugin2, ...],
* wrap: [plugin3, ...],
* add: [plugin4, ...],
* },
* 'Window': {
* delete: [plugin1, ...],
* replace: [plugin2, ...],
* wrap: [plugin3, ...],
* add: [plugin4, ...],
* }
* }
*/
function mapPlugins(plugins) {
return plugins.reduce((map, plugin) => (
update(map, [plugin.target, plugin.mode], x => [...x || [], plugin])
), {});
}
import React, { Component } from 'react';
import curry from 'lodash/curry';
import isEmpty from 'lodash/isEmpty';
import { connect } from 'react-redux';
import { pluginStore } from '.';
......@@ -8,25 +9,44 @@ function _withPlugins(targetName, TargetComponent) { // eslint-disable-line no-u
/** plugin wrapper hoc */
class PluginHoc extends Component {
/** render */
render() {
const plugin = pluginStore.getPlugins().find(p => p.target === targetName);
render() { // eslint-disable-line consistent-return
const plugins = pluginStore.getPlugins(targetName);
if (plugin && plugin.mode === 'delete') {
if (isEmpty(plugins)) {
return <TargetComponent {...this.props} />;
}
if (!isEmpty(plugins.delete)) {
return null;
}
if (plugin && plugin.mode === 'replace') {
const PluginComponent = connectPluginComponent(plugin);
return <PluginComponent {...this.props} />;
if (!isEmpty(plugins.replace)) {
const ReplacePluginComponent = connectPluginComponent(plugins.replace[0]);
return <ReplacePluginComponent {...this.props} />;
}
if (plugin && plugin.mode === 'add') {
const PluginComponent = connectPluginComponent(plugin);
return <TargetComponent {...this.props} PluginComponent={PluginComponent} />;
if (!isEmpty(plugins.wrap) && !isEmpty(plugins.add)) {
const AddPluginComponents = plugins.add.map(plugin => connectPluginComponent(plugin));
/** we want to pass an react constructor to WrapPluginComponent
rather then a react element */
const TargetComponentWithAddPlugins = props => (
<TargetComponent {...props} PluginComponents={AddPluginComponents} />
);
const WrapPluginComponent = connectPluginComponent(plugins.wrap[0]);
return (
<WrapPluginComponent {...this.props} TargetComponent={TargetComponentWithAddPlugins} />
);
}
if (plugin && plugin.mode === 'wrap') {
const PluginComponent = connectPluginComponent(plugin);
return <PluginComponent {...this.props} TargetComponent={TargetComponent} />;
if (!isEmpty(plugins.wrap)) {
const WrapPluginComponent = connectPluginComponent(plugins.wrap[0]);
return <WrapPluginComponent {...this.props} TargetComponent={TargetComponent} />;
}
if (!isEmpty(plugins.add)) {
const AddPluginComponents = plugins.add.map(plugin => connectPluginComponent(plugin));
return <TargetComponent {...this.props} PluginComponents={AddPluginComponents} />;
}
return <TargetComponent {...this.props} />;
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment