diff --git a/web/client/plugins/Print.jsx b/web/client/plugins/Print.jsx
index b3d12aa00f6..e7dc8fac75b 100644
--- a/web/client/plugins/Print.jsx
+++ b/web/client/plugins/Print.jsx
@@ -12,7 +12,7 @@ import { head } from 'lodash';
import assign from 'object-assign';
import PropTypes from 'prop-types';
import React from 'react';
-import { Accordion, Col, Glyphicon, Grid, Panel, Row } from 'react-bootstrap';
+import { PanelGroup, Col, Glyphicon, Grid, Panel, Row } from 'react-bootstrap';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
@@ -30,6 +30,7 @@ import { reprojectBbox } from '../utils/CoordinatesUtils';
import { getMessageById } from '../utils/LocaleUtils';
import { defaultGetZoomForExtent, getResolutions, mapUpdated, dpi2dpu, DEFAULT_SCREEN_DPI } from '../utils/MapUtils';
import { isInsideResolutionsLimits } from '../utils/LayersUtils';
+import { getPluginItems } from "../utils/PluginsUtils";
/**
* Print plugin. This plugin allows to print current map view. **note**: this plugin requires the **printing module** to work.
@@ -81,20 +82,7 @@ export default {
loadPlugin: (resolve) => {
require.ensure('./print/index', () => {
const {
- Name,
- Description,
- Resolution,
- DefaultBackgroundOption,
- Sheet,
- LegendOption,
- MultiPageOption,
- LandscapeOption,
- ForceLabelsOption,
- AntiAliasingOption,
- IconSizeOption,
- LegendDpiOption,
- Font,
- MapPreview,
+ defaultItems,
PrintSubmit,
PrintPreview
} = require('./print/index').default;
@@ -151,11 +139,14 @@ export default {
currentLocaleLanguage: PropTypes.string,
overrideOptions: PropTypes.object,
isLocalizedLayerStylesEnabled: PropTypes.bool,
- localizedLayerStylesEnv: PropTypes.object
+ localizedLayerStylesEnv: PropTypes.object,
+ items: PropTypes.array
};
static contextTypes = {
- messages: PropTypes.object
+ messages: PropTypes.object,
+ plugins: PropTypes.object,
+ loadedPlugins: PropTypes.object
};
static defaultProps = {
@@ -177,19 +168,6 @@ export default {
mapType: "leaflet",
minZoom: 1,
maxZoom: 23,
- alternatives: [{
- name: "legend",
- component: LegendOption,
- regex: /legend/
- }, {
- name: "2pages",
- component: MultiPageOption,
- regex: /2_pages/
- }, {
- name: "landscape",
- component: LandscapeOption,
- regex: /landscape/
- }],
usePreview: true,
mapPreviewOptions: {
enableScalebox: false,
@@ -213,10 +191,19 @@ export default {
},
style: {},
currentLocale: 'en-US',
- overrideOptions: {}
+ overrideOptions: {},
+ items: []
};
+ state = {
+ items: [],
+ activeAccordionPanel: 0
+ }
+
UNSAFE_componentWillMount() {
+ this.setState({
+ items: getPluginItems({}, this.context.plugins, defaultItems, "Print", "", true, this.context.loadedPlugins)
+ });
this.configurePrintMap();
}
@@ -228,7 +215,11 @@ export default {
this.configurePrintMap(nextProps);
}
}
-
+ getItems = (target) => {
+ return [...this.props.items, ...this.state.items]
+ .filter(i => !target || i.target === target)
+ .sort((i1, i2) => (i1.position ?? 0) - (i2.position ?? 0));
+ };
getMapSize = (layout) => {
const currentLayout = layout || this.getLayout();
return {
@@ -243,15 +234,6 @@ export default {
return head(capabilities.layouts.filter((l) => l.name === layoutName));
};
- renderLayoutsAlternatives = () => {
- return this.props.alternatives.map((alternative) =>
- ()
- );
- };
-
renderPreviewPanel = () => {
return ;
};
@@ -269,48 +251,49 @@ export default {
}
return null;
};
-
+ renderItem = (item, options) => {
+ const Comp = item.plugin;
+ return ;
+ };
+ renderItems = (target, options) => {
+ return this.getItems(target)
+ .map(item => this.renderItem(item, options));
+ };
+ renderAccordion = (target, options) => {
+ const items = this.getItems(target);
+ return ( {
+ this.setState({
+ activeAccordionPanel: key
+ });
+ }}>
+ {items.map((item, pos) => (
+
+ {this.renderItem(item, options)}
+
+ ))}
+ );
+ };
renderPrintPanel = () => {
const layout = this.getLayout();
- const layoutName = this.props.getLayoutName(this.props.printSpec);
- const mapSize = this.getMapSize(layout);
+ const options = {
+ layout,
+ layoutName: this.props.getLayoutName(this.props.printSpec),
+ mapSize: this.getMapSize(layout),
+ resolutions: getResolutions(),
+ onRefresh: () => this.configurePrintMap(),
+ notAllowedLayers: this.isBackgroundIgnored()
+ };
return (
{this.renderError()}
{this.renderWarning(layout)}
-
-
-
-
-
- {this.renderLayoutsAlternatives()}
-
-
-
-
-
-
-
-
-
+ {this.renderItems("left-panel", options)}
+ {this.renderAccordion("left-panel-accordion", options)}
-
- this.configurePrintMap()}
- layout={layoutName}
- layoutSize={layout && layout.map || {width: 10, height: 10}}
- resolutions={getResolutions()}
- useFixedScales={this.props.useFixedScales}
- env={this.props.localizedLayerStylesEnv}
- {...this.props.mapPreviewOptions}
- />
- {this.isBackgroundIgnored() ? : null}
+ {this.renderItems("right-panel", options)}
{this.renderDownload()}
diff --git a/web/client/plugins/__tests__/Print-test.jsx b/web/client/plugins/__tests__/Print-test.jsx
new file mode 100644
index 00000000000..09fe4828348
--- /dev/null
+++ b/web/client/plugins/__tests__/Print-test.jsx
@@ -0,0 +1,123 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import expect from "expect";
+
+import Print from "../Print";
+import { getLazyPluginForTest } from './pluginsTestUtils';
+import TextInput from "../print/TextInput";
+import Layout from "../print/Layout";
+import LegendOptions from "../print/LegendOptions";
+import Resolution from "../print/Resolution";
+import MapPreview from "../print/MapPreview";
+import Option from "../print/Option";
+
+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 = () => {
+ return print.mycustomplugin
;
+};
+
+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();
+}
+
+function getPrintPlugin({items = [], layers = []} = {}) {
+ return getLazyPluginForTest({
+ plugin: Print,
+ storeState: {
+ ...initialState,
+ layers,
+ print: {
+ ...initialState.print,
+ map: {
+ center: {x: 0, y: 0},
+ layers
+ }
+ }
+ },
+ additionalPlugins: {
+ PrintTextInputPlugin: TextInput,
+ PrintLayoutPlugin: Layout,
+ PrintLegendOptionsPlugin: LegendOptions,
+ PrintResolution: Resolution,
+ PrintMapPreview: MapPreview,
+ PrintOption: Option
+ },
+ items
+ });
+}
+
+describe('Print Plugin', () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('default configuration', (done) => {
+ getPrintPlugin().then(({ Plugin }) => {
+ ReactDOM.render(, document.getElementById("container"));
+ expectDefaultItems();
+ done();
+ });
+ });
+
+ it('default configuration with not allowed layers', (done) => {
+ getPrintPlugin({
+ layers: [{visibility: true, type: "google"}]
+ }).then(({ Plugin }) => {
+ ReactDOM.render(, document.getElementById("container"));
+ expectDefaultItems();
+ expect(getByXPath("//*[text()='print.defaultBackground']")).toExist();
+ done();
+ });
+ });
+
+ it('custom plugin', (done) => {
+ getPrintPlugin({items: [{
+ plugin: CustomComponent,
+ target: "left-panel"
+ }]}).then(({ Plugin }) => {
+ ReactDOM.render(, document.getElementById("container"));
+ expectDefaultItems();
+ expect(getByXPath("//*[text()='print.mycustomplugin']")).toExist();
+ done();
+ });
+ });
+});
diff --git a/web/client/plugins/__tests__/pluginsTestUtils.js b/web/client/plugins/__tests__/pluginsTestUtils.js
index 1983bcc797c..5c4e44eba11 100644
--- a/web/client/plugins/__tests__/pluginsTestUtils.js
+++ b/web/client/plugins/__tests__/pluginsTestUtils.js
@@ -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';
@@ -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,
@@ -41,6 +44,35 @@ 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.
*
@@ -48,6 +80,9 @@ const createRegisterActionsMiddleware = (actions) => {
* @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: , ...}), 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
@@ -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) => {
@@ -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) => ,
+ Plugin: (props) => ,
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: , ...}), 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