From 47463ac896e8f400ea5042ae224cd4230804173c Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Mon, 26 Mar 2018 16:15:46 +0200 Subject: [PATCH] Fix #2754 Add map widget layer's editing (#2767) --- .../TOC/enhancers/tocItemsSettings.js | 57 ++++++----- .../__tests__/onMapViewChanges-test.js | 46 +++++++++ .../components/map/enhancers/autoResize.js | 8 +- .../map/enhancers/onMapViewChanges.js | 41 ++++++++ web/client/components/map/plugins/cesium.js | 2 +- web/client/components/map/plugins/sink.js | 18 ++++ .../widgets/builder/wizard/ChartWizard.jsx | 4 +- .../widgets/builder/wizard/MapWizard.jsx | 27 ++++- .../wizard/__tests__/MapWizard-test.jsx | 50 ++++++++++ .../builder/wizard/common/WidgetOptions.jsx | 1 - .../wizard/common/layerselector/Toolbar.jsx | 2 +- .../widgets/builder/wizard/map/MapOptions.jsx | 41 ++++++++ .../builder/wizard/map/MapSelector.jsx | 20 +++- .../widgets/builder/wizard/map/NodeEditor.jsx | 48 +++++++++ .../widgets/builder/wizard/map/PreviewMap.jsx | 16 +++ .../widgets/builder/wizard/map/TOC.jsx | 54 ++++++++++ .../widgets/builder/wizard/map/Toolbar.jsx | 21 +++- .../wizard/map/__tests__/MapOptions-test.jsx | 45 +++++++++ .../wizard/map/__tests__/NodeEditor-test.jsx | 37 +++++++ .../builder/wizard/map/__tests__/TOC-test.jsx | 50 ++++++++++ .../__tests__/handleNodeFiltering-test.js | 34 +++++++ .../handleNodePropertyChanges-test.jsx | 65 ++++++++++++ .../__tests__/handleNodeSelection-test.js | 40 ++++++++ .../enhancers/__tests__/mapToNodes-test.jsx | 45 +++++++++ .../enhancers/__tests__/nodeEditor-test.jsx | 55 +++++++++++ .../enhancers/__tests__/withSortable-test.jsx | 37 +++++++ .../map/enhancers/handleNodeFiltering.js | 52 ++++++++++ .../enhancers/handleNodePropertyChanges.js | 58 +++++++++++ .../map/enhancers/handleNodeSelection.js | 60 +++++++++++ .../wizard/map/enhancers/mapToNodes.js | 21 ++++ .../wizard/map/enhancers/nodeEditor.js | 99 +++++++++++++++++++ .../enhancers/withCapabilitiesRetrieval.js | 39 ++++++++ .../wizard/map/enhancers/withSelectedNode.js | 32 ++++++ .../wizard/map/enhancers/withSortable.js | 42 ++++++++ .../components/widgets/widget/MapView.jsx | 20 ++++ .../components/widgets/widget/MapWidget.jsx | 25 ++--- web/client/observables/wms.js | 5 + web/client/plugins/TOC.jsx | 4 +- .../plugins/widgetbuilder/ChartBuilder.jsx | 10 +- .../plugins/widgetbuilder/CounterBuilder.jsx | 7 +- .../plugins/widgetbuilder/LayerSelector.jsx | 34 +------ .../plugins/widgetbuilder/MapBuilder.jsx | 66 ++++++++++--- .../widgetbuilder/MapLayerSelector.jsx | 56 +++++++++++ .../plugins/widgetbuilder/MapSelector.jsx | 4 +- .../plugins/widgetbuilder/TableBuilder.jsx | 10 +- .../widgetbuilder/WidgetTypeSelector.jsx | 2 +- .../enhancers/chartLayerSelector.js | 22 +++++ .../enhancers/handleNodeEditing.js | 20 ++++ .../widgetbuilder/enhancers/layerSelector.js | 39 ++++++++ .../widgetbuilder/enhancers/manageLayers.js | 28 ++++++ .../widgetbuilder/enhancers/mapToolbar.js | 54 ++++++++++ web/client/reducers/config.js | 2 +- web/client/translations/data.de-DE | 6 +- web/client/translations/data.en-US | 6 +- web/client/translations/data.es-ES | 6 +- web/client/translations/data.fr-FR | 6 +- web/client/translations/data.it-IT | 6 +- web/client/utils/LayersUtils.js | 33 ++++++- 58 files changed, 1613 insertions(+), 125 deletions(-) create mode 100644 web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js create mode 100644 web/client/components/map/enhancers/onMapViewChanges.js create mode 100644 web/client/components/map/plugins/sink.js create mode 100644 web/client/components/widgets/builder/wizard/__tests__/MapWizard-test.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/MapOptions.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/NodeEditor.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/PreviewMap.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/TOC.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/__tests__/MapOptions-test.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/__tests__/NodeEditor-test.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/__tests__/TOC-test.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodeFiltering-test.js create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodePropertyChanges-test.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodeSelection-test.js create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/__tests__/mapToNodes-test.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/__tests__/nodeEditor-test.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/__tests__/withSortable-test.jsx create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/handleNodeFiltering.js create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/handleNodePropertyChanges.js create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/handleNodeSelection.js create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/mapToNodes.js create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/nodeEditor.js create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/withCapabilitiesRetrieval.js create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/withSelectedNode.js create mode 100644 web/client/components/widgets/builder/wizard/map/enhancers/withSortable.js create mode 100644 web/client/components/widgets/widget/MapView.jsx create mode 100644 web/client/plugins/widgetbuilder/MapLayerSelector.jsx create mode 100644 web/client/plugins/widgetbuilder/enhancers/chartLayerSelector.js create mode 100644 web/client/plugins/widgetbuilder/enhancers/handleNodeEditing.js create mode 100644 web/client/plugins/widgetbuilder/enhancers/layerSelector.js create mode 100644 web/client/plugins/widgetbuilder/enhancers/manageLayers.js create mode 100644 web/client/plugins/widgetbuilder/enhancers/mapToolbar.js diff --git a/web/client/components/TOC/enhancers/tocItemsSettings.js b/web/client/components/TOC/enhancers/tocItemsSettings.js index abab3ed652..56b81561a0 100644 --- a/web/client/components/TOC/enhancers/tocItemsSettings.js +++ b/web/client/components/TOC/enhancers/tocItemsSettings.js @@ -6,8 +6,8 @@ * LICENSE file in the root directory of this source tree. */ -const {isNil, isEqual} = require('lodash'); -const {withState, withHandlers, compose, lifecycle} = require('recompose'); +const { isNil, isEqual } = require('lodash'); +const { withState, withHandlers, compose, lifecycle } = require('recompose'); /** * Enhancer for settings state needed in TOCItemsSettings plugin @@ -39,37 +39,44 @@ const settingsState = compose( */ const settingsLifecycle = compose( withHandlers({ - onUpdateParams: props => (newParams, update = true) => { - let originalSettings = {...props.originalSettings}; + onUpdateParams: ({ + settings = {}, + initialSettings = {}, + originalSettings: orig, + onUpdateOriginalSettings = () => {}, + onUpdateSettings = () => {}, + onUpdateNode = () => {} + }) => (newParams, update = true) => { + let originalSettings = { ...(orig || {}) }; // TODO one level only storage of original settings for the moment Object.keys(newParams).forEach((key) => { - originalSettings[key] = props.initialSettings[key]; + originalSettings[key] = initialSettings && initialSettings[key]; }); - props.onUpdateOriginalSettings(originalSettings); - props.onUpdateSettings(newParams); + onUpdateOriginalSettings(originalSettings); + onUpdateSettings(newParams); if (update) { - props.onUpdateNode( - props.settings.node, - props.settings.nodeType, - {...props.settings.options, ...newParams} + onUpdateNode( + settings.node, + settings.nodeType, + { ...settings.options, ...newParams } ); } }, - onClose: ({onUpdateNode, originalSettings, settings, onHideSettings, onShowAlertModal}) => forceClose => { - const originalOptions = Object.keys(settings.options).reduce((options, key) => ({...options, [key]: key === 'opacity' && !originalSettings[key] && 1.0 || originalSettings[key]}), {}); + onClose: ({ onUpdateNode, originalSettings, settings, onHideSettings, onShowAlertModal }) => forceClose => { + const originalOptions = Object.keys(settings.options).reduce((options, key) => ({ ...options, [key]: key === 'opacity' && !originalSettings[key] && 1.0 || originalSettings[key] }), {}); if (!isEqual(originalOptions, settings.options) && !forceClose) { onShowAlertModal(true); } else { onUpdateNode( settings.node, settings.nodeType, - {...settings.options, ...originalSettings} + { ...settings.options, ...originalSettings } ); onHideSettings(); onShowAlertModal(false); } }, - onSave: ({onHideSettings = () => {}, onShowAlertModal = () => {}}) => () => { + onSave: ({ onHideSettings = () => { }, onShowAlertModal = () => { } }) => () => { onHideSettings(); onShowAlertModal(false); } @@ -78,19 +85,19 @@ const settingsLifecycle = compose( componentWillMount() { const { element = {}, - onUpdateOriginalSettings = () => {}, - onUpdateInitialSettings = () => {} + onUpdateOriginalSettings = () => { }, + onUpdateInitialSettings = () => { } } = this.props; - onUpdateOriginalSettings({...element}); - onUpdateInitialSettings({...element}); + onUpdateOriginalSettings({ ...element }); + onUpdateInitialSettings({ ...element }); }, componentWillReceiveProps(newProps) { // an empty description does not trigger the single layer getCapabilites, // it does only for missing description const { settings = {}, - onRetrieveLayerData = () => {} + onRetrieveLayerData = () => { } } = this.props; if (!settings.expanded && newProps.settings && newProps.settings.expanded && isNil(newProps.element.description) && newProps.element.type === "wms") { @@ -100,15 +107,15 @@ const settingsLifecycle = compose( componentWillUpdate(newProps) { const { settings = {}, - onUpdateOriginalSettings = () => {}, - onUpdateInitialSettings = () => {}, - onSetTab = () => {} + onUpdateOriginalSettings = () => { }, + onUpdateInitialSettings = () => { }, + onSetTab = () => { } } = this.props; if (!settings.expanded && newProps.settings && newProps.settings.expanded) { // update initial and original settings - onUpdateOriginalSettings({...newProps.element}); - onUpdateInitialSettings({...newProps.element}); + onUpdateOriginalSettings({ ...newProps.element }); + onUpdateInitialSettings({ ...newProps.element }); onSetTab('general'); } } diff --git a/web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js b/web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js new file mode 100644 index 0000000000..c4d28036e1 --- /dev/null +++ b/web/client/components/map/enhancers/__tests__/onMapViewChanges-test.js @@ -0,0 +1,46 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const ReactDOM = require('react-dom'); +const {createSink} = require('recompose'); +const expect = require('expect'); +const onMapViewChanges = require('../onMapViewChanges'); + +describe('onMapViewChanges enhancer', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('onMapViewChanges rendering with defaults', () => { + const Sink = onMapViewChanges(createSink( props => { + expect(props.eventHandlers.onMapViewChanges).toExist(); + setTimeout(props.eventHandlers.onMapViewChanges("CENTER", "ZOOM", { bbox: { x: 2 } }, "SIZE", "mapStateSource", "projection")); + + })); + const actions = { + onMapViewChanges: () => {} + }; + const spy = expect.spyOn(actions, 'onMapViewChanges'); + ReactDOM.render(, document.getElementById("container")); + expect(spy).toHaveBeenCalled(); + const map = spy.calls[0].arguments[0]; + expect(map).toExist(); + expect(map.center).toExist(); + expect(map.zoom).toExist(); + expect(map.bbox).toExist(); + expect(map.size).toExist(); + expect(map.mapStateSource).toExist(); + expect(map.projection).toExist(); + }); + +}); diff --git a/web/client/components/map/enhancers/autoResize.js b/web/client/components/map/enhancers/autoResize.js index 0be990dc8f..f0b17861af 100644 --- a/web/client/components/map/enhancers/autoResize.js +++ b/web/client/components/map/enhancers/autoResize.js @@ -19,13 +19,7 @@ module.exports = (debounceTime = 0) => compose( resize: 0 }), { - onResize: ({resize = 0, map}) => ({width, height} = {}) => ({ - map: { - ...map, - size: { - width, height - } - }, + onResize: ({resize = 0}) => () => ({ resize: resize + 1 }) } diff --git a/web/client/components/map/enhancers/onMapViewChanges.js b/web/client/components/map/enhancers/onMapViewChanges.js new file mode 100644 index 0000000000..1040aaa775 --- /dev/null +++ b/web/client/components/map/enhancers/onMapViewChanges.js @@ -0,0 +1,41 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const {compose, withHandlers, withPropsOnChange} = require('recompose'); + +/** + * Enhance map to add the `onMapViewChanges` handler passed as prop to the list of event handlers. + * The handler is called with only one parameter (the map) that is the result of the merge of + * original callback (where center, zoom, bbox, size, mapStateSource and projection are separated) with the current `map` prop + */ +module.exports = compose( + withHandlers({ + onMapViewChanges: ({ map = {}, onMapViewChanges = () => {}}) => (center, zoom, bbox, size, mapStateSource, projection) => { + onMapViewChanges({ + ...map, + center, + bbox: { + ...map.bbox, + ...bbox + }, + zoom, + size, + mapStateSource, + projection + }); + } + }), + withPropsOnChange( + ['onMapViewChanges', 'eventHandlers'], + ({ onMapViewChanges = () => {}, eventHandlers ={} } = {}) => ({ + eventHandlers: { + ...eventHandlers, + onMapViewChanges + } + }) + ) +); diff --git a/web/client/components/map/plugins/cesium.js b/web/client/components/map/plugins/cesium.js index a40ef3a3cb..456cff6c3a 100644 --- a/web/client/components/map/plugins/cesium.js +++ b/web/client/components/map/plugins/cesium.js @@ -11,6 +11,6 @@ module.exports = () => { return { Map: require('../cesium/Map'), Layer: require('../cesium/Layer'), - Feature: createSink() + Feature: createSink(() => {}) }; }; diff --git a/web/client/components/map/plugins/sink.js b/web/client/components/map/plugins/sink.js new file mode 100644 index 0000000000..42bb02b3ac --- /dev/null +++ b/web/client/components/map/plugins/sink.js @@ -0,0 +1,18 @@ +/** +* Copyright 2016, GeoSolutions Sas. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ +const { createSink } = require('recompose'); +/** + * Dummy implementation of mapType for tests + */ +module.exports = () => { + return { + Map: createSink(() => {}), + Layer: createSink(() => {}), + Feature: createSink(() => {}) + }; +}; diff --git a/web/client/components/widgets/builder/wizard/ChartWizard.jsx b/web/client/components/widgets/builder/wizard/ChartWizard.jsx index 4fc3d7c862..c7caed7341 100644 --- a/web/client/components/widgets/builder/wizard/ChartWizard.jsx +++ b/web/client/components/widgets/builder/wizard/ChartWizard.jsx @@ -20,14 +20,14 @@ const dependenciesToFilter = require('../../enhancers/dependenciesToFilter'); const emptyChartState = require('../../enhancers/emptyChartState'); const errorChartState = require('../../enhancers/errorChartState'); const {compose, lifecycle} = require('recompose'); -const enhanchePreview = compose( +const enhancePreview = compose( dependenciesToFilter, wpsChart, loadingState, errorChartState, emptyChartState ); -const PreviewChart = enhanchePreview(require('../../../charts/SimpleChart')); +const PreviewChart = enhancePreview(require('../../../charts/SimpleChart')); const SampleChart = sampleData(require('../../../charts/SimpleChart')); const sampleProps = { diff --git a/web/client/components/widgets/builder/wizard/MapWizard.jsx b/web/client/components/widgets/builder/wizard/MapWizard.jsx index 0e23ef46e7..b5d18c1007 100644 --- a/web/client/components/widgets/builder/wizard/MapWizard.jsx +++ b/web/client/components/widgets/builder/wizard/MapWizard.jsx @@ -10,21 +10,42 @@ const WidgetOptions = require('./common/WidgetOptions'); const {wizardHandlers} = require('../../../misc/wizard/enhancers'); const Wizard = wizardHandlers(require('../../../misc/wizard/WizardContainer')); +const MapOptions = require('./map/MapOptions'); +const Preview = require('./map/PreviewMap'); + module.exports = ({ onChange = () => {}, onFinish = () => {}, setPage= () => {}, step=0, - editorData = {} + selectedNodes=[], + onNodeSelect = () => {}, + editorData = {}, + editNode, + setEditNode = () => {}, + closeNodeEditor = () => {} } = {}) => ( + } + map={editorData.map} + /> - - ); +); diff --git a/web/client/components/widgets/builder/wizard/__tests__/MapWizard-test.jsx b/web/client/components/widgets/builder/wizard/__tests__/MapWizard-test.jsx new file mode 100644 index 0000000000..a67cbbaf0f --- /dev/null +++ b/web/client/components/widgets/builder/wizard/__tests__/MapWizard-test.jsx @@ -0,0 +1,50 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const ReactDOM = require('react-dom'); +const PropTypes = require('prop-types'); +const { withContext } = require('recompose'); +const expect = require('expect'); + +const mockStore = withContext({ + store: PropTypes.any +}, ({store = {}} = {}) => ({ + store: { + dispatch: () => { }, + subscribe: () => { }, + getState: () => ({}), + ...store + } +})); +const MapWizard = mockStore(require('../MapWizard')); +describe('MapWizard component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('MapWizard rendering with map', () => { + // mock the store for empty map type + const store = { + getState: () => ({ + maptype: { + mapType: 'sink' + } + }) + }; + const map = { groups: [{ id: 'GGG' }], layers: [{ id: "LAYER", name: "Layer", group: "GGG", options: {} }] }; + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const el = container.querySelector('.ms-wizard'); + expect(el).toExist(); + }); +}); diff --git a/web/client/components/widgets/builder/wizard/common/WidgetOptions.jsx b/web/client/components/widgets/builder/wizard/common/WidgetOptions.jsx index 014cce8a42..107e66f538 100644 --- a/web/client/components/widgets/builder/wizard/common/WidgetOptions.jsx +++ b/web/client/components/widgets/builder/wizard/common/WidgetOptions.jsx @@ -27,7 +27,6 @@ module.exports = ({data = {}, onChange = () => {}, sampleChart}) => ( onChange("title", e.target.value)} /> - diff --git a/web/client/components/widgets/builder/wizard/common/layerselector/Toolbar.jsx b/web/client/components/widgets/builder/wizard/common/layerselector/Toolbar.jsx index 08d472a47a..3b154cbb3b 100644 --- a/web/client/components/widgets/builder/wizard/common/layerselector/Toolbar.jsx +++ b/web/client/components/widgets/builder/wizard/common/layerselector/Toolbar.jsx @@ -18,7 +18,7 @@ module.exports = ({canProceed, selected, onProceed = () => {}} = {}) => (); diff --git a/web/client/components/widgets/builder/wizard/map/MapOptions.jsx b/web/client/components/widgets/builder/wizard/map/MapOptions.jsx new file mode 100644 index 0000000000..811dd779a8 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/MapOptions.jsx @@ -0,0 +1,41 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const StepHeader = require('../../../../misc/wizard/StepHeader'); +const Message = require('../../../../I18N/Message'); +const TOC = require('./TOC'); +const nodeEditor = require('./enhancers/nodeEditor'); +const Editor = nodeEditor(require('./NodeEditor')); + +module.exports = ({ preview, map = {}, onChange = () => { }, selectedNodes = [], onNodeSelect = () => { }, editNode, closeNodeEditor = () => { } }) => (
+ } /> +
+
+ {preview} +
+
+ {editNode + ? [} />, + ] + : } +
); diff --git a/web/client/components/widgets/builder/wizard/map/MapSelector.jsx b/web/client/components/widgets/builder/wizard/map/MapSelector.jsx index 7c756e65de..c2254c3cb0 100644 --- a/web/client/components/widgets/builder/wizard/map/MapSelector.jsx +++ b/web/client/components/widgets/builder/wizard/map/MapSelector.jsx @@ -10,6 +10,7 @@ const React = require('react'); require('rxjs'); const GeoStoreDAO = require('../../../../../api/GeoStoreDAO'); +const ConfigUtils = require('../../../../../utils/ConfigUtils'); const BorderLayout = require('../../../../layout/BorderLayout'); @@ -25,10 +26,22 @@ const MapCatalog = mcEnhancer(require('../../../../maps/MapCatalog')); module.exports = compose( withState('selected', "setSelected", null), withHandlers({ - onMapChoice: ({ onMapSelected = () => { } } = {}) => map => GeoStoreDAO.getData(map.id) + onMapChoice: ({ onMapSelected = () => { } } = {}) => map => GeoStoreDAO + .getData(map.id) + .then((config => { + let mapState = !config.version ? ConfigUtils.convertFromLegacy(config) : ConfigUtils.normalizeConfig(config.map); + return { + ...mapState, + layers: mapState.layers.map(l => { + if (l.group === "background" && (l.type === "ol" || l.type === "OpenLayers.Layer")) { + l.type = "empty"; + } + return l; + }) + }; + })) .then(res => onMapSelected({ - map: res.map, - layers: res.layers + map: res })) }), mapPropsStream(props$ => @@ -50,6 +63,7 @@ module.exports = compose( bsSize: "sm" }} buttons={[{ + tooltipId: "widgets.builder.wizard.useThisMap", onClick: () => onMapChoice(selected), visible: true, disabled: !selected, diff --git a/web/client/components/widgets/builder/wizard/map/NodeEditor.jsx b/web/client/components/widgets/builder/wizard/map/NodeEditor.jsx new file mode 100644 index 0000000000..5ecd3cc662 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/NodeEditor.jsx @@ -0,0 +1,48 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); +const Message = require('../../../../I18N/Message'); +const tooltip = require('../../../../misc/enhancers/tooltip'); + +const { Row, Col, Nav, NavItem: BSNavItem, Glyphicon } = require('react-bootstrap'); +const NavItem = tooltip(BSNavItem); +/** + * Provides a node (layer or group) property editor for the TOC + */ +module.exports = ({ + settings, element = {}, tabs = [], activeTab, width, groups, + setActiveTab = () => { }, onUpdateParams = () => { }, onRetrieveLayerData = () => { }, realtimeUpdate, ...props} = {}) => + ( + + + + + {tabs.filter(tab => tab.id && tab.id === activeTab).filter(tab => tab.Component).map(tab => ( + onUpdateParams({ [key]: value }, realtimeUpdate)} /> + ))} + ); diff --git a/web/client/components/widgets/builder/wizard/map/PreviewMap.jsx b/web/client/components/widgets/builder/wizard/map/PreviewMap.jsx new file mode 100644 index 0000000000..f36221728a --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/PreviewMap.jsx @@ -0,0 +1,16 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + + +const { compose, withHandlers} = require('recompose'); + +module.exports = compose( + withHandlers({ + onMapViewChanges: ({onChange = () => {}}) => map => onChange('map', map) + }) +)(require('../../../widget/MapView')); diff --git a/web/client/components/widgets/builder/wizard/map/TOC.jsx b/web/client/components/widgets/builder/wizard/map/TOC.jsx new file mode 100644 index 0000000000..f9c25dfb7a --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/TOC.jsx @@ -0,0 +1,54 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const { compose } = require('recompose'); +const TOC = require('../../../../TOC/TOC'); +const DefaultLayerOrGroup = require('../../../../TOC/DefaultLayerOrGroup'); +const DefaultGroup = require('../../../../TOC/DefaultGroup'); +const DefaultLayer = require('../../../../TOC/DefaultLayer'); + +const handleNodePropertyChanges = require('./enhancers/handleNodePropertyChanges'); +const handleNodeFiltering = require('./enhancers/handleNodeFiltering'); +const mapToNodes = require('./enhancers/mapToNodes'); +const withSortable = require('./enhancers/withSortable'); + +const enhanceTOC = compose( + mapToNodes, + handleNodeFiltering, + handleNodePropertyChanges, + withSortable +); + +module.exports = enhanceTOC(({ + changeLayerPropertyByGroup = () => {}, + changeLayerProperty = () => {}, + changeGroupProperty = () => {}, + onSort, + onSelect, + selectedNodes, + nodes =[]} = {} + ) => + Object.keys(changes).map(k => changeLayerPropertyByGroup( id, k, changes[k]))} + onToggle={(id, expanded) => changeGroupProperty(id, "expanded", !expanded)} + groupVisibilityCheckbox/>} + layerElement={ Object.keys(changes).map(k => changeLayerProperty(layer, k, changes[k]))} + onUpdateNode={(layer, _, changes) => Object.keys(changes).map(k => changeLayerProperty(layer, k, changes[k]))} + onToggle={(id, expanded) => changeLayerProperty(id, "expanded", !expanded)} />} /> +); diff --git a/web/client/components/widgets/builder/wizard/map/Toolbar.jsx b/web/client/components/widgets/builder/wizard/map/Toolbar.jsx index 81e482084d..d3a2afb80c 100644 --- a/web/client/components/widgets/builder/wizard/map/Toolbar.jsx +++ b/web/client/components/widgets/builder/wizard/map/Toolbar.jsx @@ -17,13 +17,28 @@ const getSaveTooltipId = (step, {id} = {}) => { return "widgets.builder.wizard.addToTheMap"; }; -module.exports = ({step = 0, editorData = {}, onFinish = () => {}} = {}) => ( {}, onFinish = () => { }, toggleLayerSelector = () => { } } = {}) => ( onFinish(Math.min(step + 1, 1)), + buttons={buttons || [ ...(step === 0 ? tocButtons : []), { + onClick: () => toggleLayerSelector(true), + visible: step === 0, + glyph: "plus", + tooltipId: "widgets.builder.wizard.addLayer" + }, { + onClick: () => setPage(Math.max(step - 1, 0)), + visible: step === 1, + glyph: "arrow-left", + tooltipId: "widgets.builder.wizard.configureMapOptions" + }, { + onClick: () => setPage(Math.min(step + 1, 2)), visible: step === 0, + glyph: "arrow-right", + tooltipId: "widgets.builder.wizard.configureWidgetOptions" + }, { + onClick: () => onFinish(Math.min(step + 1, 1)), + visible: step === 1, glyph: "floppy-disk", tooltipId: getSaveTooltipId(step, editorData) }]} />); diff --git a/web/client/components/widgets/builder/wizard/map/__tests__/MapOptions-test.jsx b/web/client/components/widgets/builder/wizard/map/__tests__/MapOptions-test.jsx new file mode 100644 index 0000000000..193cf6a761 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/__tests__/MapOptions-test.jsx @@ -0,0 +1,45 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const ReactDOM = require('react-dom'); + +const expect = require('expect'); +const MapOptions = require('../MapOptions'); + +describe('MapOptions component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('MapOptions rendering with defaults', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + expect(container.querySelector('.mapstore-step-title')).toExist(); + // renders the TOC + expect(container.querySelector('#mapstore-layers')).toExist(); + // not the Editor + expect(container.querySelector('.ms-row-tab')).toNotExist(); + }); + it('MapOptions rendering node editor', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + // renders the editor + expect(container.querySelector('.ms-row-tab')).toExist(); + // not the TOC + expect(container.querySelector('#mapstore-layers')).toNotExist(); + }); +}); diff --git a/web/client/components/widgets/builder/wizard/map/__tests__/NodeEditor-test.jsx b/web/client/components/widgets/builder/wizard/map/__tests__/NodeEditor-test.jsx new file mode 100644 index 0000000000..58d4621d8e --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/__tests__/NodeEditor-test.jsx @@ -0,0 +1,37 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const ReactDOM = require('react-dom'); +const {createSink} = require('recompose'); +const expect = require('expect'); +const NodeEditor = require('../NodeEditor'); +describe('NodeEditor component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('NodeEditor rendering with defaults', () => { + ReactDOM.render( {}) + }]} element={{}} />, document.getElementById("container")); + const container = document.getElementById('container'); + // search the icon rendered + const el = container.querySelector('.glyphicon-wrench'); + expect(el).toExist(); + }); +}); diff --git a/web/client/components/widgets/builder/wizard/map/__tests__/TOC-test.jsx b/web/client/components/widgets/builder/wizard/map/__tests__/TOC-test.jsx new file mode 100644 index 0000000000..916221e69d --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/__tests__/TOC-test.jsx @@ -0,0 +1,50 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); +const ReactDOM = require('react-dom'); +const ReactTestUtils = require('react-dom/test-utils'); + +const expect = require('expect'); +const TOC = require('../TOC'); +describe('TOC component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('TOC rendering with layer', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const el = container.querySelector('.toc-title'); + expect(el).toExist(); + }); + it('Test TOC onChange', () => { + const actions = { + onChange: () => {} + }; + const spyonChange = expect.spyOn(actions, 'onChange'); + ReactDOM.render(, document.getElementById("container")); + + const container = document.getElementById('container'); + const el = container.querySelector('.visibility-check'); + expect(el).toExist(); + ReactTestUtils.Simulate.click(el); // <-- trigger event callback + expect(spyonChange).toHaveBeenCalled(); + }); +}); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodeFiltering-test.js b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodeFiltering-test.js new file mode 100644 index 0000000000..604f9e4c36 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodeFiltering-test.js @@ -0,0 +1,34 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const ReactDOM = require('react-dom'); +const {createSink} = require('recompose'); +const expect = require('expect'); +const handleNodeFiltering = require('../handleNodeFiltering'); + +describe('handleNodeFiltering enhancer', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('handleNodeFiltering rendering nodes defaults', (done) => { + const Sink = handleNodeFiltering(createSink( props => { + expect(props).toExist(); + expect(props.nodes).toExist(); + expect(props.nodes[0]).toExist(); + expect(props.nodes[0].showComponent).toBe(true); + done(); + })); + ReactDOM.render(, document.getElementById("container")); + }); +}); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodePropertyChanges-test.jsx b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodePropertyChanges-test.jsx new file mode 100644 index 0000000000..77d2108ddd --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodePropertyChanges-test.jsx @@ -0,0 +1,65 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const ReactDOM = require('react-dom'); +const {createSink} = require('recompose'); +const expect = require('expect'); +const handleNodePropertyChanges = require('../handleNodePropertyChanges'); + +describe('handleNodePropertyChanges enhancer', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('handleNodePropertyChanges rendering with defaults', (done) => { + const Sink = handleNodePropertyChanges(createSink( props => { + expect(props).toExist(); + expect(props.changeGroupProperty).toExist(); + expect(props.changeLayerProperty).toExist(); + expect(props.changeLayerPropertyByGroup).toExist(); + done(); + })); + ReactDOM.render(, document.getElementById("container")); + }); + it('Test handleNodePropertyChange onChange calls', () => { + const actions = { + onChange: () => {} + }; + const spyCallbacks = expect.spyOn(actions, 'onChange'); + const Sink = handleNodePropertyChanges(createSink(props => { + expect(props).toExist(); + props.changeGroupProperty("GGG", "title", "TEST"); + props.changeLayerProperty("LAYER", "name", "TEST"); + props.changeLayerPropertyByGroup("GGG", "title", "TEST"); + props.updateMapEntries({ + a: "a", + b: "b" + }); + + })); + ReactDOM.render(, document.getElementById("container")); + expect(spyCallbacks.calls.length).toBe(5); + expect(spyCallbacks.calls[0].arguments[0]).toBe("map.groups[0].title"); + expect(spyCallbacks.calls[0].arguments[1]).toBe("TEST"); + expect(spyCallbacks.calls[1].arguments[0]).toBe("map.layers[0].name"); + expect(spyCallbacks.calls[1].arguments[1]).toBe("TEST"); + expect(spyCallbacks.calls[2].arguments[0]).toBe("map.layers[0].title"); + expect(spyCallbacks.calls[2].arguments[1]).toBe("TEST"); + expect(spyCallbacks.calls[3].arguments[0]).toBe("map[a]"); + expect(spyCallbacks.calls[3].arguments[1]).toBe("a"); + expect(spyCallbacks.calls[4].arguments[0]).toBe("map[b]"); + expect(spyCallbacks.calls[4].arguments[1]).toBe("b"); + }); +}); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodeSelection-test.js b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodeSelection-test.js new file mode 100644 index 0000000000..c69e0e8c79 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/handleNodeSelection-test.js @@ -0,0 +1,40 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); +const ReactDOM = require('react-dom'); +const {createSink} = require('recompose'); +const expect = require('expect'); +const handleNodeSelection = require('../handleNodeSelection'); + +describe('handleNodeSelection enhancer', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('handleNodeSelection rendering with defaults', (done) => { + const Sink = handleNodeSelection(createSink( props => { + expect(props).toExist(); + expect(props.onNodeSelect).toExist(); + props.onNodeSelect('LAYER', "layers"); + // after onNodeSelect call the selectedNodes array is populated + if (props.selectedNodes && props.selectedNodes.length === 1) { + expect(props.selectedNodes[0]).toBe('LAYER'); + done(); + } + })); + ReactDOM.render(, document.getElementById("container")); + }); +}); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/mapToNodes-test.jsx b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/mapToNodes-test.jsx new file mode 100644 index 0000000000..a27743aa8b --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/mapToNodes-test.jsx @@ -0,0 +1,45 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const ReactDOM = require('react-dom'); +const {createSink} = require('recompose'); +const expect = require('expect'); +const mapToNodes = require('../mapToNodes'); + +describe('mapToNodes enhancer', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('mapToNodes rendering with defaults', (done) => { + const Sink = mapToNodes(createSink( props => { + expect(props).toExist(); + expect(props.nodes).toExist(); + expect(props.nodes.length).toBe(1); + const gNode = props.nodes[0]; + expect(gNode.name).toBe("GGG"); + expect(gNode.title).toBe("GGG"); + expect(gNode.id).toBe("GGG"); + expect(gNode.nodes.length).toBe(1); + const lNode = gNode.nodes[0]; + expect(lNode.id).toBe("LAYER"); + expect(lNode.name).toBe("LAYER"); + expect(lNode.group).toBe("GGG"); + expect(lNode.options).toExist(); + done(); + })); + ReactDOM.render(, document.getElementById("container")); + }); +}); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/nodeEditor-test.jsx b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/nodeEditor-test.jsx new file mode 100644 index 0000000000..a660320118 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/nodeEditor-test.jsx @@ -0,0 +1,55 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const ReactDOM = require('react-dom'); +const {createSink} = require('recompose'); +const expect = require('expect'); +const nodeEditor = require('../nodeEditor'); + +describe('nodeEditor enhancer', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('nodeEditor rendering with map', (done) => { + const Sink = nodeEditor(createSink( props => { + expect(props).toExist(); + expect(props.groups.length).toBe(1); + expect(props.nodes).toExist(); + expect(props.element).toExist(); + expect(props.activeTab).toBe("general"); + expect(props.settings.nodeType).toBe('layers'); + done(); + })); + ReactDOM.render(, document.getElementById("container")); + }); + it('nodeEditor rendering callback', () => { + const Sink = nodeEditor(createSink( props => { + expect(props.onChange).toExist(); + props.onChange("a", "b"); + + })); + const actions = { + onChange: () => { } + }; + const spyonChange = expect.spyOn(actions, 'onChange'); + ReactDOM.render(, document.getElementById("container")); + expect(spyonChange).toHaveBeenCalled(); + }); + +}); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/withSortable-test.jsx b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/withSortable-test.jsx new file mode 100644 index 0000000000..b97438ee40 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/__tests__/withSortable-test.jsx @@ -0,0 +1,37 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const ReactDOM = require('react-dom'); +const {createSink} = require('recompose'); +const expect = require('expect'); +const withSortable = require('../withSortable'); + +describe('withSortable enhancer', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('withSortable rendering with defaults', (done) => { + const Sink = withSortable(createSink( props => { + expect(props).toExist(); + props.onSort('GROUP', [1, 0]); + })); + ReactDOM.render( { + expect(layers.length).toBe(2); + expect(layers[0].id).toBe("LAYER_2"); + done(); + }}/>, document.getElementById("container")); + }); +}); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/handleNodeFiltering.js b/web/client/components/widgets/builder/wizard/map/enhancers/handleNodeFiltering.js new file mode 100644 index 0000000000..e2199e005a --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/handleNodeFiltering.js @@ -0,0 +1,52 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const { head} = require('lodash'); +const { withProps } = require('recompose'); + +const addFilteredAttributesGroups = (nodes, filters) => { + return nodes.reduce((newNodes, currentNode) => { + let node = { ...currentNode }; + if (node.nodes) { + node = { ...node, nodes: addFilteredAttributesGroups(node.nodes, filters) }; + } + filters.forEach(filter => { + if (node.nodes && filter.func(node)) { + node = { ...node, ...filter.options }; + } else if (node.nodes) { + node = { ...node }; + } + }); + newNodes.push(node); + return newNodes; + }, []); +}; +const filterLayersByTitle = () => true; + +/** + * Dummy replacement of original logic to add filtering by title in TOC. + * For the moment it only adds the defaults + * @param {object} props + */ +const addNodeFilters = ({ nodes, filterText, currentLocale }) => addFilteredAttributesGroups(nodes, [ + { + options: { showComponent: true }, + func: () => !filterText + }, + { + options: { loadingError: true }, + func: (node) => head(node.nodes.filter(n => n.loadingError && n.loadingError !== 'Warning')) + }, + { + options: { expanded: true, showComponent: true }, + func: (node) => filterText && head(node.nodes.filter(l => filterLayersByTitle(l, filterText, currentLocale) || l.nodes && head(node.nodes.filter(g => g.showComponent)))) + } +]); + +module.exports = withProps((props) => ({ + nodes: addNodeFilters(props) +})); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/handleNodePropertyChanges.js b/web/client/components/widgets/builder/wizard/map/enhancers/handleNodePropertyChanges.js new file mode 100644 index 0000000000..0c09e6a000 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/handleNodePropertyChanges.js @@ -0,0 +1,58 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +// handle property changes +const { withHandlers } = require('recompose'); +const {belongsToGroup} = require('../../../../../../utils/LayersUtils'); +const { findIndex } = require('lodash'); +/** + * Add to the TOC or the Node editor some handlers for TOC nodes + * add to the wrapped component the following methods: + * - changeLayerProperty (id, key, value) - calls onChange on map.layers[index].key + * - changeLayerPropertyGroup (gid, key, value) - calls multiple times onChange + * - changeGroupProperty(gid, key, value) - calls onChange on map.groups[index].key + * - updateMapEntries(object) - calls multiple times onChange + * These method will call the method `onChange` from props mapping accordingly + * @prop {function} onChange callback with arguments : (path, value) -> path will be something like: `map.layers[2].title` or `map.groups[1].title`, `map[somethingElse]` + */ +module.exports = withHandlers({ + /** + * Changes the layer property + */ + changeLayerProperty: + ({ onChange = () => { }, map = {} }) => + (id, key, value) => { + const index = findIndex(map.layers || [], { + id + }); + onChange(`map.layers[${index}].${key}`, value); + }, + /** + * Change layer properties by group + * + */ + changeLayerPropertyByGroup: + ({ onChange = () => { }, map = {} }) => + (gid, key, value) => + map.layers + .filter(belongsToGroup(gid)) + .map(({ id } = {}) => findIndex(map.layers || [], { id })) + .filter(i => i >= 0) + .map(index => onChange(`map.layers[${index}].${key}`, value)), + /** + * Change group properties (expanded...) + */ + changeGroupProperty: + ({ onChange = () => { }, map = [] }) => + (id, key, value) => { + const index = findIndex(map.groups || [], { + id + }); + onChange(`map.groups[${index}].${key}`, value); + }, + updateMapEntries: ({ onChange = () => { } }) => (obj = {}) => Object.keys(obj).map(k => onChange(`map[${k}]`, obj[k])) +}); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/handleNodeSelection.js b/web/client/components/widgets/builder/wizard/map/enhancers/handleNodeSelection.js new file mode 100644 index 0000000000..123da441e6 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/handleNodeSelection.js @@ -0,0 +1,60 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +// handle selection +const { withProps, compose, withStateHandlers } = require('recompose'); +const {findIndex} = require('lodash'); +const getGroupLayerIds = (id, map) => + (map.layers || []) + .filter(({ group = "Default" } = {}) => group === id) + .map(({ id: lid } = {}) => lid); +/** + * Allows management of node selection in localState. Useful to use TOC. + * Requires a `map` prop with groups and layers. Each layer must have an id property + * passes to wrapped component : + * - onNodeSelect : handler to call to select a node. example: `onNodeSelected(nodeId, 'layers', false);` nodeType should be one of 'layers' or 'groups' + * - selectedNodes: array of id of the selected nodes + * - selectedLayers, selectedGroups: same as selectedNodes, but only with selected groups or layers ids + */ +module.exports = compose( + withStateHandlers( + () => ({ selectedLayers: [], selectedGroups: [] }), + { + onNodeSelect: ({ selectedLayers = [], selectedGroups = [] }, { map = {} }) => (id, nodeType, ctrlKey) => ({ + selectedLayers: nodeType === "group" + ? findIndex(selectedGroups, item => item === id) >= 0 + // remove all layers + ? selectedLayers.filter(item => findIndex(getGroupLayerIds(id, map), lid => lid === item) < 0) + // add all layers + : ctrlKey + ? [...selectedLayers, ...getGroupLayerIds(id, map)] + : [...getGroupLayerIds(id, map)] + // layer selection + : findIndex(selectedLayers, item => item === id) >= 0 + // remove + ? selectedLayers.filter(i => i !== id) + : ctrlKey + ? [...selectedLayers, id] + : [id], + selectedGroups: nodeType === "group" + ? findIndex(selectedGroups, item => item === id) >= 0 + // remove group + ? selectedGroups.filter(g => g !== id) + // add group + : ctrlKey + ? [...selectedGroups, id] + : [id] + : ctrlKey + ? selectedGroups + : [] + }) + } + ), + withProps(({ selectedLayers, selectedGroups }) => ({ + selectedNodes: [...selectedLayers, ...selectedGroups] + })) +); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/mapToNodes.js b/web/client/components/widgets/builder/wizard/map/enhancers/mapToNodes.js new file mode 100644 index 0000000000..158ff442c2 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/mapToNodes.js @@ -0,0 +1,21 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const LayersUtils = require('../../../../../../utils/LayersUtils'); +const { withProps } = require('recompose'); + +/** + * Maps MapStore's map (as stored on back-end) to be mapped properly to + * TOC nodes + * + */ +const mapToNodes = ({ map }) => ({ + nodes: ( + ({ layers = {} }) => (LayersUtils.denormalizeGroups(layers.flat || [], layers.groups || []).groups) + )(LayersUtils.splitMapAndLayers(map)) +}); +module.exports = withProps(mapToNodes); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/nodeEditor.js b/web/client/components/widgets/builder/wizard/map/enhancers/nodeEditor.js new file mode 100644 index 0000000000..275c9eb953 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/nodeEditor.js @@ -0,0 +1,99 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const { compose, withProps, withState, withHandlers } = require('recompose'); +const { get } = require('lodash'); + +const withControllableState = require('../../../../../misc/enhancers/withControllableState'); +const {splitMapAndLayers} = require('../../../../../../utils/LayersUtils'); +const mapToNodes = require('./mapToNodes'); +const withSelectedNode = require('./withSelectedNode'); +const withCapabilitiesRetrieval = require('./withCapabilitiesRetrieval'); + +/* TABS definitions */ +const General = require('../../../../../TOC/fragments/settings/General'); +const Display = require('../../../../../TOC/fragments/settings/Display'); +const WMSStyle = withCapabilitiesRetrieval(require('../../../../../TOC/fragments/settings/WMSStyle')); +const handleNodePropertyChanges = require('./handleNodePropertyChanges'); +const { settingsLifecycle } = require('../../../../../TOC/enhancers/tocItemsSettings'); + +const withDefaultTabs = withProps((props) => ({ + tabs: props.tabs || [{ + id: 'general', + titleId: 'layerProperties.general', + tooltipId: 'layerProperties.general', + glyph: 'wrench', + visible: true, + Component: General + }, + { + id: 'display', + titleId: 'layerProperties.display', + tooltipId: 'layerProperties.display', + glyph: 'eye-open', + visible: props.settings && props.settings.nodeType === 'layers', + Component: Display + }, + { + id: 'style', + titleId: 'layerProperties.style', + tooltipId: 'layerProperties.style', + glyph: 'dropper', + visible: props.settings && props.settings.nodeType === 'layers' && props.element && props.element.type === "wms", + Component: WMSStyle + }] +})); +/** + * Manages internal TOC node editor state exposing only + * @prop {string} editNode the ID of the node to edit + * @prop {object} map the map (with layers and groups) + * @prop {function} onChange method called when a change has been applied + * @example + * // `path` can be something like `map.layers[idx].prop` (path definition of lodash get, set) + * set(map, path, value)} /> // set could be immutable version of lodash set + */ +module.exports = compose( + // select selected node + mapToNodes, + withSelectedNode, + // manage settings variables for local changes (required by tocItemsSettings tabs) + withState('settings', 'onUpdateSettings', {}), + // transform selected node to fit tocItemsSettings props + withProps(({ map = {}, selectedNode, settings = {}} = {}) => ({ + element: selectedNode, + settings: { + ...settings, + nodeType: selectedNode.nodes ? "groups" : "layers", + options: { + + opacity: settings.opacity >= 0 + ? settings.opacity + : selectedNode.opacity >= 0 + ? selectedNode.opacity + : 1 + } + }, + groups: get(splitMapAndLayers(map), 'layers.groups') + })), + // adapter for handlers + handleNodePropertyChanges, + withHandlers({ + onUpdateNode: ({changeLayerProperty = () => {}, changeGroupProperty = () => {}, editNode}) => (node, type, newProps) => { + if (type === "layers") { + Object.keys(newProps).map(k => changeLayerProperty(editNode, k, newProps[k])); + } + if (type === "groups") { + Object.keys(newProps).map(k => changeGroupProperty(editNode, k, newProps[k])); + } + } + }), + // manage local changes + settingsLifecycle, + // manage tabs of node editor + withControllableState('activeTab', 'setActiveTab', 'general'), + withDefaultTabs +); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/withCapabilitiesRetrieval.js b/web/client/components/widgets/builder/wizard/map/enhancers/withCapabilitiesRetrieval.js new file mode 100644 index 0000000000..832ab0aa55 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/withCapabilitiesRetrieval.js @@ -0,0 +1,39 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const { createEventHandler, mapPropsStream } = require('recompose'); +const { getLayerCapabilities } = require('../../../../../../observables/wms'); +const Rx = require('rxjs'); +module.exports = mapPropsStream(props$ => { + const { stream: retrieveLayerData$, handler: retrieveLayerData} = createEventHandler(); + return props$ + .pluck('element') + .distinctUntilChanged((a = {}, b = {}) => a.id === b.id) + .switchMap(() => + retrieveLayerData$.switchMap((element) => + getLayerCapabilities(element) + .map(layerCapability => ({ + capabilities: layerCapability, + capabilitiesLoading: null, + description: layerCapability._abstract, + boundingBox: layerCapability.latLonBoundingBox, + availableStyles: layerCapability.style && (Array.isArray(layerCapability.style) ? layerCapability.style : [layerCapability.style]) + })).startWith({ + capabilitiesLoading: true + })) + .catch((error) => Rx.Observable.of({ capabilitiesLoading: null, capabilities: { error: "error getting capabilities", details: error }, description: null })) + ) + .startWith({}) + .combineLatest(props$, (elementProps = {}, props = {}) => ({ + ...props, + retrieveLayerData, + element: { + ...props.element, + ...elementProps + } + })); +}); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/withSelectedNode.js b/web/client/components/widgets/builder/wizard/map/enhancers/withSelectedNode.js new file mode 100644 index 0000000000..185af82973 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/withSelectedNode.js @@ -0,0 +1,32 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +/** + * Works with handleNodeSelection and mapToNode to retrieve node in `nodes` with the `editNode` id. + */ +const { isMatch } = require('lodash'); +const { withProps } = require('recompose'); +const traverse = (branch = [], filter) => { + for (let i = 0; i < branch.length; i++) { + if (isMatch(branch[i], filter)) { + return branch[i]; + } + } + + for (let j = 0; j < branch.length; j++) { + let result = traverse(branch[j].nodes, filter); + if (result !== undefined) { + return result; + } + } + + return undefined; // no match found + +}; +module.exports = withProps(({ nodes = {}, editNode }) => ({ + selectedNode: editNode && traverse(nodes, { id: editNode }) +})); diff --git a/web/client/components/widgets/builder/wizard/map/enhancers/withSortable.js b/web/client/components/widgets/builder/wizard/map/enhancers/withSortable.js new file mode 100644 index 0000000000..a7ac7447d7 --- /dev/null +++ b/web/client/components/widgets/builder/wizard/map/enhancers/withSortable.js @@ -0,0 +1,42 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const {withHandlers} = require('recompose'); +const {get} = require('lodash'); +const { deepChange, sortLayers: DEFAULT_SORT_LAYERS, splitMapAndLayers, getNode} = require('../../../../../../utils/LayersUtils'); +/** + * add sorting capabilities to TOC. + * This is a porting logic from layers reducer. TODO: refactor. + * NOTES: To understand this algorithm (original from layers reducer + StandardStore re-mappings of state) + * you should consider that the order of tree is imposed by the layers, because a group can not be empty. + * So the groups are only a presentation of the TOC. + * This version doesn't keep in state the ids of nested nodes because is not necessary. + * requires updateMapEntries callback, that can be added using handleNodePropertyChanges enhancer + */ +module.exports = withHandlers({ + onSort: ({ map = {}, activateSortLayers = true, filterText, sortLayers = DEFAULT_SORT_LAYERS, updateMapEntries = () => {}}) => + activateSortLayers && !filterText + ? (nodeId, order = []) => { + // get the groups in form {nodes: ["layer1_id", "layer2_id", {id: nestedGroup, nodes: [...]}]} + const { flat: layers, groups = [] } = get(splitMapAndLayers(map), 'layers') || {}; + // get the list of nodes to change order + const node = getNode(groups || [], nodeId); + const nodes = nodeId === 'root' ? groups : node.nodes; + if (nodes) { + // modify the groups object to apply sortLayers + const reorderedGroups = order.map(idx => nodes[idx]); + const newGroups = nodeId === 'root' ? reorderedGroups : + deepChange(groups, nodeId, 'nodes', reorderedGroups); + // modify layer's order + const newLayers = sortLayers ? sortLayers(newGroups, layers || []) : layers || []; + updateMapEntries({ + layers: newLayers + }); + } + } + : null +}); diff --git a/web/client/components/widgets/widget/MapView.jsx b/web/client/components/widgets/widget/MapView.jsx new file mode 100644 index 0000000000..fda99b6eae --- /dev/null +++ b/web/client/components/widgets/widget/MapView.jsx @@ -0,0 +1,20 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const autoMapType = require('../../map/enhancers/autoMapType'); +const mapType = require('../../map/enhancers/mapType'); +const autoResize = require('../../map/enhancers/autoResize'); +const onMapViewChanges = require('../../map/enhancers/onMapViewChanges'); +const {compose} = require('recompose'); +module.exports = compose( + onMapViewChanges, + autoResize(0), + autoMapType, + mapType + +)(require('../../map/BaseMap')); + diff --git a/web/client/components/widgets/widget/MapWidget.jsx b/web/client/components/widgets/widget/MapWidget.jsx index 6a834682e5..0c64a86c2b 100644 --- a/web/client/components/widgets/widget/MapWidget.jsx +++ b/web/client/components/widgets/widget/MapWidget.jsx @@ -7,18 +7,14 @@ */ const React = require('react'); const WidgetContainer = require('./WidgetContainer'); +const InfoPopover = require('./InfoPopover'); const Message = require('../../I18N/Message'); -const autoMapType = require('../../map/enhancers/autoMapType'); -const mapType = require('../../map/enhancers/mapType'); -const autoResize = require('../../map/enhancers/autoResize'); -const MMap = autoResize(0)(autoMapType(mapType(require('../../map/BaseMap')))); +const {withHandlers} = require('recompose'); +const MapView = withHandlers({ + onMapViewChanges: ({ updateProperty = () => { } }) => map => updateProperty('map', map) +})(require('./MapView')); -const MapView = ({id, map, layers = []}) => (); const { Glyphicon, ButtonToolbar, @@ -26,15 +22,22 @@ const { MenuItem } = require('react-bootstrap'); + +const renderHeaderLeftTopItem = ({ title, description } = {}) => { + return description ? : null; +}; + module.exports = ({ onEdit = () => { }, + updateProperty = () => { }, toggleDeleteConfirm = () => { }, - id, title, + id, title, loading, description, map, confirmDelete = false, onDelete = () => {} } = {}) => ( } noCaret id="dropdown-no-caret"> onEdit()} eventKey="3">  @@ -42,5 +45,5 @@ module.exports = ({ } > - + ); diff --git a/web/client/observables/wms.js b/web/client/observables/wms.js index 63d443ddd7..2d5e78d08b 100644 --- a/web/client/observables/wms.js +++ b/web/client/observables/wms.js @@ -7,6 +7,8 @@ */ const {Observable} = require('rxjs'); const axios = require('../libs/ajax'); +const WMS = require('../api/WMS'); +const LayersUtils = require('../utils/LayersUtils'); const urlUtil = require('url'); const {interceptOGCError} = require('../utils/ObservableUtils'); const toDescribeLayerURL = ({name, search = {}, url} = {}) => { @@ -28,6 +30,9 @@ const toDescribeLayerURL = ({name, search = {}, url} = {}) => { }; const describeLayer = l => Observable.defer( () => axios.get(toDescribeLayerURL(l))).let(interceptOGCError); module.exports = { + getLayerCapabilities: l => Observable.defer(() => WMS.getCapabilities(LayersUtils.getCapabilitiesUrl(l))) + .let(interceptOGCError) + .map(c => WMS.parseLayerCapabilities(c, l)), describeLayer, addSearch: l => describeLayer(l) diff --git a/web/client/plugins/TOC.jsx b/web/client/plugins/TOC.jsx index 904d5dae96..49aae11355 100644 --- a/web/client/plugins/TOC.jsx +++ b/web/client/plugins/TOC.jsx @@ -445,12 +445,12 @@ class LayerTree extends React.Component { * @prop {boolean} cfg.activateRemoveLayer: activate remove layer tool, default `true` * @prop {boolean} cfg.activateQueryTool: activate query tool options, default `false` * @prop {boolean} cfg.activateDownloadTool: activate a button to download layer data through wfs, default `false` - * @prop {boolean} cfg.activateSortLayer: activate drag and drob to sort layers, default `true` + * @prop {boolean} cfg.activateSortLayer: activate drag and drop to sort layers, default `true` * @prop {boolean} cfg.activateAddLayerButton: activate a button to open the catalog, default `false` * @prop {object} cfg.layerOptions: options to pass to the layer. * @prop {boolean} cfg.showFullTitleOnExpand shows full length title in the legend. default `false`. * Some of the layerOptions are: `legendContainerStyle`, `legendStyle`. These 2 allow to customize the legend: - * For instance you can pass some stying props to the legend. + * For instance you can pass some styling props to the legend. * this example is to make the legend scrollable horizontally * ``` * "layerOptions": { diff --git a/web/client/plugins/widgetbuilder/ChartBuilder.jsx b/web/client/plugins/widgetbuilder/ChartBuilder.jsx index d31d6489f1..556aeb464b 100644 --- a/web/client/plugins/widgetbuilder/ChartBuilder.jsx +++ b/web/client/plugins/widgetbuilder/ChartBuilder.jsx @@ -16,7 +16,7 @@ const BorderLayout = require('../../components/layout/BorderLayout'); const {insertWidget, onEditorChange, setPage, openFilterEditor, changeEditorSetting} = require('../../actions/widgets'); const builderConfiguration = require('../../components/widgets/enhancers/builderConfiguration'); - +const chartLayerSelector = require('./enhancers/chartLayerSelector'); const { wizardStateToProps, wizardSelector @@ -49,18 +49,18 @@ const Toolbar = connect(wizardSelector, { )(require('../../components/widgets/builder/wizard/chart/Toolbar')); /* - * in case you don't have a layer selected (e.g. dashboard) the chartbuilder + * in case you don't have a layer selected (e.g. dashboard) the chart builder * prompts a catalog view to allow layer selection */ -const chooseLayerEhnancer = compose( +const chooseLayerEnhancer = compose( connect(wizardSelector), branch( ({layer} = {}) => !layer, - renderComponent(require('./LayerSelector')) + renderComponent(chartLayerSelector(require('./LayerSelector'))) ) ); -module.exports = chooseLayerEhnancer(({enabled, onClose = () => {}, dependencies, ...props} = {}) => +module.exports = chooseLayerEnhancer(({enabled, onClose = () => {}, dependencies, ...props} = {}) => (} diff --git a/web/client/plugins/widgetbuilder/CounterBuilder.jsx b/web/client/plugins/widgetbuilder/CounterBuilder.jsx index b4b6132197..a5d1524ca1 100644 --- a/web/client/plugins/widgetbuilder/CounterBuilder.jsx +++ b/web/client/plugins/widgetbuilder/CounterBuilder.jsx @@ -16,6 +16,7 @@ const BorderLayout = require('../../components/layout/BorderLayout'); const { insertWidget, onEditorChange, setPage, openFilterEditor, changeEditorSetting } = require('../../actions/widgets'); const builderConfiguration = require('../../components/widgets/enhancers/builderConfiguration'); +const chartLayerSelector = require('./enhancers/chartLayerSelector'); const { wizardStateToProps, @@ -52,15 +53,15 @@ const Toolbar = connect(wizardSelector, { * in case you don't have a layer selected (e.g. dashboard) the chartbuilder * prompts a catalog view to allow layer selection */ -const chooseLayerEhnancer = compose( +const chooseLayerEhahncer = compose( connect(wizardSelector), branch( ({ layer } = {}) => !layer, - renderComponent(require('./LayerSelector')) + renderComponent(chartLayerSelector(require('./LayerSelector'))) ) ); -module.exports = chooseLayerEhnancer(({ enabled, onClose = () => { }, dependencies, ...props } = {}) => +module.exports = chooseLayerEhahncer(({ enabled, onClose = () => { }, dependencies, ...props } = {}) => (} diff --git a/web/client/plugins/widgetbuilder/LayerSelector.jsx b/web/client/plugins/widgetbuilder/LayerSelector.jsx index 3d8907d34a..3e5c0585e6 100644 --- a/web/client/plugins/widgetbuilder/LayerSelector.jsx +++ b/web/client/plugins/widgetbuilder/LayerSelector.jsx @@ -9,18 +9,13 @@ const React = require('react'); const {connect} = require('react-redux'); const {createSelector} = require('reselect'); -const Rx = require('rxjs'); const BorderLayout = require('../../components/layout/BorderLayout'); const {selectedCatalogSelector} = require('../../selectors/catalog'); const Toolbar = require('../../components/widgets/builder/wizard/common/layerselector/Toolbar'); const BuilderHeader = require('./BuilderHeader'); const InfoPopover = require('../../components/widgets/widget/InfoPopover'); const {Message, HTML} = require('../../components/I18N/I18N'); -const {recordToLayer} = require('../../utils/CatalogUtils'); -const canGenerateCharts = require('../../observables/widgets/canGenerateCharts'); -const {compose, withState, mapPropsStream, branch} = require('recompose'); -const {onEditorChange} = require('../../actions/widgets'); -const {addSearch} = require('../../observables/wms'); +const { compose, branch} = require('recompose'); const Catalog = compose( branch( @@ -30,30 +25,9 @@ const Catalog = compose( )(require('./Catalog')); /** * Builder page that allows layer's selection + * @prop {function} [layerValidationStream] */ -module.exports = compose( - connect( () => {}, { - onLayerChoice: (l) => onEditorChange("layer", l) - }), - withState('selected', "setSelected", null), - withState('layer', "setLayer", null), - mapPropsStream(props$ => - props$.distinctUntilKeyChanged('selected').filter(({selected} = {}) => selected) - .switchMap( - ({selected, setLayer = () => {}} = {}) => - canGenerateCharts(recordToLayer(selected)) - .switchMap(() => addSearch(recordToLayer(selected))) - .do(l => setLayer(l)) - .mapTo({canProceed: true}) - .catch((error) => Rx.Observable.of({error, canProceed: false})) - ).startWith({}) - .combineLatest(props$, ({canProceed} = {}, props) => ({ - canProceed, - ...props - }) - ) - ) -)(({onClose = () => {}, setSelected = () => {}, onLayerChoice = () => {}, selected, canProceed, layer, catalog, catalogServices} = {}) => +module.exports = ({onClose = () => {}, setSelected = () => {}, onLayerChoice = () => {}, selected, canProceed, layer, catalog, catalogServices} = {}) => ( @@ -66,4 +40,4 @@ module.exports = compose( } > setSelected(r)} /> - )); + ); diff --git a/web/client/plugins/widgetbuilder/MapBuilder.jsx b/web/client/plugins/widgetbuilder/MapBuilder.jsx index e798f5ec45..999d55254b 100644 --- a/web/client/plugins/widgetbuilder/MapBuilder.jsx +++ b/web/client/plugins/widgetbuilder/MapBuilder.jsx @@ -7,27 +7,46 @@ */ const React = require('react'); const {connect} = require('react-redux'); -const {onEditorChange, insertWidget, setPage} = require('../../actions/widgets'); +const {onEditorChange} = require('../../actions/widgets'); const {wizardSelector, wizardStateToProps} = require('./commons'); +const layerSelector = require('./enhancers/layerSelector'); +const manageLayers = require('./enhancers/manageLayers'); +const mapToolbar = require('./enhancers/mapToolbar'); +const handleNodeEditing = require('./enhancers/handleNodeEditing'); const BorderLayout = require('../../components/layout/BorderLayout'); + const BuilderHeader = require('./BuilderHeader'); +const { compose, branch, renderComponent, withState, withHandlers, withProps } = require('recompose'); +const handleNodeSelection = require('../../components/widgets/builder/wizard/map/enhancers/handleNodeSelection'); + +const Toolbar = mapToolbar(require('../../components/widgets/builder/wizard/map/Toolbar')); -const Toolbar = connect(wizardSelector, { - setPage, - insertWidget - }, - wizardStateToProps -)(require('../../components/widgets/builder/wizard/map/Toolbar')); -const { compose, branch, renderComponent } = require('recompose'); /* - * in case you don't have a layer selected (e.g. dashboard) the chartbuilder - * prompts a catalog view to allow layer selection + * Prompts Map Selection or Layer selector (to add layers) */ const chooseMapEnhancer = compose( connect(wizardSelector), + // map selector branch( ({ editorData = {} } = {}) => !editorData.map, renderComponent(require('./MapSelector')) + ), + // layer selector - to add layers to the map + withState('layerSelectorOpen', 'toggleLayerSelector', false), + branch( + ({ layerSelectorOpen = false } = {}) => layerSelectorOpen, + renderComponent( + compose( + manageLayers, + withHandlers({ + onLayerChoice: ({ toggleLayerSelector = () => { }, addLayer = () => { } }) => (layer) => { + addLayer(layer); + toggleLayerSelector(false); + } + }), + layerSelector + )(require('./MapLayerSelector')) + ) ) ); const Builder = connect( @@ -37,10 +56,31 @@ const Builder = connect( }, wizardStateToProps )(require('../../components/widgets/builder/wizard/MapWizard')); -module.exports = chooseMapEnhancer(({enabled, onClose = () => {}} = {}) => + +const mapBuilder = compose( + chooseMapEnhancer, + withProps(({ editorData = {}}) => ({ + map: editorData.map + })), + handleNodeSelection, + handleNodeEditing +); + + +module.exports = mapBuilder(({ enabled, onClose = () => { }, toggleLayerSelector = () => { }, editNode, setEditNode, closeNodeEditor, selectedGroups=[], selectedLayers=[], selectedNodes, onNodeSelect = () => { } } = {}) => (} + header={( + )} > - {enabled ? : null} + {enabled ? : null} )); diff --git a/web/client/plugins/widgetbuilder/MapLayerSelector.jsx b/web/client/plugins/widgetbuilder/MapLayerSelector.jsx new file mode 100644 index 0000000000..3e3e29d8c6 --- /dev/null +++ b/web/client/plugins/widgetbuilder/MapLayerSelector.jsx @@ -0,0 +1,56 @@ +/* + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const {connect} = require('react-redux'); +const {createSelector} = require('reselect'); + +const BorderLayout = require('../../components/layout/BorderLayout'); +const {selectedCatalogSelector} = require('../../selectors/catalog'); +const Toolbar = require('../../components/misc/toolbar/Toolbar'); +const BuilderHeader = require('./BuilderHeader'); +const InfoPopover = require('../../components/widgets/widget/InfoPopover'); +const {Message, HTML} = require('../../components/I18N/I18N'); +const {compose, branch} = require('recompose'); + +const Catalog = compose( + branch( + ({catalog} = {}) => !catalog, + connect(createSelector(selectedCatalogSelector, catalog => ({catalog}))) + ), +)(require('./Catalog')); +/** + * Builder page that allows layer's selection + */ +module.exports = ({ onClose = () => { }, setSelected = () => { }, onLayerChoice = () => { }, toggleLayerSelector = () => {}, selected, canProceed, layer, catalog, catalogServices} = {}) => + ( + toggleLayerSelector(false), + tooltipId: "close", + glyph: "1-close" + }, { + onClick: () => onLayerChoice(layer), + disabled: !selected || !canProceed, + tooltipId: "widgets.builder.wizard.useTheSelectedLayer", + glyph: "plus" + }]} /> + { selected && !canProceed ? } + text={} /> : null} + } + > + setSelected(r)} /> + ); diff --git a/web/client/plugins/widgetbuilder/MapSelector.jsx b/web/client/plugins/widgetbuilder/MapSelector.jsx index 22fa9a50cc..844d318732 100644 --- a/web/client/plugins/widgetbuilder/MapSelector.jsx +++ b/web/client/plugins/widgetbuilder/MapSelector.jsx @@ -7,11 +7,11 @@ */ const {connect} = require('react-redux'); const { onEditorChange } = require('../../actions/widgets'); - +const { normalizeMap } = require('../../utils/LayersUtils'); module.exports = connect( () => ({}), { - onMapSelected: ({ map }) => onEditorChange("map", map) + onMapSelected: ({ map }) => onEditorChange("map", normalizeMap(map)) } diff --git a/web/client/plugins/widgetbuilder/TableBuilder.jsx b/web/client/plugins/widgetbuilder/TableBuilder.jsx index e2679a48a1..de3a922a0a 100644 --- a/web/client/plugins/widgetbuilder/TableBuilder.jsx +++ b/web/client/plugins/widgetbuilder/TableBuilder.jsx @@ -20,7 +20,7 @@ const BorderLayout = require('../../components/layout/BorderLayout'); const { insertWidget, onEditorChange, setPage, openFilterEditor, changeEditorSetting } = require('../../actions/widgets'); const builderConfiguration = require('../../components/widgets/enhancers/builderConfiguration'); - +const chartLayerSelector = require('./enhancers/chartLayerSelector'); const { wizardStateToProps, wizardSelector @@ -64,18 +64,18 @@ const Toolbar = connect(wizardSelector, { )(require('../../components/widgets/builder/wizard/table/Toolbar')); /* - * in case you don't have a layer selected (e.g. dashboard) the chartbuilder + * in case you don't have a layer selected (e.g. dashboard) the table builder * prompts a catalog view to allow layer selection */ -const chooseLayerEhnancer = compose( +const chooseLayerEnhancer = compose( connect(wizardSelector), branch( ({ layer } = {}) => !layer, - renderComponent(require('./LayerSelector')) + renderComponent(chartLayerSelector(require('./LayerSelector'))) ) ); -module.exports = chooseLayerEhnancer(({ enabled, onClose = () => { }, editorData = {}, dependencies, ...props } = {}) => +module.exports = chooseLayerEnhancer(({ enabled, onClose = () => { }, editorData = {}, dependencies, ...props } = {}) => ( {}, + () => ({}), { onSelect: (type) => onEditorChange("widgetType", type) } diff --git a/web/client/plugins/widgetbuilder/enhancers/chartLayerSelector.js b/web/client/plugins/widgetbuilder/enhancers/chartLayerSelector.js new file mode 100644 index 0000000000..8d01f34105 --- /dev/null +++ b/web/client/plugins/widgetbuilder/enhancers/chartLayerSelector.js @@ -0,0 +1,22 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const { connect } = require('react-redux'); +const { compose, defaultProps, setDisplayName } = require('recompose'); +const layerSelector = require('./layerSelector'); +const { onEditorChange } = require('../../../actions/widgets'); +const canGenerateCharts = require('../../../observables/widgets/canGenerateCharts'); +module.exports = compose( + setDisplayName('ChartLayerSelector'), + connect(() => { }, { + onLayerChoice: (l) => onEditorChange("layer", l) + }), + defaultProps({ + layerValidationStream: stream$ => stream$.switchMap(layer => canGenerateCharts(layer)) + }), + layerSelector +); diff --git a/web/client/plugins/widgetbuilder/enhancers/handleNodeEditing.js b/web/client/plugins/widgetbuilder/enhancers/handleNodeEditing.js new file mode 100644 index 0000000000..24c2d9953f --- /dev/null +++ b/web/client/plugins/widgetbuilder/enhancers/handleNodeEditing.js @@ -0,0 +1,20 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const {connect} = require('react-redux'); +const {createSelector} = require('reselect'); +const { changeEditorSetting } = require('../../../actions/widgets'); +const {getEditorSettings} = require('../../../selectors/widgets'); +module.exports = connect( createSelector( + getEditorSettings, + ({ editNode } = {}) => ({ + editNode + }) + ), { + setEditNode: node => changeEditorSetting('editNode', node), + closeNodeEditor: () => changeEditorSetting('editNode', undefined) +}); diff --git a/web/client/plugins/widgetbuilder/enhancers/layerSelector.js b/web/client/plugins/widgetbuilder/enhancers/layerSelector.js new file mode 100644 index 0000000000..0b90d5bc1b --- /dev/null +++ b/web/client/plugins/widgetbuilder/enhancers/layerSelector.js @@ -0,0 +1,39 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const Rx = require('rxjs'); +const {compose, withState, mapPropsStream} = require('recompose'); +const { addSearch } = require('../../../observables/wms'); +const { recordToLayer } = require('../../../utils/CatalogUtils'); + +/** + * enhancer for CompactCatalog (or a container) to validate a selected record, + * convert it to layer and return as prop. Intercepts also validation errors, setting + * canProceed = false and error as props. + * TODO: this can become a more general validate enhancer + */ +module.exports = compose( + withState('selected', "setSelected", null), + withState('layer', "setLayer", null), + mapPropsStream(props$ => + props$.distinctUntilKeyChanged('selected').filter(({ selected } = {}) => selected) + .switchMap( + ({ selected, layerValidationStream = s => s, setLayer = () => { } } = {}) => + Rx.Observable.of(recordToLayer(selected)) + .let(layerValidationStream) + .switchMap(() => addSearch(recordToLayer(selected))) + .do(l => setLayer(l)) + .mapTo({ canProceed: true }) + .catch((error) => Rx.Observable.of({ error, canProceed: false })) + ).startWith({}) + .combineLatest(props$, ({ canProceed } = {}, props) => ({ + canProceed, + ...props + }) + ) + ) +); diff --git a/web/client/plugins/widgetbuilder/enhancers/manageLayers.js b/web/client/plugins/widgetbuilder/enhancers/manageLayers.js new file mode 100644 index 0000000000..8e4c5ddd14 --- /dev/null +++ b/web/client/plugins/widgetbuilder/enhancers/manageLayers.js @@ -0,0 +1,28 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const { compose, withProps, withHandlers} = require('recompose'); +const {connect} = require('react-redux'); +const {castArray, find} = require('lodash'); +const { normalizeLayer } = require('../../../utils/LayersUtils'); +const { onEditorChange } = require('../../../actions/widgets'); + +/** + * Gets the editor's data and allow to do basic operations on layers + */ +module.exports = compose( + withProps(({ editorData = {} }) => ({ + layers: editorData.map && editorData.map.layers + })), + connect(() => ({}), { + setLayers: layers => onEditorChange('map.layers', layers) + }), + withHandlers({ + addLayer: ({ layers = [], setLayers = () => { } }) => layer => setLayers([...layers, normalizeLayer(layer)]), + removeLayersById: ({ layers = [], setLayers = () => { } }) => (ids = []) => setLayers(layers.filter(l => !find(castArray(ids), id => id === l.id))) + }) +); diff --git a/web/client/plugins/widgetbuilder/enhancers/mapToolbar.js b/web/client/plugins/widgetbuilder/enhancers/mapToolbar.js new file mode 100644 index 0000000000..f61555d8af --- /dev/null +++ b/web/client/plugins/widgetbuilder/enhancers/mapToolbar.js @@ -0,0 +1,54 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const { compose, branch, withProps, withHandlers} = require('recompose'); +const {connect} = require('react-redux'); +const { insertWidget, setPage} = require('../../../actions/widgets'); +const manageLayers = require('./manageLayers'); +const handleNodeEditing = require('./handleNodeEditing'); +const { wizardSelector, wizardStateToProps } = require('../commons'); + +module.exports = compose( + connect(wizardSelector, { + setPage, + insertWidget + }, + wizardStateToProps + ), + manageLayers, + handleNodeEditing, + withHandlers({ + onRemoveSelected: ({selectedLayers = [], removeLayersById = () => { } }) => () => { + removeLayersById(selectedLayers); + } + }), + branch( + ({editNode}) => !!editNode, + withProps(({ selectedNodes = [], setEditNode = () => { } }) => ({ + buttons: [{ + visible: selectedNodes.length === 1, + tooltipId: "close", + glyph: "1-close", + onClick: () => setEditNode(false) + }] + })), + withProps(({ selectedNodes = [], onRemoveSelected = () => { }, setEditNode = () => { } }) => ({ + tocButtons: [{ + visible: selectedNodes.length === 1, + glyph: "wrench", + tooltipId: "toc.toolLayerSettingsTooltip", + onClick: () => setEditNode(selectedNodes[0]) + }, { + onClick: () => onRemoveSelected(), + visible: selectedNodes.length > 0, + glyph: "trash", + tooltipId: "toc.toolTrashLayerTooltip" + }] + })) + ) + +); diff --git a/web/client/reducers/config.js b/web/client/reducers/config.js index c8dae5b218..b431fa0c7c 100644 --- a/web/client/reducers/config.js +++ b/web/client/reducers/config.js @@ -18,7 +18,7 @@ function mapConfig(state = null, action) { case MAP_CONFIG_LOADED: let size = state && state.map && state.map.present && state.map.present.size || state && state.map && state.map.size; - let hasVersion = action.config.version && action.config.version >= 2; + let hasVersion = action.config && action.config.version >= 2; // we get from the configuration what will be used as the initial state let mapState = action.legacy && !hasVersion ? ConfigUtils.convertFromLegacy(action.config) : ConfigUtils.normalizeConfig(action.config.map); let newMapState = { diff --git a/web/client/translations/data.de-DE b/web/client/translations/data.de-DE index d1390a6d02..c873c1bf50 100644 --- a/web/client/translations/data.de-DE +++ b/web/client/translations/data.de-DE @@ -1122,7 +1122,11 @@ "updateWidget": "Aktualisieren Sie das Widget", "addToTheMap": "Hinzufügen des Widgets zur Karte", "titlePlaceholder": "Gib den Titel ein...", - "textPlaceholder": "Text eingeben..." + "textPlaceholder": "Text eingeben...", + "useThisMap": "Benutze diese Karte", + "configureMapOptions": "Kartenoptionen konfigurieren", + "addLayer": "Fügt der Karte eine Ebene hinzu", + "useTheSelectedLayer": "Verwende die ausgewählte Ebene" }, "errors": { "noWidgetsAvailableTitle": "Keine Widgets verfügbar", diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US index 317cc29d6b..165899c68a 100644 --- a/web/client/translations/data.en-US +++ b/web/client/translations/data.en-US @@ -1123,7 +1123,11 @@ "updateWidget": "Update the widget", "addToTheMap": "Add the widget to the map", "titlePlaceholder": "Insert title...", - "textPlaceholder": "Insert text..." + "textPlaceholder": "Insert text...", + "useThisMap": "Use this map", + "configureMapOptions": "Configure map options", + "addLayer": "Add a layer to the map", + "useTheSelectedLayer": "Use the selected layer" }, "errors": { "noWidgetsAvailableTitle": "No Widgets available", diff --git a/web/client/translations/data.es-ES b/web/client/translations/data.es-ES index 70ae1097d7..d666f7551b 100644 --- a/web/client/translations/data.es-ES +++ b/web/client/translations/data.es-ES @@ -1122,7 +1122,11 @@ "updateWidget": "Actualiza el widget", "addToTheMap": "Agrega el widget al mapa", "titlePlaceholder": "Ingrese el título...", - "textPlaceholder": "Ingresar texto..." + "textPlaceholder": "Ingresar texto...", + "useThisMap": "Usar este mapa", + "configureMapOptions": "Configurar opciones de mapa", + "addLayer": "Agregar una capa al mapa", + "useTheSelectedLayer": "Usar la capa seleccionada" }, "errors": { "noWidgetsAvailableTitle": "Sin widgets disponibles", diff --git a/web/client/translations/data.fr-FR b/web/client/translations/data.fr-FR index 2697ab8702..7493754a73 100644 --- a/web/client/translations/data.fr-FR +++ b/web/client/translations/data.fr-FR @@ -1123,7 +1123,11 @@ "updateWidget": "Mettre à jour le widget", "addToTheMap": "Ajouter le widget à la carte", "titlePlaceholder": "Entrez le titre...", - "textPlaceholder": "Entrez le texte..." + "textPlaceholder": "Entrez le texte...", + "useThisMap": "Utiliser cette carte", + "configureMapOptions": "Configurer les options de la carte", + "addLayer": "Ajouter une couche à la carte", + "useTheSelectedLayer": "Utiliser le calque sélectionné" }, "errors": { "noWidgetsAvailableTitle": "Aucun widget disponible", diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT index b2d36be088..2f4b7ee70d 100644 --- a/web/client/translations/data.it-IT +++ b/web/client/translations/data.it-IT @@ -1122,7 +1122,11 @@ "updateWidget": "Aggiorna il widget", "addToTheMap": "Aggiungi il widget alla mappa", "titlePlaceholder": "Inserisci il titolo...", - "textPlaceholder": "Inserisci il testo..." + "textPlaceholder": "Inserisci il testo...", + "useThisMap": "Usa questa mappa", + "configureMapOptions": "Configura opzioni mappa", + "addLayer": "Aggiungi un livello alla mappa", + "useTheSelectedLayer": "Usa il livello selezionato" }, "errors": { "noWidgetsAvailableTitle": "Nessun widgets disponibile", diff --git a/web/client/utils/LayersUtils.js b/web/client/utils/LayersUtils.js index 8e2cfd99e5..d7124b4398 100644 --- a/web/client/utils/LayersUtils.js +++ b/web/client/utils/LayersUtils.js @@ -192,9 +192,40 @@ const LayersUtils = { return null; } }, + /** + * Returns an id for the layer. If the layer has layer.id returns it, otherwise it will return a generated id. + * If the layer doesn't have any layer and if the 2nd argument is passed (it should be an array), + * the layer id will returned will be something like `layerName__2` when 2 is the layer size (for retro compatibility, it should be removed in the future). + * Otherwise a random string will be appended to the layer name. + * @param {object} layer the layer + * @param {array} [layers] an array to use to generate the id @deprecated + * @returns {string} the id of the layer, or a generated one + */ getLayerId: (layerObj, layers) => { - return layerObj && layerObj.id || layerObj.name + "__" + layers.length; + return layerObj && layerObj.id || layerObj.name + "__" + (layers ? layers.length : Math.random().toString(36).substring(2, 15)); }, + /** + * Normalizes the layer to assign missing Ids + * @param {object} layer the layer to normalize + * @returns {object} the normalized layer + */ + normalizeLayer: (layer) => layer.id ? layer : { ...layer, id: LayersUtils.getLayerId(layer) }, + /** + * Normalizes the map adding missing ids, default groups. + * @param {object} map the map + * @param {object} the normalized map + */ + normalizeMap: (rawMap = {}) => + [ + (map) => (map.layers || []).filter(({ id } = {}) => !id).length > 0 ? {...map, layers: (map.layers || []).map(l => LayersUtils.normalizeLayer(l))} : map, + (map) => map.groups ? map : {...map, groups: {id: "Default", expanded: true}} + // this is basically a compose + ].reduce((f, g) => (...args) => f(g(...args)))(rawMap), + /** + * @param gid + * @return function that filter by group + */ + belongsToGroup: (gid) => l => (l.group || "Default") === gid || (l.group || "").indexOf(`${gid}.`) === 0, getLayersByGroup: (configLayers) => { let i = 0; let mapLayers = configLayers.map((layer) => assign({}, layer, {storeIndex: i++}));