Skip to content
Snippets Groups Projects
Unverified Commit d13df2d9 authored by Jack Reed's avatar Jack Reed Committed by GitHub
Browse files

Revert "Connect plugins on start. Closes #2411"

parent 83ca5480
No related branches found
No related tags found
No related merge requests found
import {
filterValidPlugins,
connectPluginsToStore,
addPluginReducersToStore,
createTargetToPluginMapping,
} from '../../../src/extend/pluginPreprocessing';
describe('filterValidPlugins', () => {
it('returns empty array if plugin array is empty', () => {
expect(filterValidPlugins([])).toEqual([]);
});
it('returns only valid plugins', () => {
const plugins = [
{
component: props => null,
mode: 'add',
name: 'valid plugin 1',
target: 'Window',
},
{
component: props => null,
mode: 'wrap',
name: 'valid plugin 2',
target: 'Window',
},
{
name: 'invalid Plugin 1',
},
{
name: 'invalid Plugin 2',
},
];
const result = filterValidPlugins(plugins);
expect(result.length).toBe(2);
expect(result[0].name).toBe('valid plugin 1');
expect(result[1].name).toBe('valid plugin 2');
});
});
describe('createTargetToPluginMapping', () => {
it('returns empty object if plugin array is empty', () => {
expect(createTargetToPluginMapping([])).toEqual({});
});
it('should create a mapping from targets to plugins and modes', () => {
/** */
const component = props => null;
const plugins = [
{ component, mode: 'wrap', target: 'Window' },
{ component, mode: 'wrap', target: 'Window' },
{ component, mode: 'add', target: 'Window' },
{ component, mode: 'add', target: 'Window' },
{ component, mode: 'wrap', target: 'TopBar' },
{ component, mode: 'wrap', target: 'TopBar' },
{ component, mode: 'add', target: 'TopBar' },
{ component, mode: 'add', target: 'TopBar' },
];
expect(createTargetToPluginMapping(plugins)).toEqual({
TopBar: {
add: [
{ component, mode: 'add', target: 'TopBar' },
{ component, mode: 'add', target: 'TopBar' },
],
wrap: [
{ component, mode: 'wrap', target: 'TopBar' },
{ component, mode: 'wrap', target: 'TopBar' },
],
},
Window: {
add: [
{ component, mode: 'add', target: 'Window' },
{ component, mode: 'add', target: 'Window' },
],
wrap: [
{ component, mode: 'wrap', target: 'Window' },
{ component, mode: 'wrap', target: 'Window' },
],
},
});
});
});
describe('connectPluginsToStore', () => {
it('returns empty array if plugin array is empty', () => {
expect(filterValidPlugins([])).toEqual([]);
});
it('returns plugins with components connected to store', () => {
/** */
const ComponentA = props => null;
/** */
const ComponentB = props => null;
const plugins = [
{ component: ComponentA, mode: 'wrap', target: 'Window' },
{ component: ComponentB, mode: 'add', target: 'TopBar' },
];
const result = connectPluginsToStore(plugins);
expect(result.length).toBe(2);
expect(result[0].component.displayName).toBe('Connect(ComponentA)');
expect(result[1].component.displayName).toBe('Connect(ComponentB)');
});
});
describe('addPluginReducersToStore', () => {
const store = { replaceReducer: jest.fn() };
const createRootReducer = jest.fn(pluginReducers => pluginReducers);
/** */ const fooReducer = x => x;
/** */ const barReducer = x => x;
/** */ const bazReducer = x => x;
const plugins = [
{
component: props => null,
mode: 'add',
reducers: {
bar: barReducer,
foo: fooReducer,
},
target: 'Window',
},
{
component: props => null,
mode: 'add',
reducers: {
baz: bazReducer,
},
target: 'Window',
},
];
addPluginReducersToStore(store, createRootReducer, plugins);
expect(store.replaceReducer.mock.calls.length).toBe(1);
expect(store.replaceReducer.mock.calls[0][0]).toEqual({
bar: barReducer,
baz: bazReducer,
foo: fooReducer,
});
});
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('returns undefined if no plugin for target exist', () => {
pluginStore.storePlugins();
expect(pluginStore.getPlugins('target')).not.toBeDefined();
});
it('returns mode->plugins mapping for target', () => {
/** */
const component = x => x;
const plugins = [
{ component, mode: 'wrap', target: 'Window' },
{ component, mode: 'wrap', target: 'Window' },
{ component, mode: 'add', target: 'Window' },
{ component, mode: 'add', target: 'Window' },
{ component, mode: 'wrap', target: 'TopBar' },
{ component, mode: 'wrap', target: 'TopBar' },
{ component, mode: 'add', target: 'TopBar' },
{ component, mode: 'add', target: 'TopBar' },
];
pluginStore.storePlugins(plugins);
expect(pluginStore.getPlugins('Window')).toEqual({
add: [
{ component, mode: 'add', target: 'Window' },
{ component, mode: 'add', target: 'Window' },
],
wrap: [
{ component, mode: 'wrap', target: 'Window' },
{ component, mode: 'wrap', target: 'Window' },
],
});
expect(pluginStore.getPlugins('TopBar')).toEqual({
add: [
{ component, mode: 'add', target: 'TopBar' },
{ component, mode: 'add', target: 'TopBar' },
],
wrap: [
{ component, mode: 'wrap', target: 'TopBar' },
{ component, mode: 'wrap', target: 'TopBar' },
],
});
});
// see also pluginValidation.test.js
it('filter out invalid plugins', () => {
/** */
const component = x => x;
const plugins = [
{ component, mode: 'add', target: 'Window' },
{ component, mode: 'LURK', target: 'Window' },
];
pluginStore.storePlugins(plugins);
expect(pluginStore.getPlugins('Window')).toEqual({
add: [
{ component, mode: 'add', target: 'Window' },
],
});
});
});
import React from 'react';
import { mount } from 'enzyme';
import { withPlugins, PluginContext } from '../../../src/extend';
import { shallow } from 'enzyme';
import { withPlugins } from '../../../src/extend';
import { pluginStore } from '../../../src/extend/pluginStore';
jest.mock('../../../src/extend/pluginStore');
/** Mock target component */
const Target = props => <div>Hello</div>;
/** create wrapper */
function createPluginHoc(pluginMap) {
function createPluginHoc(plugins) {
pluginStore.getPlugins = () => plugins;
const props = { bar: 2, foo: 1 };
const PluginHoc = withPlugins('Target', Target);
return mount(
<PluginContext.Provider value={pluginMap}>
<PluginHoc {...props} />
</PluginContext.Provider>,
);
return shallow(<PluginHoc {...props} />);
}
describe('withPlugins', () => {
......@@ -34,7 +34,7 @@ describe('withPlugins', () => {
describe('PluginHoc: if no plugin exists for the target', () => {
it('renders the target component', () => {
const hoc = createPluginHoc({});
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);
......@@ -45,16 +45,14 @@ describe('PluginHoc: if wrap plugins exist for target', () => {
it('renders the first wrap plugin and passes the target component and the target props to it', () => {
/** */ const WrapPluginComponentA = props => <div>look i am a plugin</div>;
/** */ const WrapPluginComponentB = props => <div>look i am a plugin</div>;
const pluginMap = {
Target: {
const plugins = {
wrap: [
{ component: WrapPluginComponentA, mode: 'wrap', target: 'Target' },
{ component: WrapPluginComponentB, mode: 'wrap', target: 'Target' },
],
},
};
const hoc = createPluginHoc(pluginMap);
const selector = 'WrapPluginComponentA';
const hoc = createPluginHoc(plugins);
const selector = 'Connect(WrapPluginComponentA)';
expect(hoc.find(selector).length).toBe(1);
expect(hoc.find(selector).props().TargetComponent).toBe(Target);
expect(hoc.find(selector).props().targetProps).toEqual({ bar: 2, foo: 1 });
......@@ -66,20 +64,20 @@ describe('PluginHoc: if add plugins exist but no wrap plugin', () => {
/** */ const AddPluginComponentA = props => <div>look i am a plugin</div>;
/** */ const AddPluginComponentB = props => <div>look i am a plugin</div>;
const plugins = {
Target: {
add: [
{ component: AddPluginComponentA, mode: 'add', target: 'Target' },
{ component: AddPluginComponentB, mode: 'add', target: 'Target' },
],
},
};
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]).toBe(AddPluginComponentA);
expect(hoc.find(selector).props().PluginComponents[1]).toBe(AddPluginComponentB);
expect(hoc.find(selector).props().PluginComponents[0].displayName)
.toBe('Connect(AddPluginComponentA)');
expect(hoc.find(selector).props().PluginComponents[1].displayName)
.toBe('Connect(AddPluginComponentB)');
});
});
......@@ -90,7 +88,6 @@ describe('PluginHoc: if wrap plugins AND add plugins exist for target', () => {
/** */ const AddPluginComponentA = props => <div>look i am a plugin</div>;
/** */ const AddPluginComponentB = props => <div>look i am a plugin</div>;
const plugins = {
Target: {
add: [
{ component: AddPluginComponentA, mode: 'add', target: 'Target' },
{ component: AddPluginComponentB, mode: 'add', target: 'Target' },
......@@ -99,10 +96,9 @@ describe('PluginHoc: if wrap plugins AND add plugins exist for target', () => {
{ component: WrapPluginComponentA, mode: 'wrap', target: 'Target' },
{ component: WrapPluginComponentB, mode: 'wrap', target: 'Target' },
],
},
};
const hoc = createPluginHoc(plugins);
expect(hoc.find(WrapPluginComponentA).length).toBe(1);
expect(hoc.find('Connect(WrapPluginComponentA)').length).toBe(1);
expect(hoc.find(Target).length).toBe(0);
});
});
import ReactDOM from 'react-dom';
import { pluginStore } from '../../../src/extend/pluginStore';
import MiradorViewer from '../../../src/lib/MiradorViewer';
jest.unmock('react-i18next');
jest.mock('../../../src/extend/pluginStore');
jest.mock('react-dom');
describe('MiradorViewer', () => {
......@@ -21,6 +23,31 @@ describe('MiradorViewer', () => {
expect(ReactDOM.render).toHaveBeenCalled();
});
});
describe('process plugins', () => {
it('should store plugins and set reducers to state', () => {
/** */ const fooReducer = (state = 0) => state;
/** */ const barReducer = (state = 0) => state;
/** */ const bazReducer = (state = 0) => state;
/** */ const plugins = [
{
reducers: {
bar: barReducer,
foo: fooReducer,
},
},
{
reducers: {
baz: bazReducer,
},
},
];
instance = new MiradorViewer({}, plugins);
expect(pluginStore.storePlugins).toBeCalledWith(plugins);
expect(instance.store.getState().foo).toBeDefined();
expect(instance.store.getState().bar).toBeDefined();
expect(instance.store.getState().baz).toBeDefined();
});
});
describe('processConfig', () => {
it('transforms config values to actions to dispatch to store', () => {
instance = new MiradorViewer({
......
export * from './pluginContext';
export * from './pluginProvider';
export * from './pluginPreprocessing';
export * from './pluginValidation';
export * from './pluginStore';
export * from './withPlugins';
export * from './pluginValidation';
import React from 'react';
export const PluginContext = React.createContext();
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { ReactReduxContext } from 'react-redux';
import {
PluginContext,
filterValidPlugins,
addPluginReducersToStore,
connectPluginsToStore,
createTargetToPluginMapping,
} from '.';
/** */
export function PluginProvider(props) {
const { store } = useContext(ReactReduxContext);
const { plugins, createRootReducer, children } = props;
const [pluginMap, setPluginMap] = useState({});
useEffect(() => {
const validPlugins = filterValidPlugins(plugins);
const connectedPlugins = connectPluginsToStore(validPlugins);
createRootReducer && addPluginReducersToStore(store, createRootReducer, validPlugins);
setPluginMap(createTargetToPluginMapping(connectedPlugins));
}, []);
return (
<PluginContext.Provider value={pluginMap}>
{ children }
</PluginContext.Provider>
);
}
PluginProvider.propTypes = {
children: PropTypes.node,
createRootReducer: PropTypes.func,
plugins: PropTypes.array, // eslint-disable-line react/forbid-prop-types
};
PluginProvider.defaultProps = {
children: null,
createRootReducer: null,
plugins: [],
};
import update from 'lodash/update';
import { connect } from 'react-redux';
import { validatePlugin } from '.';
export const pluginStore = {
/**
* Returns a mapping from targets to plugins and modes
* Get plugins for target
*
* @param {String} targetName
* @return {Object | undefined } - looks like:
*
* {
* wrap: [plugin1, ...],
* add: [plugin2, ...],
* }
*/
getPlugins(target) {
return this.pluginMap[target];
},
/**
* Store Plugins
*
* @param {Array} plugins
*/
storePlugins(plugins = []) {
const { validPlugins, invalidPlugins } = filterPlugins(plugins);
logInvalidPlugins(invalidPlugins);
this.pluginMap = mapPlugins(validPlugins);
},
};
/**
* Returns a mapping from plugins to targets and modes
*
* @param {Array} plugins
* @return {Object} - looks like:
*
*
* {
* 'WorkspacePanel': {
* wrap: [plugin3, ...],
* add: [plugin4, ...],
* },
* ...
* 'Window': {
* wrap: [plugin3, ...],
* add: [plugin4, ...],
* }
* }
*/
export function createTargetToPluginMapping(plugins) {
function mapPlugins(plugins) {
return plugins.reduce((map, plugin) => (
update(map, [plugin.target, plugin.mode], x => [...x || [], plugin])
), {});
}
/** */
export function filterValidPlugins(plugins) {
const { validPlugins, invalidPlugins } = splitPluginsByValidation(plugins);
logInvalidPlugins(invalidPlugins);
return validPlugins;
}
/** */
export function connectPluginsToStore(plugins) {
return plugins.map(plugin => (
{ ...plugin, component: connectPluginComponent(plugin) }
));
}
/** */
export function addPluginReducersToStore(store, createRootReducer, plugins) {
const pluginReducers = getReducersFromPlugins(plugins);
store.replaceReducer(createRootReducer(pluginReducers));
}
/** */
function splitPluginsByValidation(plugins) {
const splittedPlugins = { invalidPlugins: [], validPlugins: [] };
function filterPlugins(plugins) {
const filteredPlugins = { invalidPlugins: [], validPlugins: [] };
plugins.forEach(plugin => (
validatePlugin(plugin)
? splittedPlugins.validPlugins.push(plugin)
: splittedPlugins.invalidPlugins.push(plugin)
? filteredPlugins.validPlugins.push(plugin)
: filteredPlugins.invalidPlugins.push(plugin)
));
return splittedPlugins;
return filteredPlugins;
}
/** */
......@@ -59,13 +69,3 @@ function logInvalidPlugins(plugins) {
console.log(`Mirador: Plugin ${plugin.name} is not valid and was rejected.`)
));
}
/** Connect plugin component to state */
function connectPluginComponent(plugin) {
return connect(plugin.mapStateToProps, plugin.mapDispatchToProps)(plugin.component);
}
/** */
function getReducersFromPlugins(plugins) {
return plugins && plugins.reduce((acc, plugin) => ({ ...acc, ...plugin.reducers }), {});
}
import React, { useContext } from 'react';
import React, { Component } from 'react';
import curry from 'lodash/curry';
import isEmpty from 'lodash/isEmpty';
import { PluginContext } from '.';
import { connect } from 'react-redux';
import { pluginStore } from '.';
/** withPlugins should be the innermost HOC */
function _withPlugins(targetName, TargetComponent) { // eslint-disable-line no-underscore-dangle
/** */
function PluginHoc(props) {
const pluginMap = useContext(PluginContext);
const plugins = pluginMap[targetName];
/** plugin wrapper hoc */
class PluginHoc extends Component {
/** render */
render() { // eslint-disable-line consistent-return
const plugins = pluginStore.getPlugins(targetName);
if (isEmpty(plugins)) {
return <TargetComponent {...props} />;
return <TargetComponent {...this.props} />;
}
if (!isEmpty(plugins.wrap)) {
const PluginComponent = plugins.wrap[0].component;
return <PluginComponent targetProps={props} TargetComponent={TargetComponent} />;
const WrapPluginComponent = connectPluginComponent(plugins.wrap[0]);
return <WrapPluginComponent targetProps={this.props} TargetComponent={TargetComponent} />;
}
if (!isEmpty(plugins.add)) {
const PluginComponents = plugins.add.map(plugin => plugin.component);
return <TargetComponent {...props} PluginComponents={PluginComponents} />;
const AddPluginComponents = plugins.add.map(plugin => connectPluginComponent(plugin));
return <TargetComponent {...this.props} PluginComponents={AddPluginComponents} />;
}
}
}
......@@ -30,5 +32,10 @@ function _withPlugins(targetName, TargetComponent) { // eslint-disable-line no-u
return PluginHoc;
}
/** Connect plugin component to state */
function connectPluginComponent(plugin) {
return connect(plugin.mapStateToProps, plugin.mapDispatchToProps)(plugin.component);
}
/** withPlugins('MyComponent')(MyComponent) */
export const withPlugins = curry(_withPlugins);
......@@ -2,10 +2,9 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import deepmerge from 'deepmerge';
import { PluginProvider } from '../extend';
import App from '../containers/App';
import { pluginStore } from '../extend';
import createStore from '../state/createStore';
import createRootReducer from '../state/reducers/rootReducer';
import * as actions from '../state/actions';
import settings from '../config/settings';
......@@ -16,7 +15,9 @@ class MiradorViewer {
/**
*/
constructor(config, plugins) {
this.store = createStore();
pluginStore.storePlugins(plugins);
const pluginReducers = getReducersFromPlugins(plugins);
this.store = createStore(pluginReducers);
this.config = config;
this.processConfig();
const viewer = {
......@@ -26,9 +27,7 @@ class MiradorViewer {
ReactDOM.render(
<Provider store={this.store}>
<PluginProvider plugins={plugins} createRootReducer={createRootReducer}>
<App />
</PluginProvider>
</Provider>,
document.getElementById(config.id),
);
......@@ -74,4 +73,9 @@ class MiradorViewer {
}
}
/** Return reducers from plugins */
function getReducersFromPlugins(plugins) {
return plugins && plugins.reduce((acc, plugin) => ({ ...acc, ...plugin.reducers }), {});
}
export default MiradorViewer;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment