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} 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)); +}; diff --git a/web/client/plugins/print/Layout.jsx b/web/client/plugins/print/Layout.jsx new file mode 100644 index 00000000000..87f5c16cf70 --- /dev/null +++ b/web/client/plugins/print/Layout.jsx @@ -0,0 +1,93 @@ +import React from "react"; +import PropTypes from "prop-types"; +import {connect} from "react-redux"; +import {createPlugin} from "../../utils/PluginsUtils"; +import { getMessageById } from '../../utils/LocaleUtils'; +import { compose } from "redux"; + +import { setPrintParameter } from "../../actions/print"; +import Sheet from "../../components/print/Sheet"; +import PrintOptionsComp from '../../components/print/PrintOptions'; +import PrintOptionComp from '../../components/print/PrintOption'; +import { currentLayouts, twoPageEnabled } from '../../selectors/print'; + +const LegendOption = connect((state) => ({ + checked: state.print && state.print.spec && !!state.print.spec.includeLegend, + layouts: currentLayouts(state) +}), { + onChange: setPrintParameter.bind(null, 'includeLegend') +})(PrintOptionComp); + +export const MultiPageOption = connect((state) => ({ + checked: state.print && state.print.spec.includeLegend && state.print.spec && !!state.print.spec.twoPages, + layouts: currentLayouts(state), + isEnabled: () => twoPageEnabled(state) +}), { + onChange: setPrintParameter.bind(null, 'twoPages') +})(PrintOptionComp); + +export const LandscapeOption = connect((state) => ({ + selected: state.print && state.print.spec && state.print.spec.landscape ? 'landscape' : 'portrait', + layouts: currentLayouts(state), + options: [{label: 'print.alternatives.landscape', value: 'landscape'}, {label: 'print.alternatives.portrait', value: 'portrait'}] +}), { + onChange: compose(setPrintParameter.bind(null, 'landscape'), (selected) => selected === 'landscape') +})(PrintOptionsComp); + + +const defaultAlternatives = [{ + name: "legend", + component: LegendOption, + regex: /legend/ +}, { + name: "2pages", + component: MultiPageOption, + regex: /2_pages/ +}, { + name: "landscape", + component: LandscapeOption, + regex: /landscape/ +}]; + +function renderLayoutsAlternatives(alternatives, context) { + return alternatives.map((alternative) => + () + ); +} + +const PrintLayout = ({layouts, spec, onChangeParameter, alternatives}, context) => { + return ( + <> + onChangeParameter("sheet", l)} + layouts={layouts} + label={getMessageById(context.messages, "print.sheetsize")} + /> + {renderLayoutsAlternatives(alternatives || defaultAlternatives, context)} + + ); +}; + +PrintLayout.contextTypes = { + messages: PropTypes.object +}; + +export default createPlugin("PrintLayout", { + component: connect( + (state) => ({ + spec: state?.print?.spec || {}, + layouts: state?.print?.capabilities?.layouts || [] + }), { + onChangeParameter: setPrintParameter + } + )(PrintLayout), + containers: { + Print: { + priority: 1 + } + } +}); diff --git a/web/client/plugins/print/LegendOptions.jsx b/web/client/plugins/print/LegendOptions.jsx new file mode 100644 index 00000000000..e0e16815967 --- /dev/null +++ b/web/client/plugins/print/LegendOptions.jsx @@ -0,0 +1,68 @@ +import React from "react"; +import PropTypes from "prop-types"; +import {connect} from "react-redux"; +import {createPlugin} from "../../utils/PluginsUtils"; +import { getMessageById } from '../../utils/LocaleUtils'; + +import { setPrintParameter } from "../../actions/print"; +import Font from "../../components/print/Font"; +import PrintOption from "../../components/print/PrintOption"; +import { TextInput } from "./TextInput"; + +const PrintLegendOptions = ({spec, onChangeParameter}, context) => { + return ( + <> + onChangeParameter('fontFamily', f)} + onChangeSize={s => onChangeParameter('fontSize', s)} + onChangeBold={b => onChangeParameter('bold', b)} + onChangeItalic={i => onChangeParameter('italic', i)} + label={getMessageById(context.messages, "print.legend.font")}/> + onChangeParameter("forceLabels", a)}/> + onChangeParameter("antiAliasing", a)}/> + + + + ); +}; + +PrintLegendOptions.contextTypes = { + messages: PropTypes.object +}; + +export default createPlugin("PrintLegendOptions", { + component: connect( + (state) => ({ + spec: state?.print?.spec || {} + }), { + onChangeParameter: setPrintParameter + } + )(PrintLegendOptions), + containers: { + Print: { + priority: 1 + } + } +}); diff --git a/web/client/plugins/print/MapPreview.jsx b/web/client/plugins/print/MapPreview.jsx new file mode 100644 index 00000000000..f5e27b4fcff --- /dev/null +++ b/web/client/plugins/print/MapPreview.jsx @@ -0,0 +1,53 @@ +import React from "react"; +import PropTypes from "prop-types"; +import {connect} from "react-redux"; +import {createPlugin} from "../../utils/PluginsUtils"; + +import MapPreview from "../../components/print/MapPreview"; +import { + changeMapPrintPreview, + changePrintZoomLevel +} from '../../actions/print'; + +const PrintMapPreview = ({mapSize, layout, layoutName, resolutions, useFixedScales, + localizedLayerStylesEnv, mapPreviewOptions, mapType, + map, capabilities, onRefresh, ...rest}) => { + const scales = capabilities.scales.slice(0).reverse().map((scale) => parseFloat(scale.value)) || []; + return ( + + ); +}; + +PrintMapPreview.contextTypes = { + messages: PropTypes.object +}; + +export default createPlugin("PrintMapPreview", { + component: connect( + (state) => ({ + map: state.print?.map, + capabilities: state.print?.capabilities ?? {} + }), { + onChangeZoomLevel: changePrintZoomLevel, + onMapViewChanges: changeMapPrintPreview + } + )(PrintMapPreview), + containers: { + Print: { + priority: 1 + } + } +}); diff --git a/web/client/plugins/print/Option.jsx b/web/client/plugins/print/Option.jsx new file mode 100644 index 00000000000..659c60ffbd5 --- /dev/null +++ b/web/client/plugins/print/Option.jsx @@ -0,0 +1,35 @@ +import React from "react"; +import PropTypes from "prop-types"; +import {connect} from "react-redux"; +import {createPlugin, handleExpression} from "../../utils/PluginsUtils"; +import { getMessageById } from '../../utils/LocaleUtils'; + +import { setPrintParameter } from "../../actions/print"; + +import PrintOptionComp from "../../components/print/PrintOption"; + +export const PrintOption = (props, context) => { + const {spec, property, label, onChangeParameter, enabled = true} = props; + return handleExpression({}, {...props}, "{" + enabled + "}") ? ( onChangeParameter(property, v)}/>) : null; +}; + +PrintOption.contextTypes = { + messages: PropTypes.object +}; + +export default createPlugin("PrintOption", { + component: connect( + (state) => ({ + spec: state?.print?.spec || {} + }), { + onChangeParameter: setPrintParameter + } + )(PrintOption), + containers: { + Print: { + priority: 1 + } + } +}); diff --git a/web/client/plugins/print/Resolution.jsx b/web/client/plugins/print/Resolution.jsx new file mode 100644 index 00000000000..2c325a5f300 --- /dev/null +++ b/web/client/plugins/print/Resolution.jsx @@ -0,0 +1,44 @@ +import React from "react"; +import PropTypes from "prop-types"; +import {connect} from "react-redux"; +import {createPlugin} from "../../utils/PluginsUtils"; +import { getMessageById } from '../../utils/LocaleUtils'; + +import { setPrintParameter } from "../../actions/print"; +import Choice from "../../components/print/Choice"; + +const Resolution = ({items, spec, onChangeParameter}, context) => { + return ( + <> + onChangeParameter("resolution", e.target.value)} + items={items} + label={getMessageById(context.messages, "print.resolution")} + /> + + ); +}; + +Resolution.contextTypes = { + messages: PropTypes.object +}; + +export default createPlugin("PrintResolution", { + component: connect( + (state) => ({ + spec: state?.print?.spec || {}, + items: state?.print?.capabilities?.dpis?.map((dpi) => ({ + name: dpi.name + ' dpi', + value: dpi.value + })) ?? [] + }), { + onChangeParameter: setPrintParameter + } + )(Resolution), + containers: { + Print: { + priority: 1 + } + } +}); diff --git a/web/client/plugins/print/TextInput.jsx b/web/client/plugins/print/TextInput.jsx new file mode 100644 index 00000000000..aaa13ee8cd4 --- /dev/null +++ b/web/client/plugins/print/TextInput.jsx @@ -0,0 +1,47 @@ +import React from "react"; +import PropTypes from "prop-types"; +import {connect} from "react-redux"; +import {createPlugin} from "../../utils/PluginsUtils"; +import { getMessageById } from '../../utils/LocaleUtils'; + +import { setPrintParameter } from "../../actions/print"; + +import {FormGroup, FormControl, ControlLabel} from "react-bootstrap"; + +import get from "lodash/get"; + +function getType(type) { + if (type !== "textarea") { + return {type}; + } + return {componentClass: type}; +} + +export const TextInput = ({spec, property, label, placeholder, onChangeParameter, path = "params.", type = "text"}, context) => { + return ( + + {label && {getMessageById(context.messages, label)} || null} + onChangeParameter(path + property, e.target.value)}/> + + ); +}; + +TextInput.contextTypes = { + messages: PropTypes.object +}; + +export default createPlugin("PrintTextInput", { + component: connect( + (state) => ({ + spec: state?.print?.spec || {} + }), { + onChangeParameter: setPrintParameter + } + )(TextInput), + containers: { + Print: { + priority: 1 + } + } +}); diff --git a/web/client/plugins/print/index.js b/web/client/plugins/print/index.js index c20cb7517d2..af81c09c25a 100644 --- a/web/client/plugins/print/index.js +++ b/web/client/plugins/print/index.js @@ -5,144 +5,18 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -import React from 'react'; -import { ControlLabel, FormControl, FormGroup } from 'react-bootstrap'; + import { connect } from 'react-redux'; -import { compose } from 'redux'; import { setControlProperty } from '../../actions/controls'; import { - changeMapPrintPreview, - changePrintZoomLevel, - printCancel, - setPrintParameter + printCancel } from '../../actions/print'; -import ChoiceComp from '../../components/print/Choice'; -import FontComp from '../../components/print/Font'; -import MapPreviewComp from '../../components/print/MapPreview'; -import PrintOptionComp from '../../components/print/PrintOption'; -import PrintOptionsComp from '../../components/print/PrintOptions'; + import PrintPreviewComp from '../../components/print/PrintPreview'; import PrintSubmitComp from '../../components/print/PrintSubmit'; -import SheetComp from '../../components/print/Sheet'; -import { currentLayouts, twoPageEnabled } from '../../selectors/print'; import ConfigUtils from '../../utils/ConfigUtils'; -export const TextWithLabel = (props) => { - return ( - - {props.label && {props.label} || null} - - - ); -}; - -export const Name = connect((state) => ({ - value: state.print && state.print.spec && state.print.spec.name || '', - type: "text" -}), { - onChange: compose(setPrintParameter.bind(null, 'name'), (e) => e.target.value) -})(TextWithLabel); - -export const Description = connect((state) => ({ - value: state.print && state.print.spec && state.print.spec.description || '', - componentClass: "textarea" -}), { - onChange: compose(setPrintParameter.bind(null, 'description'), (e) => e.target.value) -})(TextWithLabel); - -export const Resolution = connect((state) => ({ - selected: state.print && state.print.spec && state.print.spec.resolution || '', - items: state.print && state.print.capabilities && state.print.capabilities.dpis.map((dpi) => ({ - name: dpi.name + ' dpi', - value: dpi.value - })) || [] -}), { - onChange: setPrintParameter.bind(null, 'resolution') -})(ChoiceComp); - -export const Sheet = connect((state) => ({ - selected: state.print && state.print.spec && state.print.spec.sheet -}), { - onChange: setPrintParameter.bind(null, 'sheet') -})(SheetComp); - - -export const LegendOption = connect((state) => ({ - checked: state.print && state.print.spec && !!state.print.spec.includeLegend, - layouts: currentLayouts(state) -}), { - onChange: setPrintParameter.bind(null, 'includeLegend') -})(PrintOptionComp); - -export const MultiPageOption = connect((state) => ({ - checked: state.print && state.print.spec.includeLegend && state.print.spec && !!state.print.spec.twoPages, - layouts: currentLayouts(state), - isEnabled: () => twoPageEnabled(state) -}), { - onChange: setPrintParameter.bind(null, 'twoPages') -})(PrintOptionComp); - -export const LandscapeOption = connect((state) => ({ - selected: state.print && state.print.spec && state.print.spec.landscape ? 'landscape' : 'portrait', - layouts: currentLayouts(state), - options: [{label: 'print.alternatives.landscape', value: 'landscape'}, {label: 'print.alternatives.portrait', value: 'portrait'}] -}), { - onChange: compose(setPrintParameter.bind(null, 'landscape'), (selected) => selected === 'landscape') -})(PrintOptionsComp); - -export const ForceLabelsOption = connect((state) => ({ - checked: state.print && state.print.spec && !!state.print.spec.forceLabels -}), { - onChange: setPrintParameter.bind(null, 'forceLabels') -})(PrintOptionComp); - -export const AntiAliasingOption = connect((state) => ({ - checked: state.print && state.print.spec && !!state.print.spec.antiAliasing -}), { - onChange: setPrintParameter.bind(null, 'antiAliasing') -})(PrintOptionComp); - -export const IconSizeOption = connect((state) => ({ - value: state.print && state.print.spec && state.print.spec.iconSize, - type: "number" -}), { - onChange: compose(setPrintParameter.bind(null, 'iconSize'), (e) => parseInt(e.target.value, 10)) -})(TextWithLabel); - -export const LegendDpiOption = connect((state) => ({ - value: state.print && state.print.spec && state.print.spec.legendDpi, - type: "number" -}), { - onChange: compose(setPrintParameter.bind(null, 'legendDpi'), (e) => parseInt(e.target.value, 10)) -})(TextWithLabel); - -export const DefaultBackgroundOption = connect((state) => ({ - checked: state.print && state.print.spec && !!state.print.spec.defaultBackground -}), { - onChange: setPrintParameter.bind(null, 'defaultBackground') -})(PrintOptionComp); - -export const Font = connect((state) => ({ - family: state.print && state.print.spec && state.print.spec.fontFamily, - size: state.print && state.print.spec && state.print.spec.fontSize, - bold: state.print && state.print.spec && state.print.spec.bold, - italic: state.print && state.print.spec && state.print.spec.italic -}), { - onChangeFamily: setPrintParameter.bind(null, 'fontFamily'), - onChangeSize: setPrintParameter.bind(null, 'fontSize'), - onChangeBold: setPrintParameter.bind(null, 'bold'), - onChangeItalic: setPrintParameter.bind(null, 'italic') -})(FontComp); - -export const MapPreview = connect((state) => ({ - map: state.print && state.print.map, - layers: state.print && state.print.map && state.print.map.layers || [], - scales: state.print && state.print.capabilities && state.print.capabilities.scales.slice(0).reverse().map((scale) => parseFloat(scale.value)) || [] -}), { - onChangeZoomLevel: changePrintZoomLevel, - onMapViewChanges: changeMapPrintPreview -})(MapPreviewComp); export const PrintSubmit = connect((state) => ({ loading: state.print && state.print.isLoading || false @@ -160,21 +34,93 @@ export const PrintPreview = connect((state) => ({ setScale: setControlProperty.bind(null, 'print', 'viewScale') })(PrintPreviewComp); +export const defaultItems = [{ + "name": "PrintTextInput", + "id": "PrintTitle", + "override": { + "Print": { + "target": "left-panel", + "position": 1 + } + }, + "cfg": { + "property": "name", + "path": "", + "label": "print.title", + "placeholder": "print.titleplaceholder" + } +}, { + "name": "PrintTextInput", + "id": "PrintDescription", + "override": { + "Print": { + "target": "left-panel", + "position": 2 + } + }, + "cfg": { + "property": "description", + "path": "", + "label": "print.description", + "placeholder": "print.descriptionplaceholder", + "type": "textarea" + } +}, { + "name": "PrintLayout", + "override": { + "Print": { + "target": "left-panel-accordion", + "position": 1 + } + }, + "cfg": { + "title": "print.layout" + } +}, { + "name": "PrintLegendOptions", + "override": { + "Print": { + "target": "left-panel-accordion", + "position": 2 + } + }, + "cfg": { + "title": "print.legendoptions" + } +}, { + "name": "PrintResolution", + "override": { + "Print": { + "target": "right-panel", + "position": 1 + } + } +}, { + "name": "PrintMapPreview", + "override": { + "Print": { + "target": "right-panel", + "position": 2 + } + } +}, { + "name": "PrintOption", + "id": "DefaultBackgrounOption", + "override": { + "Print": { + "target": "right-panel", + "position": 3 + } + }, + "cfg": { + "enabled": "context.notAllowedLayers", + "property": "defaultBackground", + "label": "print.defaultBackground" + } +}]; + export default { - Name, - Description, - Resolution, - DefaultBackgroundOption, - Sheet, - LegendOption, - MultiPageOption, - LandscapeOption, - ForceLabelsOption, - AntiAliasingOption, - IconSizeOption, - LegendDpiOption, - Font, - MapPreview, + defaultItems, PrintSubmit, PrintPreview }; diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index dd600791bdb..0fbd110e552 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -124,7 +124,13 @@ export default { WidgetsTrayPlugin: require('../plugins/WidgetsTray').default, ZoomAllPlugin: require('../plugins/ZoomAll').default, ZoomInPlugin: require('../plugins/ZoomIn').default, - ZoomOutPlugin: require('../plugins/ZoomOut').default + ZoomOutPlugin: require('../plugins/ZoomOut').default, + PrintTextInputPlugin: require('../plugins/print/TextInput').default, + PrintLayoutPlugin: require('../plugins/print/Layout').default, + PrintLegendOptionsPlugin: require('../plugins/print/LegendOptions').default, + PrintResolutionPlugin: require('../plugins/print/Resolution').default, + PrintMapPreviewPlugin: require('../plugins/print/MapPreview').default, + PrintOptionPlugin: require('../plugins/print/Option').default }, requires: { ReactSwipe: require('react-swipeable-views').default, diff --git a/web/client/reducers/__tests__/print-test.js b/web/client/reducers/__tests__/print-test.js index 43fba6412c5..56d19a787dd 100644 --- a/web/client/reducers/__tests__/print-test.js +++ b/web/client/reducers/__tests__/print-test.js @@ -32,6 +32,15 @@ describe('Test the print reducer', () => { expect(state.spec.param).toBe('val'); }); + it('set a nested printing parameter', () => { + const state = print({spec: {}}, { + type: SET_PRINT_PARAMETER, + name: 'path.param', + value: 'val' + }); + expect(state.spec.path.param).toBe('val'); + }); + it('load capabilities', () => { const state = print({capabilities: {}, spec: {}}, { type: PRINT_CAPABILITIES_LOADED, diff --git a/web/client/reducers/print.js b/web/client/reducers/print.js index 0dc79cefe2f..8f57580ee54 100644 --- a/web/client/reducers/print.js +++ b/web/client/reducers/print.js @@ -23,6 +23,8 @@ import { TOGGLE_CONTROL } from '../actions/controls'; import { isObject, get } from 'lodash'; import assign from 'object-assign'; +import set from "lodash/set"; + const initialSpec = { antiAliasing: true, iconSize: 24, @@ -64,10 +66,7 @@ function print(state = {spec: initialSpec, capabilities: null, map: null, isLoad }); } case SET_PRINT_PARAMETER: { - return assign({}, state, { - spec: assign({}, state.spec, {[action.name]: action.value}) - } - ); + return {...state, spec: set({...state.spec}, action.name, action.value)}; } case CONFIGURE_PRINT_MAP: { diff --git a/web/client/utils/PluginsUtils.js b/web/client/utils/PluginsUtils.js index a8ed74192f2..2c34f1c9669 100644 --- a/web/client/utils/PluginsUtils.js +++ b/web/client/utils/PluginsUtils.js @@ -136,11 +136,18 @@ const getPluginSimpleName = plugin => endsWith(plugin, 'Plugin') && plugin.subst const normalizeName = name => endsWith(name, 'Plugin') && name || (name + "Plugin"); -export const getPluginConfiguration = (cfg, plugin) => { +export const getPluginsConfiguration = (cfg, plugin) => { const pluginName = getPluginSimpleName(plugin); - return head(cfg.filter((cfgObj) => cfgObj.name === pluginName || cfgObj === pluginName).map(cfgObj => isString(cfgObj) ? { - name: cfgObj - } : cfgObj)) || {}; + return cfg + .filter((cfgObj) => cfgObj.name === pluginName || cfgObj === pluginName) + .map(cfgObj => isString(cfgObj) ? { + name: cfgObj + } : cfgObj); +} + +export const getPluginConfiguration = (cfg, plugin) => { + const matches = getPluginsConfiguration(cfg, plugin); + return head(matches) || {} }; /* eslint-disable */ @@ -279,12 +286,12 @@ const executeDeferredProp = (pluginImpl, pluginConfig, name) => pluginImpl && is ({...pluginImpl, [name]: pluginImpl[name](pluginConfig)}) : pluginImpl; -export const getPluginItems = (state, plugins, pluginsConfig, containerName, containerId, isDefault, loadedPlugins, filter) => { +export const getPluginItems = (state, plugins = {}, pluginsConfig = {}, containerName, containerId, isDefault, loadedPlugins = {}, filter) => { return Object.keys(plugins) - // extract basic info for each plugins (name, implementation and config) - .map(pluginName => { - const config = getPluginConfiguration(pluginsConfig, pluginName); - return { + // extract basic info for each plugins (name, implementation and config) + .reduce((acc, pluginName) => { + const configs = getPluginsConfiguration(pluginsConfig, pluginName); + const configuredPlugins = configs.map(config => ({ name: pluginName, impl: executeDeferredProp( includeLoaded(getPluginSimpleName(pluginName), loadedPlugins, plugins[pluginName]), @@ -292,13 +299,14 @@ export const getPluginItems = (state, plugins, pluginsConfig, containerName, con containerName ), config - }; - }) - // include only plugins that are configured for the current mode + })); + return [...acc, ...configuredPlugins]; + }, []) + // include only plugins that are configured for the current mode .filter((plugin) => isValidConfiguration(plugin.config)) - // include only plugins that support container as a parent + // include only plugins that support container as a parent .filter((plugin) => canContain(containerName, plugin.impl, plugin.config.override)) - // include only plugins that are configured to be shown in container (use showIn and hideFrom to customize the behaviour) + // include only plugins that are configured to be shown in container (use showIn and hideFrom to customize the behaviour) .filter((plugin) => { return showIn(state, plugins.requires, plugin.config, containerName, containerId, isDefault); }) diff --git a/web/client/utils/PrintUtils.js b/web/client/utils/PrintUtils.js index 97bb6100edd..c4f45f50f12 100644 --- a/web/client/utils/PrintUtils.js +++ b/web/client/utils/PrintUtils.js @@ -231,7 +231,8 @@ export const getMapfishPrintSpecification = (spec) => { "rotation": 0 } ], - "legends": PrintUtils.getMapfishLayersSpecification(spec.layers, spec, 'legend') + "legends": PrintUtils.getMapfishLayersSpecification(spec.layers, spec, 'legend'), + ...spec.params }; }; /** diff --git a/web/client/utils/__tests__/PrintUtils-test.js b/web/client/utils/__tests__/PrintUtils-test.js index 536ee15436d..79193701513 100644 --- a/web/client/utils/__tests__/PrintUtils-test.js +++ b/web/client/utils/__tests__/PrintUtils-test.js @@ -461,6 +461,11 @@ describe('PrintUtils', () => { expect(printSpec.layers.length).toBe(1); expect(printSpec.geodetic).toBe(false); }); + it('getMapfishPrintSpecification custom params', () => { + const printSpec = getMapfishPrintSpecification({...testSpec, params: {custom: "customvalue"}}); + expect(printSpec).toExist(); + expect(printSpec.custom).toBe("customvalue"); + }); it('from rgba to rgb', () => { const rgb = rgbaTorgb("rgba(255, 255, 255, 0.1)"); expect(rgb).toExist();