Skip to content

Commit

Permalink
#7585: refactor of the Print plugin as a plugins container (#7596)
Browse files Browse the repository at this point in the history
* #7585: refactor of the Print plugin as a plugins container

* #7585: refactor of the Print plugin as a plugins container, restored standard components

* #7585: refactor of the Print plugin as a plugins container, implemented override and removal of standard plugins

* #7585: refactor of the Print plugin as a plugins container, fix in Option to correctly use properties with path

* #7585: refactor of the Print plugin as a plugins container, fix for the build

* #7585: refactor of the Print plugin as a plugins container, moved the Null plugin in plugins/print, removed print subplugins from product plugins.js, updated docs

* Update web/client/plugins/Print.jsx

Co-authored-by: Lorenzo Natali <offtherailz@gmail.com>

* Update web/client/plugins/Print.jsx

Co-authored-by: Lorenzo Natali <offtherailz@gmail.com>

* #7585: Print plugin refactor as a plugins container, improved docs

Co-authored-by: Lorenzo Natali <offtherailz@gmail.com>
  • Loading branch information
mbarto and offtherailz authored Dec 14, 2021
1 parent a3475e5 commit 1a3a0b5
Show file tree
Hide file tree
Showing 19 changed files with 1,019 additions and 240 deletions.
9 changes: 9 additions & 0 deletions web/client/actions/print.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const PRINT_CAPABILITIES_LOADED = 'PRINT_CAPABILITIES_LOADED';
export const PRINT_CAPABILITIES_ERROR = 'PRINT_CAPABILITIES_ERROR';

export const SET_PRINT_PARAMETER = 'SET_PRINT_PARAMETER';
export const ADD_PRINT_PARAMETER = 'ADD_PRINT_PARAMETER';
export const CONFIGURE_PRINT_MAP = 'CONFIGURE_PRINT_MAP';
export const CHANGE_PRINT_ZOOM_LEVEL = 'CHANGE_PRINT_ZOOM_LEVEL';
export const CHANGE_MAP_PRINT_PREVIEW = 'CHANGE_MAP_PRINT_PREVIEW';
Expand Down Expand Up @@ -105,6 +106,14 @@ export function setPrintParameter(name, value) {
};
}

export function addPrintParameter(name, value) {
return {
type: ADD_PRINT_PARAMETER,
name,
value
};
}

export function configurePrintMap(center, zoom, scaleZoom, scale, layers, projection, currentLocale) {
return {
type: CONFIGURE_PRINT_MAP,
Expand Down
2 changes: 1 addition & 1 deletion web/client/components/print/PrintPreview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class PrintPreview extends React.Component {

render() {
return (
<div>
<div id="mapstore-print-preview-panel">
<div style={this.props.style}>
<Document file={this.props.url}
onLoadSuccess={this.onDocumentComplete}>
Expand Down
236 changes: 156 additions & 80 deletions web/client/plugins/Print.jsx

Large diffs are not rendered by default.

189 changes: 189 additions & 0 deletions web/client/plugins/__tests__/Print-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import React, {useEffect} from "react";
import ReactDOM from "react-dom";
import expect from "expect";

import Print from "../Print";
import { getLazyPluginForTest } from './pluginsTestUtils';
import {Null} from "../print/Null";

function getByXPath(xpath) {
return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}

const initialState = {
controls: {
print: {
enabled: true
}
},
print: {
spec: {},
capabilities: {
layouts: [],
dpis: [],
scales: []
}
}
};

const CustomComponent = ({actions}) => {
useEffect(() => {
actions.addParameter("custom", "");
}, []);
return <div>print.mycustomplugin</div>;
};

function expectDefaultItems() {
expect(document.getElementById("mapstore-print-panel")).toExist();
expect(getByXPath("//*[text()='print.title']")).toExist();
expect(getByXPath("//*[text()='print.description']")).toExist();
expect(document.getElementById("print_preview")).toExist();
expect(getByXPath("//*[text()='print.sheetsize']")).toExist();
expect(getByXPath("//*[text()='print.alternatives.legend']")).toExist();
expect(getByXPath("//*[text()='print.alternatives.2pages']")).toExist();
expect(getByXPath("//*[text()='print.alternatives.landscape']")).toExist();
expect(getByXPath("//*[text()='print.alternatives.portrait']")).toExist();
expect(getByXPath("//*[text()='print.legend.font']")).toExist();
expect(getByXPath("//*[text()='print.legend.forceLabels']")).toExist();
expect(getByXPath("//*[text()='print.legend.iconsSize']")).toExist();
expect(getByXPath("//*[text()='print.legend.dpi']")).toExist();
expect(getByXPath("//*[text()='print.resolution']")).toExist();
expect(getByXPath("//*[text()='print.submit']")).toExist();
expect(document.getElementById("mapstore-print-preview-panel")).toNotExist();
}

function getPrintPlugin({items = [], layers = [], preview = false} = {}) {
return getLazyPluginForTest({
plugin: Print,
storeState: {
...initialState,
browser: "good",
layers,
print: {
pdfUrl: preview ? "http://fakepreview" : undefined,
...initialState.print,
map: {
center: {x: 0, y: 0},
layers
}
}
},
items
});
}

describe('Print Plugin', () => {
beforeEach((done) => {
document.body.innerHTML = '<div id="container"></div>';
setTimeout(done);
});

afterEach((done) => {
ReactDOM.unmountComponentAtNode(document.getElementById("container"));
document.body.innerHTML = '';
setTimeout(done);
});

it('default configuration', (done) => {
getPrintPlugin().then(({ Plugin }) => {
try {
ReactDOM.render(<Plugin />, document.getElementById("container"));
expectDefaultItems();
done();
} catch (ex) {
done(ex);
}
});
});

it('default configuration with preview enabled', (done) => {
getPrintPlugin({
preview: true
}).then(({ Plugin }) => {
try {
ReactDOM.render(<Plugin />, document.getElementById("container"));
expect(document.getElementById("mapstore-print-preview-panel")).toExist();
done();
} catch (ex) {
done(ex);
}
});
});

it('default configuration with not allowed layers', (done) => {
getPrintPlugin({
layers: [{visibility: true, type: "bing"}]
}).then(({ Plugin }) => {
try {
ReactDOM.render(<Plugin />, document.getElementById("container"));
expectDefaultItems();
expect(getByXPath("//*[text()='print.defaultBackground']")).toExist();
done();
} catch (ex) {
done(ex);
}
});
});

it('custom plugin', (done) => {
getPrintPlugin({items: [{
plugin: CustomComponent,
target: "left-panel"
}]}).then(({ Plugin }) => {
try {
ReactDOM.render(<Plugin />, document.getElementById("container"));
expectDefaultItems();
expect(getByXPath("//*[text()='print.mycustomplugin']")).toExist();
done();
} catch (ex) {
done(ex);
}
});
});

it('custom plugin sets initial state', (done) => {
getPrintPlugin({items: [{
plugin: CustomComponent,
target: "left-panel"
}]}).then(({ Plugin, store }) => {
try {
ReactDOM.render(<Plugin />, document.getElementById("container"));
expect(store.getState()?.print?.spec?.params?.custom).toNotBe(undefined);
done();
} catch (ex) {
done(ex);
}
});
});

it('custom plugin replaces existing', (done) => {
getPrintPlugin({items: [{
plugin: CustomComponent,
target: "name"
}]}).then(({ Plugin }) => {
try {
ReactDOM.render(<Plugin />, document.getElementById("container"));
expect(getByXPath("//*[text()='print.title']")).toNotExist();
expect(getByXPath("//*[text()='print.mycustomplugin']")).toExist();
done();
} catch (ex) {
done(ex);
}
});
});

it('custom plugin removes existing', (done) => {
getPrintPlugin({items: [{
plugin: Null,
target: "name"
}]}).then(({ Plugin }) => {
try {
ReactDOM.render(<Plugin />, document.getElementById("container"));
expect(getByXPath("//*[text()='print.title']")).toNotExist();
done();
} catch (ex) {
done(ex);
}
});
});
});
91 changes: 83 additions & 8 deletions web/client/plugins/__tests__/pluginsTestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import PropTypes from "prop-types";
import endsWith from 'lodash/endsWith';
import castArray from 'lodash/castArray';
import flatten from 'lodash/flatten';
Expand All @@ -23,6 +24,8 @@ import annotations from '../../reducers/annotations';
import context from '../../reducers/context';
import security from '../../reducers/security';

import { getPlugins } from "../../utils/PluginsUtils";

// StandardStore add by default current reducers
const rootReducers = {
map,
Expand All @@ -41,13 +44,45 @@ const createRegisterActionsMiddleware = (actions) => {
};
};

function getImplementation(pluginDef) {
return Object.keys(pluginDef).reduce((previous, key) => {
if (endsWith(key, 'Plugin')) {
return pluginDef[key];
}
return previous;
}, null);
}

class PluginsContext extends React.Component {
static propTypes = {
plugins: PropTypes.object
};
static defaultProps = {
plugins: {}
};
static childContextTypes = {
plugins: PropTypes.object
};
getChildContext() {
return {
plugins: this.props.plugins
};
}
render() {
return <>{this.props.children}</>;
}
}

/**
* Helper to get a plugin configured for testing.
*
* @param {object} pluginDef plugin definition as loaded from require / import
* @param {object} storeState optional initial state for redux store (overrides default store built using plugin's reducers)
* @param {object} [plugins] optional plugins definition list ({MyPlugin: <definition>, ...}), used to filter available containers
* @params {function|function[]} [testEpics] optional epics to intercept actions for test
* @param {function|function[]} [containersReducers] optional additional reducers, that will be enabled, in addition to those defined by the plugin itself
* @param {object} [additionalPlugins] optional list of plugin definitions that will be added to the Plugin context
* @param {object[]} [items] optional list of plugin items (subplugins)
* @returns {object} an object with the following properties:
* - Plugin: plugin propertly connected to a mocked store
* - store: the mocked store
Expand All @@ -58,13 +93,8 @@ const createRegisterActionsMiddleware = (actions) => {
* import MyPlugin from './MyPlugin';
* const { Plugin, store, actions, containers } = getPluginForTest(MyPlugin, {}, {ContainerPlugin: {}});
*/
export const getPluginForTest = (pluginDef, storeState, plugins, testEpics = [], containersReducers, actions = [] ) => {
const PluginImpl = Object.keys(pluginDef).reduce((previous, key) => {
if (endsWith(key, 'Plugin')) {
return pluginDef[key];
}
return previous;
}, null);
export const getPluginForTest = (pluginDef, storeState, plugins, testEpics = [], containersReducers, actions = [], additionalPlugins = {}, items = [] ) => {
const PluginImpl = getImplementation(pluginDef);
const containers = Object.keys(PluginImpl)
.filter(prop => !plugins || Object.keys(plugins).indexOf(prop + 'Plugin') !== -1)
.reduce((previous, key) => {
Expand All @@ -87,12 +117,57 @@ export const getPluginForTest = (pluginDef, storeState, plugins, testEpics = [],
const epicMiddleware = createEpicMiddleware(rootEpic);

const store = applyMiddleware(thunkMiddleware, epicMiddleware, createRegisterActionsMiddleware(actions))(createStore)(reducer, storeState);
const pluginProps = items?.length ? {items} : undefined;
return {
PluginImpl,
Plugin: (props) => <Provider store={store}><PluginImpl {...props} /></Provider>,
Plugin: (props) => <Provider store={store}><PluginsContext plugins={getPlugins(additionalPlugins)}><PluginImpl {...props} {...pluginProps}/></PluginsContext></Provider>,
store,
actions,
containers
};
};

/**
* Helper to get a lazy loaded plugin configured for testing.
*
* @param {object} pluginDef plugin definition as loaded from require / import
* @param {object} storeState optional initial state for redux store (overrides default store built using plugin's reducers)
* @param {object} [plugins] optional plugins definition list ({MyPlugin: <definition>, ...}), used to filter available containers
* @params {function|function[]} [testEpics] optional epics to intercept actions for test
* @param {function|function[]} [containersReducers] optional additional reducers, that will be enabled, in addition to those defined by the plugin itself
* @param {object} [additionalPlugins] optional list of plugin definitions that will be added to the Plugin context
* @param {object[]} [items] optional list of plugin items (subplugins)
* @returns {Promise<object>} an object with the following properties:
* - Plugin: plugin propertly connected to a mocked store
* - store: the mocked store
* - actions: list of dispatched actions (can be read at any time to test actions launched)
* - containers: list of plugins supported containers
*
* @example
* import MyPlugin from './MyPlugin';
* getLazyPluginForTest(MyPlugin, {}, {ContainerPlugin: {}}).then(({ Plugin, store, actions, containers }) => {
* ...
* });
*/
export const getLazyPluginForTest = ({
plugin,
storeState = {},
plugins = {},
testEpics = [],
containersReducers,
actions = [],
additionalPlugins = {},
items = []} ) => {
const PluginImpl = getImplementation(plugin);
if (PluginImpl.loadPlugin) {
return new Promise((resolve) => {
PluginImpl.loadPlugin((lazy) => {
resolve(getPluginForTest({
...plugin,
LazyPlugin: lazy
}, storeState, plugins, testEpics, containersReducers, actions, additionalPlugins, items));
});
});
}
return Promise.resolve(getPluginForTest(plugin, storeState, plugins, testEpics, containersReducers, actions, additionalPlugins, items));
};
32 changes: 32 additions & 0 deletions web/client/plugins/print/ActionButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";
import {connect} from "react-redux";
import {createPlugin, handleExpression} from "../../utils/PluginsUtils";
import {Glyphicon, Button} from "react-bootstrap";
import Spinner from 'react-spinkit';
import Message from "../../components/I18N/Message";

export const ActionButton = (props) => {
const {text, actions, action, enabled, actionConfig, loading, className = ""} = props;
const {glyph, buttonConfig} = actionConfig;
const icon = glyph ? <Glyphicon glyph={glyph}/> : <span/>;
const enable = !!handleExpression({}, {...props}, "{" + enabled + "}");
return (
<Button className={className} disabled={!enable} {...buttonConfig} style={{marginTop: "10px", marginRight: "5px"}} onClick={actions[action]}>
{loading ? <Spinner spinnerName="circle" overrideSpinnerClassName="spinner" noFadeIn /> : icon} <Message msgId={text}/>
</Button>
);
};

export default createPlugin("Action", {
component: connect(
(state) => ({
spec: state?.print?.spec || {},
loading: state.print && state.print.isLoading || false
})
)(ActionButton),
containers: {
Print: {
priority: 1
}
}
});
Loading

0 comments on commit 1a3a0b5

Please sign in to comment.