diff --git a/__tests__/src/extend/pluginPreprocessing.test.js b/__tests__/src/extend/pluginPreprocessing.test.js deleted file mode 100644 index d695c53c98d613841e2cec87fe46782c456ec079..0000000000000000000000000000000000000000 --- a/__tests__/src/extend/pluginPreprocessing.test.js +++ /dev/null @@ -1,146 +0,0 @@ -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, - }); -}); diff --git a/__tests__/src/extend/pluginStore.test.js b/__tests__/src/extend/pluginStore.test.js new file mode 100644 index 0000000000000000000000000000000000000000..fa34f945c0f6665062bb261cafba5c809fda22ce --- /dev/null +++ b/__tests__/src/extend/pluginStore.test.js @@ -0,0 +1,77 @@ +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' }, + ], + }); + }); +}); diff --git a/__tests__/src/extend/withPlugins.test.js b/__tests__/src/extend/withPlugins.test.js index 08b33c5ebf75724efb01d9050caad16bdd62f281..1e3372b2501359e748c030215d3b331a239ed508 100644 --- a/__tests__/src/extend/withPlugins.test.js +++ b/__tests__/src/extend/withPlugins.test.js @@ -1,20 +1,20 @@ 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: { - wrap: [ - { component: WrapPluginComponentA, mode: 'wrap', target: 'Target' }, - { component: WrapPluginComponentB, mode: 'wrap', target: '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' }, - ], - }, + 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,19 +88,17 @@ 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' }, - ], - wrap: [ - { component: WrapPluginComponentA, mode: 'wrap', target: 'Target' }, - { component: WrapPluginComponentB, mode: 'wrap', target: 'Target' }, - ], - }, + add: [ + { component: AddPluginComponentA, mode: 'add', target: 'Target' }, + { component: AddPluginComponentB, mode: 'add', target: 'Target' }, + ], + wrap: [ + { 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); }); }); diff --git a/__tests__/src/lib/MiradorViewer.test.js b/__tests__/src/lib/MiradorViewer.test.js index 0df7c0e0a77228952615ce581ee8b4244eb9b779..b8592e40c24123209f449fcfe26bd26da2e82025 100644 --- a/__tests__/src/lib/MiradorViewer.test.js +++ b/__tests__/src/lib/MiradorViewer.test.js @@ -1,7 +1,9 @@ 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({ diff --git a/src/extend/index.js b/src/extend/index.js index 18c4065c501310884a0ea15c5f24f327dc3ba6e3..d534dcaffb6e8c4ed65d661bc2cb94c63836b39b 100644 --- a/src/extend/index.js +++ b/src/extend/index.js @@ -1,5 +1,3 @@ -export * from './pluginContext'; -export * from './pluginProvider'; -export * from './pluginPreprocessing'; -export * from './pluginValidation'; +export * from './pluginStore'; export * from './withPlugins'; +export * from './pluginValidation'; diff --git a/src/extend/pluginContext.js b/src/extend/pluginContext.js deleted file mode 100644 index 8e4d874591f2d81625adf0bf20bb3eae51973f21..0000000000000000000000000000000000000000 --- a/src/extend/pluginContext.js +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; - -export const PluginContext = React.createContext(); diff --git a/src/extend/pluginPreprocessing.js b/src/extend/pluginPreprocessing.js deleted file mode 100644 index 6bed9602bd70a9deda1493dd7f479e2e76f67159..0000000000000000000000000000000000000000 --- a/src/extend/pluginPreprocessing.js +++ /dev/null @@ -1,71 +0,0 @@ -import update from 'lodash/update'; -import { connect } from 'react-redux'; -import { validatePlugin } from '.'; - -/** - * Returns a mapping from targets to plugins and modes - * - * @param {Array} plugins - * @return {Object} - looks like: - * - * { - * 'WorkspacePanel': { - * wrap: [plugin3, ...], - * add: [plugin4, ...], - * }, - * ... - * } - */ -export function createTargetToPluginMapping(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: [] }; - plugins.forEach(plugin => ( - validatePlugin(plugin) - ? splittedPlugins.validPlugins.push(plugin) - : splittedPlugins.invalidPlugins.push(plugin) - )); - return splittedPlugins; -} - -/** */ -function logInvalidPlugins(plugins) { - plugins.forEach(plugin => ( - 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 }), {}); -} diff --git a/src/extend/pluginProvider.js b/src/extend/pluginProvider.js deleted file mode 100644 index 73d2739b05b442f7b721f260777895c4db2ff4d5..0000000000000000000000000000000000000000 --- a/src/extend/pluginProvider.js +++ /dev/null @@ -1,43 +0,0 @@ -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: [], -}; diff --git a/src/extend/pluginStore.js b/src/extend/pluginStore.js new file mode 100644 index 0000000000000000000000000000000000000000..0b191b3dd1e59a54f5bd57c6b81d912b5b95fb83 --- /dev/null +++ b/src/extend/pluginStore.js @@ -0,0 +1,71 @@ +import update from 'lodash/update'; +import { validatePlugin } from '.'; + +export const pluginStore = { + /** + * 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, ...], + * } + * } + */ +function mapPlugins(plugins) { + return plugins.reduce((map, plugin) => ( + update(map, [plugin.target, plugin.mode], x => [...x || [], plugin]) + ), {}); +} + +/** */ +function filterPlugins(plugins) { + const filteredPlugins = { invalidPlugins: [], validPlugins: [] }; + plugins.forEach(plugin => ( + validatePlugin(plugin) + ? filteredPlugins.validPlugins.push(plugin) + : filteredPlugins.invalidPlugins.push(plugin) + )); + return filteredPlugins; +} + +/** */ +function logInvalidPlugins(plugins) { + plugins.forEach(plugin => ( + console.log(`Mirador: Plugin ${plugin.name} is not valid and was rejected.`) + )); +} diff --git a/src/extend/withPlugins.js b/src/extend/withPlugins.js index 8d78d0ef0baaa1ff96667d572eeb6777474272ea..2f79734818156b96f4cb81749de4f4286cdc5340 100644 --- a/src/extend/withPlugins.js +++ b/src/extend/withPlugins.js @@ -1,28 +1,30 @@ -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} />; - } + if (isEmpty(plugins)) { + return <TargetComponent {...this.props} />; + } - if (!isEmpty(plugins.wrap)) { - const PluginComponent = plugins.wrap[0].component; - return <PluginComponent targetProps={props} TargetComponent={TargetComponent} />; - } + if (!isEmpty(plugins.wrap)) { + 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} />; + if (!isEmpty(plugins.add)) { + 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); diff --git a/src/lib/MiradorViewer.js b/src/lib/MiradorViewer.js index 1cbcb7e04c539d6647bed7236034041db4dd9b22..91c5544a74cf762c40ade319b9723ec911628687 100644 --- a/src/lib/MiradorViewer.js +++ b/src/lib/MiradorViewer.js @@ -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> + <App /> </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;