From d095d2fda7594cf36124c3917faa8e3bce7e6c7a Mon Sep 17 00:00:00 2001 From: mahmoudadel54 Date: Mon, 13 Jan 2025 21:44:13 +0200 Subject: [PATCH 1/5] #10736: Interactive legend for TOC layers [WFS Layer part] Description: - handle the functionality of interactive legend for WFS layers - create geostyler converter to cql - handle saving the legend filter of layer into layerFilter object in saving - handle showing incompatible message in case change into filtered style - add unit tests - add translations --- web/client/api/catalog/WFS.js | 10 +- .../TOC/fragments/settings/Display.jsx | 37 ++++ .../settings/__tests__/Display-test.jsx | 41 +++- .../CommonAdvancedSettings.jsx | 50 +++-- .../__tests__/CommonAdvancedSettings-test.js | 12 + .../plugins/TOC/components/DefaultLayer.jsx | 3 + .../plugins/TOC/components/VectorLegend.jsx | 69 +++++- .../__tests__/VectorLegend-test.jsx | 207 ++++++++++++++++++ web/client/themes/default/less/toc.less | 6 +- web/client/translations/data.de-DE.json | 1 + web/client/translations/data.en-US.json | 1 + web/client/translations/data.es-ES.json | 1 + web/client/translations/data.fr-FR.json | 1 + web/client/translations/data.it-IT.json | 1 + web/client/utils/FilterUtils.js | 73 +++++- web/client/utils/LegendUtils.js | 1 - .../utils/__tests__/FilterUtils-test.js | 110 +++++++++- .../converters/__tests__/geostyler-test.js | 54 +++++ .../filter/converters/__tests__/index-test.js | 1 + .../utils/filter/converters/geostyler.js | 56 +++++ web/client/utils/filter/converters/index.js | 22 ++ 21 files changed, 719 insertions(+), 38 deletions(-) create mode 100644 web/client/utils/filter/converters/__tests__/geostyler-test.js create mode 100644 web/client/utils/filter/converters/geostyler.js diff --git a/web/client/api/catalog/WFS.js b/web/client/api/catalog/WFS.js index f436422cc0..46335fece4 100644 --- a/web/client/api/catalog/WFS.js +++ b/web/client/api/catalog/WFS.js @@ -70,7 +70,12 @@ const searchAndPaginate = (json = {}, startPosition, maxRecords, text) => { }; }; -const recordToLayer = (record) => { +const recordToLayer = (record, { + service +}) => { + const { + layerOptions + } = service || {}; return { type: record.type || "wfs", search: { @@ -85,7 +90,8 @@ const recordToLayer = (record) => { description: record.description || "", bbox: record.boundingBox, links: getRecordLinks(record), - ...record.layerOptions + ...record.layerOptions, + ...layerOptions }; }; diff --git a/web/client/components/TOC/fragments/settings/Display.jsx b/web/client/components/TOC/fragments/settings/Display.jsx index 93eb288ce5..a7a52a6904 100644 --- a/web/client/components/TOC/fragments/settings/Display.jsx +++ b/web/client/components/TOC/fragments/settings/Display.jsx @@ -30,6 +30,7 @@ import ThreeDTilesSettings from './ThreeDTilesSettings'; import ModelTransformation from './ModelTransformation'; import StyleBasedWMSJsonLegend from '../../../../plugins/TOC/components/StyleBasedWMSJsonLegend'; import { getMiscSetting } from '../../../../utils/ConfigUtils'; +import VectorLegend from '../../../../plugins/TOC/components/VectorLegend'; export default class extends React.Component { static propTypes = { @@ -351,6 +352,42 @@ export default class extends React.Component { } + {this.props.element.type === "wfs" && +
+ + + + { experimentalInteractiveLegend && this.props.element?.serverType !== ServerTypes.NO_VENDOR && !this.props?.hideInteractiveLegendOption && + + { + if (!e.target.checked) { + const newLayerFilter = updateLayerLegendFilter(this.props.element.layerFilter); + this.props.onChange("layerFilter", newLayerFilter ); + } + this.props.onChange("enableInteractiveLegend", e.target.checked); + }} + checked={enableInteractiveLegend} > + +  } /> + + + } + {enableInteractiveLegend && + +
+ +
+ } +
+
} ); } diff --git a/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx b/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx index 2ae82acbaf..119f84a315 100644 --- a/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx +++ b/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx @@ -260,7 +260,7 @@ describe('test Layer Properties Display module component', () => { expect(labels[6].innerText).toBe("layerProperties.legendOptions.legendHeight"); expect(labels[7].innerText).toBe("layerProperties.legendOptions.legendPreview"); }); - it('tests Layer Properties Legend component events', () => { + it('tests wms Layer Properties Legend component events', () => { const l = { name: 'layer00', title: 'Layer', @@ -369,4 +369,43 @@ describe('test Layer Properties Display module component', () => { expect(inputs[11].value).toBe("20"); expect(inputs[12].value).toBe("40"); }); + it('tests wfs Layer Properties Legend component events', () => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wfs', + url: 'fakeurl', + legendOptions: { + legendWidth: 15, + legendHeight: 15 + }, + enableInteractiveLegend: false + }; + const settings = { + options: { + opacity: 1 + } + }; + const handlers = { + onChange() {} + }; + let spy = expect.spyOn(handlers, "onChange"); + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toBeTruthy(); + const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" ); + const legendPreview = ReactTestUtils.scryRenderedDOMComponentsWithClass( comp, "legend-preview" ); + expect(legendPreview).toBeTruthy(); + expect(inputs).toBeTruthy(); + expect(inputs.length).toBe(6); + let interactiveLegendConfig = document.querySelector(".legend-options input[data-qa='display-interactive-legend-option']"); + // change enableInteractiveLegend to enable interactive legend + interactiveLegendConfig.checked = true; + ReactTestUtils.Simulate.change(interactiveLegendConfig); + expect(spy).toHaveBeenCalled(); + expect(spy.calls[0].arguments[0]).toEqual("enableInteractiveLegend"); + expect(spy.calls[0].arguments[1]).toEqual(true); + }); + }); diff --git a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx index 147e4127eb..4028756757 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx +++ b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx @@ -11,6 +11,7 @@ import { FormGroup, Checkbox } from "react-bootstrap"; import Message from "../../../I18N/Message"; import InfoPopover from '../../../widgets/widget/InfoPopover'; +import { getMiscSetting } from '../../../../utils/ConfigUtils'; /** * Common Advanced settings form WMS/CSW/WMTS/WFS @@ -24,23 +25,25 @@ export default ({ service, onChangeServiceProperty = () => { }, onToggleThumbnail = () => { } -}) => ( - <> - - {service.autoload !== undefined && onChangeServiceProperty("autoload", e.target.checked)} - checked={!isNil(service.autoload) ? service.autoload : false}> - - } - - - onToggleThumbnail()} - checked={!isNil(service.hideThumbnail) ? !service.hideThumbnail : true}> - - - +}) => { + const experimentalInteractiveLegend = getMiscSetting('experimentalInteractiveLegend', false); + return ( + <> + + {service.autoload !== undefined && onChangeServiceProperty("autoload", e.target.checked)} + checked={!isNil(service.autoload) ? service.autoload : false}> + + } + + + onToggleThumbnail()} + checked={!isNil(service.hideThumbnail) ? !service.hideThumbnail : true}> + + + - {!isNil(service.type) && service.type === "cog" && + {!isNil(service.type) && service.type === "cog" && onChangeServiceProperty("fetchMetadata", e.target.checked)} @@ -48,6 +51,15 @@ export default ({  } /> } - {children} - -); + {experimentalInteractiveLegend && ['wfs'].includes(service.type) && + onChangeServiceProperty("layerOptions", { ...service.layerOptions, enableInteractiveLegend: e.target.checked})} + checked={!isNil(service.layerOptions?.enableInteractiveLegend) ? service.layerOptions?.enableInteractiveLegend : false}> + +  } /> + + } + {children} + + ); +}; diff --git a/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js index 04a3eb57a2..51994fb885 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js +++ b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js @@ -10,10 +10,12 @@ import ReactDOM from "react-dom"; import CommonAdvancedSettings from "../CommonAdvancedSettings"; import expect from "expect"; import TestUtils from "react-dom/test-utils"; +import { setConfigProp } from '../../../../../utils/ConfigUtils'; describe('Test common advanced settings', () => { beforeEach((done) => { document.body.innerHTML = '
'; + setConfigProp('miscSettings', { experimentalInteractiveLegend: true }); setTimeout(done); }); afterEach((done) => { @@ -85,4 +87,14 @@ describe('Test common advanced settings', () => { expect(spyOn).toHaveBeenCalled(); expect(spyOn.calls[1].arguments).toEqual([ 'fetchMetadata', false ]); }); + it('test showing/hiding interactive legend checkbox', () => { + ReactDOM.render(, document.getElementById("container")); + const interactiveLegendCheckboxInput = document.querySelector(".wfs-interactive-legend .checkbox input[data-qa='display-interactive-legend-option']"); + expect(interactiveLegendCheckboxInput).toBeTruthy(); + const interactiveLegendLabel = document.querySelector(".wfs-interactive-legend .checkbox span"); + expect(interactiveLegendLabel).toBeTruthy(); + expect(interactiveLegendLabel.innerHTML).toEqual('layerProperties.enableInteractiveLegendInfo.label'); + }); }); diff --git a/web/client/plugins/TOC/components/DefaultLayer.jsx b/web/client/plugins/TOC/components/DefaultLayer.jsx index aa04502634..826d09b659 100644 --- a/web/client/plugins/TOC/components/DefaultLayer.jsx +++ b/web/client/plugins/TOC/components/DefaultLayer.jsx @@ -93,6 +93,9 @@ const DefaultLayerNode = ({
  • diff --git a/web/client/plugins/TOC/components/VectorLegend.jsx b/web/client/plugins/TOC/components/VectorLegend.jsx index 2068865fe5..d9c52914e8 100644 --- a/web/client/plugins/TOC/components/VectorLegend.jsx +++ b/web/client/plugins/TOC/components/VectorLegend.jsx @@ -7,20 +7,68 @@ */ import React from 'react'; import PropTypes from 'prop-types'; +import get from 'lodash/get'; +import isEmpty from 'lodash/isEmpty'; +import { Alert } from 'react-bootstrap'; + +import Message from '../../../components/I18N/Message'; +import { ButtonWithTooltip } from '../../../components/misc/Button'; import RuleLegendIcon from '../../../components/styleeditor/RuleLegendIcon'; +import { INTERACTIVE_LEGEND_ID } from '../../../utils/LegendUtils'; +import { updateLayerWFSVectorLegendFilter } from '../../../utils/FilterUtils'; + /** * VectorLegend renders the legend given a valid vector style * @prop {object} style a layer style object in geostyler format + * @prop {object} layer the vector layer object + * @prop {string} owner the owner of the compoenent + * @prop {function} onChange the onChange layer handler */ -function VectorLegend({ style }) { - +function VectorLegend({ style, layer, owner, onChange }) { + const onResetLegendFilter = () => { + const newLayerFilter = updateLayerWFSVectorLegendFilter(layer?.layerFilter); + onChange({ layerFilter: newLayerFilter }); + }; + const filterLayerHandler = (filter) => { + const isFilterDisabled = layer?.layerFilter?.disabled; + if (!filter || isFilterDisabled) return; + const newLayerFilter = updateLayerWFSVectorLegendFilter(layer?.layerFilter, filter); + onChange({ layerFilter: newLayerFilter }); + }; + const checkPreviousFiltersAreValid = (rules, prevLegendFilters) => { + const rulesFilters = rules.map(rule => rule?.filter?.toString()); + return prevLegendFilters?.every(f => rulesFilters.includes(f.id)); + }; const renderRules = (rules) => { - return (rules || []).map((rule) => { - return (
    - - {rule.name || ''} -
    ); - }); + const layerFilter = get(layer, 'layerFilter', {}); + const interactiveLegendFilters = get(layerFilter, 'filters', []).find(f => f.id === INTERACTIVE_LEGEND_ID); + const legendFilters = get(interactiveLegendFilters, 'filters', []); + const showResetWarning = !checkPreviousFiltersAreValid(rules, legendFilters) && !layerFilter.disabled; + + return (<> + {showResetWarning && owner !== 'legendPreview' ? +
    + + + +
    : null} + {isEmpty(rules) + ? + : (rules || []).map((rule, idx) => { + const isFilterDisabled = layer?.layerFilter?.disabled; + const activeFilter = legendFilters?.some(f => f.id === rule?.filter?.toString()); + return (
    filterLayerHandler(rule.filter)} + className={`ms-legend-rule ${isFilterDisabled || owner === 'legendPreview' || !rule?.filter ? "" : "filter-enabled "} ${activeFilter ? 'active' : ''}`}> + + {rule.name || ''} +
    ); + })} + ); }; return <> @@ -33,7 +81,10 @@ function VectorLegend({ style }) { } VectorLegend.propTypes = { - style: PropTypes.object + style: PropTypes.object, + layer: PropTypes.object, + owner: PropTypes.string, + onChange: PropTypes.func }; export default VectorLegend; diff --git a/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx b/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx index 118cff9406..de6de83bac 100644 --- a/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx +++ b/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx @@ -10,7 +10,145 @@ import React from 'react'; import ReactDOM from 'react-dom'; import expect from 'expect'; import VectorLegend from '../VectorLegend'; +import { INTERACTIVE_LEGEND_ID } from '../../../../utils/LegendUtils'; +const rules = [ + { + "name": ">= 0 and < 0.6", + "filter": [ + "&&", + [ + ">=", + "priority", + 0 + ], + [ + "<", + "priority", + 0.6 + ] + ], + "symbolizers": [ + { + "kind": "Fill", + "color": "#fff7ec", + "fillOpacity": 1, + "outlineColor": "#777777", + "outlineWidth": 1, + "msClassificationType": "both", + "msClampToGround": true + } + ] + }, + { + "name": ">= 0.6 and < 1.2", + "filter": [ + "&&", + [ + ">=", + "priority", + 0.6 + ], + [ + "<", + "priority", + 1.2 + ] + ], + "symbolizers": [ + { + "kind": "Fill", + "color": "#fdd49e", + "fillOpacity": 1, + "outlineColor": "#777777", + "outlineWidth": 1, + "msClassificationType": "both", + "msClampToGround": true + } + ] + }, + { + "name": ">= 1.2 and < 1.7999999999999998", + "filter": [ + "&&", + [ + ">=", + "priority", + 1.2 + ], + [ + "<", + "priority", + 1.7999999999999998 + ] + ], + "symbolizers": [ + { + "kind": "Fill", + "color": "#fc8d59", + "fillOpacity": 1, + "outlineColor": "#777777", + "outlineWidth": 1, + "msClassificationType": "both", + "msClampToGround": true + } + ] + }, + { + "name": ">= 1.7999999999999998 and < 2.4", + "filter": [ + "&&", + [ + ">=", + "priority", + 1.7999999999999998 + ], + [ + "<", + "priority", + 2.4 + ] + ], + "symbolizers": [ + { + "kind": "Fill", + "color": "#d7301f", + "fillOpacity": 1, + "outlineColor": "#777777", + "outlineWidth": 1, + "msClassificationType": "both", + "msClampToGround": true + } + ] + }, + { + "name": ">= 2.4 and <= 3", + "filter": [ + "&&", + [ + ">=", + "priority", + 2.4 + ], + [ + "<=", + "priority", + 3 + ] + ], + "symbolizers": [ + { + "kind": "Fill", + "color": "#7f0000", + "fillOpacity": 1, + "outlineColor": "#777777", + "outlineWidth": 1, + "msClassificationType": "both", + "msClampToGround": true + } + ] + } +]; describe('VectorLegend module component', () => { beforeEach((done) => { document.body.innerHTML = '
    '; @@ -314,4 +452,73 @@ describe('VectorLegend module component', () => { const textElement = ruleElements[0].getElementsByTagName('span'); expect(textElement[0].innerHTML).toBe(''); }); + it('tests legend with empty rules', async() => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wfs', + url: 'http://localhost:8080/geoserver1/wfs', + style: {format: 'geostyler', body: {rules: []}} + }; + + ReactDOM.render(, document.getElementById("container")); + const legendElem = document.querySelector('.ms-legend'); + expect(legendElem).toBeTruthy(); + expect(legendElem.innerText).toBe('layerProperties.interactiveLegend.noLegendData'); + }); + it('tests legend with incompatible filter rules', async() => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wfs', + url: 'http://localhost:8080/geoserver2/wfs', + layerFilter: { + filters: [{ + id: INTERACTIVE_LEGEND_ID, + filters: [{ + id: 'filter1' + }] + }], + disabled: false + } + }; + ReactDOM.render(, document.getElementById("container")); + const legendElem = document.querySelector('.ms-legend'); + expect(legendElem).toBeTruthy(); + const legendRuleElem = document.querySelector('.ms-legend .alert-warning'); + expect(legendRuleElem).toBeTruthy(); + expect(legendRuleElem.innerText).toContain('layerProperties.interactiveLegend.incompatibleWFSFilterWarning'); + const resetLegendFilter = document.querySelector('.ms-legend .alert-warning button'); + expect(resetLegendFilter).toBeTruthy(); + }); + it('tests hide warning when layer filter is disabled', async() => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wfs', + url: 'http://localhost:8080/geoserver3/wfs', + layerFilter: { + filters: [{ + id: INTERACTIVE_LEGEND_ID, + filters: [{ + id: 'filter1' + }] + }], + disabled: true + } + }; + + ReactDOM.render(, document.getElementById("container")); + const legendElem = document.querySelector('.ms-legend'); + expect(legendElem).toBeTruthy(); + const legendRuleElem = document.querySelector('.ms-legend .alert-warning'); + expect(legendRuleElem).toBeFalsy(); + }); }); diff --git a/web/client/themes/default/less/toc.less b/web/client/themes/default/less/toc.less index 74c2487c92..31b386c78d 100644 --- a/web/client/themes/default/less/toc.less +++ b/web/client/themes/default/less/toc.less @@ -88,10 +88,10 @@ .background-color-var(@theme-vars[selected-bg]); .outline-color-var(@theme-vars[focus-color]); } - .wms-json-legend-rule.filter-enabled:hover { + .ms-legend-rule.filter-enabled:hover,.wms-json-legend-rule.filter-enabled:hover { .background-color-var(@theme-vars[selected-hover-bg]); } - .wms-json-legend-rule.filter-enabled.active { + .ms-legend-rule.filter-enabled.active, .wms-json-legend-rule.filter-enabled.active { .background-color-var(@theme-vars[selected-bg]); } } @@ -426,6 +426,6 @@ align-items: normal; } } -.wms-json-legend-rule.filter-enabled:hover { +.ms-legend-rule.filter-enabled:hover, .wms-json-legend-rule.filter-enabled:hover { cursor: pointer; } \ No newline at end of file diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index df6b185b0e..ded105d728 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -243,6 +243,7 @@ "disableFeaturesEditing": "Deaktivieren Sie die Bearbeitung der Attributtabelle", "interactiveLegend": { "incompatibleFilterWarning": "Legendenfilter sind mit dem aktiven Ebenenfilter nicht kompatibel oder es sind keine sichtbaren Features im Kartenansichtsfenster vorhanden. Klicken Sie auf Zurücksetzen, um die Legendenfilter zu löschen", + "incompatibleWFSFilterWarning": "Legendenfilter sind mit dem aktiven Ebenenfilter nicht kompatibel. Klicken Sie auf Zurücksetzen, um die Legendenfilter zu löschen", "resetLegendFilter": "Zurücksetzen", "noLegendData": "Keine Legenden Elemente zum Anzeigen" } diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index d0d7e09dd9..3707833e43 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -243,6 +243,7 @@ "disableFeaturesEditing": "Disable editing on Attribute table", "interactiveLegend": { "incompatibleFilterWarning": "Legend filters are incompatible with the active layer filter, or no visible features are within the map view. Click reset to clear legend filters", + "incompatibleWFSFilterWarning": "Legend filters are incompatible with the active layer filter. Click reset to clear legend filters", "resetLegendFilter": "Reset", "noLegendData": "No legend items to show" } diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 05ee0381eb..f032dcc699 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -243,6 +243,7 @@ "disableFeaturesEditing": "Deshabilitar la edición en la tabla de atributos", "interactiveLegend": { "incompatibleFilterWarning": "Los filtros de leyenda son incompatibles con el filtro de capa activo, o no hay características visibles dentro de la vista del mapa. Haga clic en restablecer para borrar los filtros de leyenda", + "incompatibleWFSFilterWarning": "Los filtros de leyenda son incompatibles con el filtro de capa activo. Haga clic en restablecer para borrar los filtros de leyenda", "resetLegendFilter": "Restablecer", "noLegendData": "No hay elementos de leyenda para mostrar" } diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 61c8d8f522..6b2919b7d4 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -243,6 +243,7 @@ "disableFeaturesEditing": "Désactiver la modification sur la table attributaire", "interactiveLegend": { "incompatibleFilterWarning": "Les filtres de légende sont incompatibles avec le filtre de couche actif, ou aucune fonctionnalité visible n'est dans la vue de la carte. Cliquez sur réinitialiser pour effacer les filtres de légende", + "incompatibleWFSFilterWarning": "Les filtres de légende sont incompatibles avec le filtre de couche actif. Cliquez sur réinitialiser pour effacer les filtres de légende", "resetLegendFilter": "Réinitialiser", "noLegendData": "Aucun élément de légende à afficher" } diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 9c2e32872f..2efbfedf61 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -243,6 +243,7 @@ "disableFeaturesEditing": "Disabilita la modifica sulla tabella degli attributi", "interactiveLegend": { "incompatibleFilterWarning": "I filtri della legenda sono incompatibili con il filtro del layer attivo, oppure non ci sono feature visibili all'interno della vista della mappa. Clicca su reset per cancellare i filtri della legenda", "resetLegendFilter": "Reset", + "incompatibleWFSFilterWarning": "I filtri della legenda sono incompatibili con il filtro del layer attivo. Clicca su reset per cancellare i filtri della legenda", "resetLegendFilter": "Reset", "noLegendData": "Nessun elemento della legenda da mostrare" } }, "localizedInput": { diff --git a/web/client/utils/FilterUtils.js b/web/client/utils/FilterUtils.js index ce9297b606..e08dbd5ea4 100644 --- a/web/client/utils/FilterUtils.js +++ b/web/client/utils/FilterUtils.js @@ -1369,6 +1369,76 @@ export const updateLayerLegendFilter = (layerFilterObj, legendFilter) => { return newFilter; }; +/** + * Merges legend geostyler filter, with mapstore filter objects to filters + * @param {object} layerFilterObj previous layer filter object includes all filters + * @param {array} legendGeostylerFilter geostyler filter + * @return {object} layerFilterObj updated the layer filter object + */ +export const updateLayerWFSVectorLegendFilter = (layerFilterObj, legendGeostylerFilter) => { + const defaultLayerFilter = { + groupFields: [ + { + id: 1, + logic: 'OR', + index: 0 + } + ], + filterFields: [], + attributePanelExpanded: true, + spatialPanelExpanded: true, + crossLayerExpanded: true, + crossLayerFilter: { + attribute: 'the_geom' + }, + spatialField: { + method: null, + operation: 'INTERSECTS', + geometry: null, + attribute: 'the_geom' + } + }; + let filterObj = {...defaultLayerFilter, ...layerFilterObj}; + const isLegendFilterExist = filterObj?.filters?.find(f => f.id === INTERACTIVE_LEGEND_ID); + if (!legendGeostylerFilter) { + // clear legend filter with id = 'interactiveLegend' + if (isLegendFilterExist) { + filterObj = { + ...filterObj, filters: filterObj?.filters?.filter(f => f.id !== INTERACTIVE_LEGEND_ID) + }; + } + let newFilter = filterObj ? filterObj : undefined; + return newFilter; + } + let interactiveLegendFilters = isLegendFilterExist ? isLegendFilterExist.filters || [] : []; + + const isSelectedFilterExist = interactiveLegendFilters.find(legFilter => legFilter.body?.toString() === legendGeostylerFilter?.toString()); + if (isSelectedFilterExist) { + interactiveLegendFilters = interactiveLegendFilters.filter(legFilter => legFilter.body?.toString() !== legendGeostylerFilter?.toString()); + } else { + interactiveLegendFilters = [...interactiveLegendFilters, { + "format": "geostyler", + "version": "1.0.0", + "body": legendGeostylerFilter, + "id": `${legendGeostylerFilter?.toString()}` + }]; + } + let newFilter = { + ...(filterObj || {}), filters: [ + ...(filterObj?.filters?.filter(f => f.id !== INTERACTIVE_LEGEND_ID) || []), ...[ + { + "id": INTERACTIVE_LEGEND_ID, + "format": "logic", + "version": "1.0.0", + "logic": "OR", + "filters": [...interactiveLegendFilters] + } + ] + ] + }; + return newFilter; +}; + export function resetLayerLegendFilter(layer, reason, value) { const isResetForStyle = reason === 'style'; // here the reason for reset is change 'style' or change the enable/disable interactive legend config 'disableEnableInteractiveLegend' let needReset = false; @@ -1415,6 +1485,5 @@ FilterUtils = { processOGCSpatialFilter, createFeatureFilter, mergeFiltersToOGC, - convertFiltersToOGC, - INTERACTIVE_LEGEND_ID + convertFiltersToOGC }; diff --git a/web/client/utils/LegendUtils.js b/web/client/utils/LegendUtils.js index 18405b938e..0ccd4169a8 100644 --- a/web/client/utils/LegendUtils.js +++ b/web/client/utils/LegendUtils.js @@ -115,7 +115,6 @@ export const updateLayerWithLegendFilters = (layers, dependencies) => { }; export default { - INTERACTIVE_LEGEND_ID, getLayerFilterByLegendFormat, getWMSLegendConfig, updateLayerWithLegendFilters diff --git a/web/client/utils/__tests__/FilterUtils-test.js b/web/client/utils/__tests__/FilterUtils-test.js index 7433c3dae2..019bb0d779 100644 --- a/web/client/utils/__tests__/FilterUtils-test.js +++ b/web/client/utils/__tests__/FilterUtils-test.js @@ -30,7 +30,9 @@ import { convertFiltersToOGC, convertFiltersToCQL, isFilterEmpty, - updateLayerLegendFilter, resetLayerLegendFilter + updateLayerLegendFilter, + resetLayerLegendFilter, + updateLayerWFSVectorLegendFilter } from '../FilterUtils'; import { INTERACTIVE_LEGEND_ID } from '../LegendUtils'; @@ -2332,6 +2334,7 @@ describe('FilterUtils', () => { })).toBe(false); }); + // for wms it('test updateLayerLegendFilter for wms, simple filter', () => { const layerFilterObj = {}; const lgegendFilter = "[FIELD1 = 'Value' AND FIELD2 > '1256']"; @@ -2492,4 +2495,109 @@ describe('FilterUtils', () => { expect(updatedFilterObj.filters.length).toEqual(0); expect(updatedFilterObj.filters.find(i => i.id === INTERACTIVE_LEGEND_ID)).toBeFalsy(); }); + // for WFS + it('test updateLayerWFSVectorLegendFilter for wfs, simple filter', () => { + const layerFilterObj = {}; + const lgegendFilter = [ + "&&", + ["==", "FIELD1", 'Value'], + ["==", "FIELD2", '1256'] + ]; + const updatedFilterObj = updateLayerWFSVectorLegendFilter(layerFilterObj, lgegendFilter); + expect(updatedFilterObj).toBeTruthy(); + expect(updatedFilterObj.filters.length).toEqual(1); + expect(updatedFilterObj.filters.filter(i => i.id === INTERACTIVE_LEGEND_ID)?.length).toEqual(1); + expect(updatedFilterObj.filters[0].filters[0].format).toEqual("geostyler"); + expect(updatedFilterObj.filters.find(i => i.id === INTERACTIVE_LEGEND_ID).filters.length).toEqual(1); + }); + it('test updateLayerWFSVectorLegendFilter for wfs, apply multi legend filter', () => { + const layerFilterObj = { + "groupFields": [ + { + "id": 1, + "logic": "OR", + "index": 0 + } + ], + "filterFields": [], + "attributePanelExpanded": true, + "spatialPanelExpanded": true, + "crossLayerExpanded": true, + "crossLayerFilter": { + "attribute": "the_geom" + }, + "spatialField": { + "method": null, + "operation": "INTERSECTS", + "geometry": null, + "attribute": "the_geom" + }, + "filters": [ + { + "id": INTERACTIVE_LEGEND_ID, + "format": "logic", + "version": "1.0.0", + "logic": "OR", + "filters": [ + { + "format": "geostyler", + "version": "1.0.0", + "body": ["&&", ['>=', 'FIELD_01', '2500'], ['<', 'FIELD_01', '7000']], + "id": "&&,>=,FIELD_01,2500,<,FIELD_01,7000" + } + ] + } + ] + }; + const lgegendFilter = ["&&", ['>=', 'FIELD_01', '13000'], ['<', 'FIELD_01', '14500']]; + const updatedFilterObj = updateLayerLegendFilter(layerFilterObj, lgegendFilter); + expect(updatedFilterObj).toBeTruthy(); + expect(updatedFilterObj.filters.length).toEqual(1); + expect(updatedFilterObj.filters.filter(i => i.id === INTERACTIVE_LEGEND_ID)?.length).toEqual(1); + expect(updatedFilterObj.filters.find(i => i.id === INTERACTIVE_LEGEND_ID).filters.length).toEqual(2); + }); + it('test reset legend filter using updateLayerWFSVectorLegendFilter', () => { + const layerFilterObj = { + "groupFields": [ + { + "id": 1, + "logic": "OR", + "index": 0 + } + ], + "filterFields": [], + "attributePanelExpanded": true, + "spatialPanelExpanded": true, + "crossLayerExpanded": true, + "crossLayerFilter": { + "attribute": "the_geom" + }, + "spatialField": { + "method": null, + "operation": "INTERSECTS", + "geometry": null, + "attribute": "the_geom" + }, + "filters": [ + { + "id": INTERACTIVE_LEGEND_ID, + "format": "logic", + "version": "1.0.0", + "logic": "OR", + "filters": [ + { + "format": "geostyler", + "version": "1.0.0", + "body": ["&&", ['>=', 'FIELD_01', '2500'], ['<', 'FIELD_01', '7000']], + "id": "&&,>=,FIELD_01,2500,<,FIELD_01,7000" + } + ] + } + ] + }; + const updatedFilterObj = updateLayerWFSVectorLegendFilter(layerFilterObj); + expect(updatedFilterObj).toBeTruthy(); + expect(updatedFilterObj.filters.length).toEqual(0); + expect(updatedFilterObj.filters.find(i => i.id === INTERACTIVE_LEGEND_ID)).toBeFalsy(); + }); }); diff --git a/web/client/utils/filter/converters/__tests__/geostyler-test.js b/web/client/utils/filter/converters/__tests__/geostyler-test.js new file mode 100644 index 0000000000..9b9e18069e --- /dev/null +++ b/web/client/utils/filter/converters/__tests__/geostyler-test.js @@ -0,0 +1,54 @@ +/* + * Copyright 2025, 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. + */ +import expect from 'expect'; +import geostyler from '../geostyler'; +describe('GeoStyler converter', () => { + const SAMPLES = [ + // logic operators + { + cql: '(prop1 = 1 AND prop2 = 2)', + geostyler: { + body: ['&&', ['==', 'prop1', 1], ['==', 'prop2', 2]] + } + }, + { + cql: '(prop1 = 1 OR prop2 = 2)', + geostyler: {body: ['||', ['==', 'prop1', 1], ['==', 'prop2', 2]]} + }, + { + cql: '(prop1 = 1 AND (prop2 = 2 OR prop3 = 3))', + geostyler: {body: ['&&', ['==', 'prop1', 1], ['||', ['==', 'prop2', 2], ['==', 'prop3', 3]]]} + }, + { + cql: '(prop1 = 1 OR (prop2 = 2 AND prop3 = 3))', + geostyler: {body: ['||', ['==', 'prop1', 1], ['&&', ['==', 'prop2', 2], ['==', 'prop3', 3]]]} + }, + // comparison operators + { + cql: 'prop1 = 1', + geostyler: {body: ['==', 'prop1', 1]} + }, + { + cql: 'prop1 <> 1', + geostyler: {body: ['!=', 'prop1', 1]} + }, + { + cql: 'prop1 < 1', + geostyler: {body: ['<', 'prop1', 1]} + }, + { + cql: 'prop1 <= 1', + geostyler: {body: ['<=', 'prop1', 1]} + } + ]; + it('test geostyler to cql', () => { + SAMPLES.forEach((sample) => { + expect(geostyler.cql(sample.geostyler)).toBe(sample.cql); + }); + }); +}); diff --git a/web/client/utils/filter/converters/__tests__/index-test.js b/web/client/utils/filter/converters/__tests__/index-test.js index 68674f52c4..9ed7f208bb 100644 --- a/web/client/utils/filter/converters/__tests__/index-test.js +++ b/web/client/utils/filter/converters/__tests__/index-test.js @@ -15,6 +15,7 @@ describe('Filter converters', () => { expect(canConvert('logic', 'cql')).toBe(true, "logic to cql conversion not allowed"); expect(canConvert('ogc', 'logic')).toBe(false, "ogc to logic conversion allowed, but it shouldn't"); expect(canConvert('cql', 'logic')).toBe(false, "cql to logic conversion allowed, but it shouldn't"); + expect(canConvert('geostyler', 'cql')).toBe(true, "geostyler to cql conversion allowed"); }); const SAMPLES = [ { diff --git a/web/client/utils/filter/converters/geostyler.js b/web/client/utils/filter/converters/geostyler.js new file mode 100644 index 0000000000..8fb74347c0 --- /dev/null +++ b/web/client/utils/filter/converters/geostyler.js @@ -0,0 +1,56 @@ +/* + * Copyright 2025, 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 operatorMap = { + '==': '=', + '!=': '<>', + '>': '>', + '<': '<', + '>=': '>=', + '<=': '<=', + '&&': 'AND', + '||': 'OR' +}; + +/** + * Converts a Geostyler filter to CQL filter + * @param {object|string} geostylerFilter geostyler filter rules array + * @returns {string} CQL filter + */ + +export const cql = (geostylerFilter) => { + function parseCondition(filter) { + if (Array.isArray(filter)) { + let [operator, ...filterRules] = filter; + if (operatorMap[operator]) { + if (operator === '&&' || operator === '||') { + return `(${filterRules.map(parseCondition).join(` ${operatorMap[operator]} `)})`; + } + let field = filter[1]; + let value = filter[2]; + if (typeof value === 'string') { + value = `'${value}'`; + } else if (typeof value === 'boolean') { + value = value ? 'TRUE' : 'FALSE'; + } + return `${field} ${operatorMap[operator]} ${value}`; + + } + } + return ''; + } + let geostylerRules = geostylerFilter.body; + return parseCondition(geostylerRules); +}; + +// TODO: create a converter from cql to geostyler rules + +export default { + cql + // geostyler +}; diff --git a/web/client/utils/filter/converters/index.js b/web/client/utils/filter/converters/index.js index 11b43216b1..73ae202c48 100644 --- a/web/client/utils/filter/converters/index.js +++ b/web/client/utils/filter/converters/index.js @@ -9,6 +9,7 @@ * @prop {function} toOgc */ import cql from './cql'; +import geostyler from './geostyler'; const converters = { }; @@ -33,6 +34,7 @@ export const canConvert = (from, to) => { }; converters.cql = cql; +converters.geostyler = geostyler; converters.logic = { cql: (filter) => { @@ -83,5 +85,25 @@ converters.logic = { } return null; + }, + geostyler: (filter) => { + if (filter.logic) { + const convertFilter = (f) => { + if (canConvert(f.format, 'geostyler')) { + return getConverter(f.format, 'geostyler')(f); + } + return null; + }; + if (!filter.filters || filter.filters.length === 0) { + return []; + } else if (filter.filters.length === 1) { + if (filter.logic.toUpperCase() === 'NOT') { + return ['!', convertFilter(filter.filters[0])]; + } + return convertFilter(filter.filters[0]); + } + return [ filter.logic.toUpperCase(), ...filter.filters.map(convertFilter) ]; + } + return null; } }; From ccce09030a94279e2d14d1144b35be3388182b75 Mon Sep 17 00:00:00 2001 From: mahmoudadel54 Date: Mon, 13 Jan 2025 23:47:35 +0200 Subject: [PATCH 2/5] #10736: fix FE unit tests for CommonAdvancedSettings --- .../AdvancedSettings/__tests__/CommonAdvancedSettings-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js index 51994fb885..9c44a38cb6 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js +++ b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js @@ -40,7 +40,7 @@ describe('Test common advanced settings', () => { const advancedSettingPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingPanel).toBeTruthy(); const fields = document.querySelectorAll(".form-group"); - expect(fields.length).toBe(2); + expect(fields.length).toBe(3); }); it('test wms advanced options onChangeServiceProperty autoreload', () => { const action = { @@ -54,7 +54,7 @@ describe('Test common advanced settings', () => { const advancedSettingPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingPanel).toBeTruthy(); const fields = document.querySelectorAll(".form-group"); - expect(fields.length).toBe(2); + expect(fields.length).toBe(3); const autoload = document.querySelectorAll('input[type="checkbox"]')[0]; const formGroup = document.querySelectorAll('.form-group')[0]; expect(formGroup.textContent.trim()).toBe('catalog.autoload'); From 9101a9453b1fd0702306eee27297ac106145d680 Mon Sep 17 00:00:00 2001 From: mahmoudadel54 Date: Fri, 17 Jan 2025 11:31:02 +0200 Subject: [PATCH 3/5] #10736: Interactive legend for TOC layers [WFS Layer part] (resolve reviews) - fix issue of activating interactive legend even if experimentalInteractiveLegend = false in localConfig - remove serverType check for wfs interactivity legend display - replace owner prop with interactive prop for wms and wfs - handle not calling legend filter handler in case not activate the interactivity or no rule to apply - add double check for geostyler if it is a direct array or has body in converter file - reset miscSettings in CommonAdvancedSettings test file in afterEach - add tranlations --- .../TOC/fragments/settings/Display.jsx | 10 ++++---- .../CommonAdvancedSettings.jsx | 2 +- .../__tests__/CommonAdvancedSettings-test.js | 1 + .../plugins/TOC/components/DefaultLayer.jsx | 3 ++- .../components/StyleBasedWMSJsonLegend.jsx | 13 ++++++---- .../plugins/TOC/components/VectorLegend.jsx | 24 ++++++++++++------- .../plugins/TOC/components/WMSLegend.jsx | 1 + web/client/translations/data.de-DE.json | 1 + web/client/translations/data.en-US.json | 1 + web/client/translations/data.es-ES.json | 1 + web/client/translations/data.fr-FR.json | 1 + web/client/translations/data.it-IT.json | 1 + .../utils/filter/converters/geostyler.js | 4 +++- 13 files changed, 41 insertions(+), 22 deletions(-) diff --git a/web/client/components/TOC/fragments/settings/Display.jsx b/web/client/components/TOC/fragments/settings/Display.jsx index a7a52a6904..293f07808f 100644 --- a/web/client/components/TOC/fragments/settings/Display.jsx +++ b/web/client/components/TOC/fragments/settings/Display.jsx @@ -326,7 +326,6 @@ export default class extends React.Component {
    { enableInteractiveLegend ? } {this.props.element.type === "wfs" &&
    - + {experimentalInteractiveLegend && - - { experimentalInteractiveLegend && this.props.element?.serverType !== ServerTypes.NO_VENDOR && !this.props?.hideInteractiveLegendOption && + } + { experimentalInteractiveLegend && !this.props?.hideInteractiveLegendOption && -  } /> +  } /> } @@ -380,7 +379,6 @@ export default class extends React.Component {
    diff --git a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx index 4028756757..8158645754 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx +++ b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx @@ -56,7 +56,7 @@ export default ({ onChange={(e) => onChangeServiceProperty("layerOptions", { ...service.layerOptions, enableInteractiveLegend: e.target.checked})} checked={!isNil(service.layerOptions?.enableInteractiveLegend) ? service.layerOptions?.enableInteractiveLegend : false}> -  } /> +  } /> } {children} diff --git a/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js index 9c44a38cb6..c4a4d73a3d 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js +++ b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js @@ -21,6 +21,7 @@ describe('Test common advanced settings', () => { afterEach((done) => { ReactDOM.unmountComponentAtNode(document.getElementById("container")); document.body.innerHTML = ''; + setConfigProp('miscSettings', { }); setTimeout(done); }); it('creates the component with defaults', () => { diff --git a/web/client/plugins/TOC/components/DefaultLayer.jsx b/web/client/plugins/TOC/components/DefaultLayer.jsx index 826d09b659..b37c98f69a 100644 --- a/web/client/plugins/TOC/components/DefaultLayer.jsx +++ b/web/client/plugins/TOC/components/DefaultLayer.jsx @@ -94,7 +94,7 @@ const DefaultLayerNode = ({ @@ -113,6 +113,7 @@ const DefaultLayerNode = ({ language={config?.language} {...config?.layerOptions?.legendOptions} onChange={onChange} + interactive /> diff --git a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx index 0bac5e6f36..b5a625fbbe 100644 --- a/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx +++ b/web/client/plugins/TOC/components/StyleBasedWMSJsonLegend.jsx @@ -43,7 +43,7 @@ class StyleBasedWMSJsonLegend extends React.Component { scaleDependent: PropTypes.bool, language: PropTypes.string, onChange: PropTypes.func, - owner: PropTypes.string, + interactive: PropTypes.bool, // the indicator flag that refers if this legend is interactive or not projection: PropTypes.string, mapSize: PropTypes.object, mapBbox: PropTypes.object @@ -56,7 +56,7 @@ class StyleBasedWMSJsonLegend extends React.Component { style: {maxWidth: "100%"}, scaleDependent: true, onChange: () => {}, - owner: '' + interactive: false }; state = { error: false, @@ -155,6 +155,7 @@ class StyleBasedWMSJsonLegend extends React.Component { const interactiveLegendFilters = get(layerFilter, 'filters', []).find(f => f.id === INTERACTIVE_LEGEND_ID); const legendFilters = get(interactiveLegendFilters, 'filters', []); const showResetWarning = !this.checkPreviousFiltersAreValid(rules, legendFilters) && !layerFilter.disabled; + const isNotInteractiveLegend = !this.props.interactive; return ( <> {showResetWarning ? @@ -172,11 +173,15 @@ class StyleBasedWMSJsonLegend extends React.Component { : rules.map((rule, idx) => { const activeFilter = legendFilters?.some(f => f.id === rule.filter); const isFilterDisabled = this.props?.layer?.layerFilter?.disabled; + const isLegendFilterNotApplicable = isFilterDisabled || isNotInteractiveLegend || !rule?.filter; return (
    this.filterWMSLayerHandler(rule.filter)}> + onClick={() => { + if (isLegendFilterNotApplicable) return; + this.filterWMSLayerHandler(rule.filter); + }}> {rule.name || rule.title || ''}
    diff --git a/web/client/plugins/TOC/components/VectorLegend.jsx b/web/client/plugins/TOC/components/VectorLegend.jsx index d9c52914e8..2a5ef783d2 100644 --- a/web/client/plugins/TOC/components/VectorLegend.jsx +++ b/web/client/plugins/TOC/components/VectorLegend.jsx @@ -16,22 +16,21 @@ import { ButtonWithTooltip } from '../../../components/misc/Button'; import RuleLegendIcon from '../../../components/styleeditor/RuleLegendIcon'; import { INTERACTIVE_LEGEND_ID } from '../../../utils/LegendUtils'; import { updateLayerWFSVectorLegendFilter } from '../../../utils/FilterUtils'; +import { getMiscSetting } from '../../../utils/ConfigUtils'; /** * VectorLegend renders the legend given a valid vector style * @prop {object} style a layer style object in geostyler format * @prop {object} layer the vector layer object - * @prop {string} owner the owner of the compoenent + * @prop {boolean} interactive the indicator flag that refers if this legend is interactive or not * @prop {function} onChange the onChange layer handler */ -function VectorLegend({ style, layer, owner, onChange }) { +function VectorLegend({ style, layer, interactive, onChange }) { const onResetLegendFilter = () => { const newLayerFilter = updateLayerWFSVectorLegendFilter(layer?.layerFilter); onChange({ layerFilter: newLayerFilter }); }; const filterLayerHandler = (filter) => { - const isFilterDisabled = layer?.layerFilter?.disabled; - if (!filter || isFilterDisabled) return; const newLayerFilter = updateLayerWFSVectorLegendFilter(layer?.layerFilter, filter); onChange({ layerFilter: newLayerFilter }); }; @@ -44,9 +43,10 @@ function VectorLegend({ style, layer, owner, onChange }) { const interactiveLegendFilters = get(layerFilter, 'filters', []).find(f => f.id === INTERACTIVE_LEGEND_ID); const legendFilters = get(interactiveLegendFilters, 'filters', []); const showResetWarning = !checkPreviousFiltersAreValid(rules, legendFilters) && !layerFilter.disabled; - + const experimentalInteractiveLegend = getMiscSetting('experimentalInteractiveLegend', false); + const isNotInteractiveLegend = !(interactive && layer?.enableInteractiveLegend && experimentalInteractiveLegend); return (<> - {showResetWarning && owner !== 'legendPreview' ? + {showResetWarning && !isNotInteractiveLegend ?
    { const isFilterDisabled = layer?.layerFilter?.disabled; const activeFilter = legendFilters?.some(f => f.id === rule?.filter?.toString()); + const isLegendFilterNotApplicable = isFilterDisabled || isNotInteractiveLegend || !rule?.filter; + return (
    filterLayerHandler(rule.filter)} - className={`ms-legend-rule ${isFilterDisabled || owner === 'legendPreview' || !rule?.filter ? "" : "filter-enabled "} ${activeFilter ? 'active' : ''}`}> + onClick={() => { + // don't call filter handler if it is not interactive legend or filter is disabled or the filter rule is not truthy value + if (isLegendFilterNotApplicable) return; + filterLayerHandler(rule.filter); + }} + className={`ms-legend-rule ${isLegendFilterNotApplicable ? "" : "filter-enabled "} ${activeFilter && interactive ? 'active' : ''}`}> {rule.name || ''}
    ); @@ -83,7 +89,7 @@ function VectorLegend({ style, layer, owner, onChange }) { VectorLegend.propTypes = { style: PropTypes.object, layer: PropTypes.object, - owner: PropTypes.string, + interactive: PropTypes.bool, onChange: PropTypes.func }; diff --git a/web/client/plugins/TOC/components/WMSLegend.jsx b/web/client/plugins/TOC/components/WMSLegend.jsx index cd8db22cea..9164cc0173 100644 --- a/web/client/plugins/TOC/components/WMSLegend.jsx +++ b/web/client/plugins/TOC/components/WMSLegend.jsx @@ -126,6 +126,7 @@ class WMSLegend extends React.Component { legendOptions={this.props.WMSLegendOptions} onChange={this.props.onChange} {...this.getLegendProps()} + interactive />
    ); diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index ded105d728..3eba05df93 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -206,6 +206,7 @@ "enableInteractiveLegendInfo": { "label": "Aktivieren Sie die interaktive Legende", "info": "Wenn diese Option aktiviert ist, kann der Filter nach Legende angewendet werden, indem im Inhaltsverzeichnis auf Legendenelemente dieser Ebene geklickt wird. Hinweis: Diese Einstellung benötigt spezifische Konfigurationen im GeoServer", + "infoWithoutGSNote": "Wenn diese Option aktiviert ist, kann der Filter nach Legende angewendet werden, indem im Inhaltsverzeichnis auf Legendenelemente dieser Ebene geklickt wird.", "fetchError": "Die Legenden Informationen konnten nicht vom Dienst abgerufen werden" }, "enableLocalizedLayerStyles": { diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 3707833e43..7f6b38ec63 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -206,6 +206,7 @@ "enableInteractiveLegendInfo": { "label": "Enable interactive legend", "info": "If this option is enabled, filter by legend can be applied by clicking on legend items of this layer from the TOC. Note: This parameter requires specific configurations on GeoServer", + "infoWithoutGSNote": "If this option is enabled, filter by legend can be applied by clicking on legend items of this layer from the TOC.", "fetchError": "Failed to retrieve the legend info from the service" }, "enableLocalizedLayerStyles": { diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index f032dcc699..2a059f24b3 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -203,6 +203,7 @@ "enableInteractiveLegendInfo": { "label": "Habilitar una leyenda interactiva", "info": "Si esta opción está habilitada, se puede aplicar el filtro por leyenda haciendo clic en los elementos de leyenda de esta capa desde el TOC. Nota: este parámetro requiere configuraciones específicas en GeoServer", + "infoWithoutGSNote": "Si esta opción está habilitada, se puede aplicar el filtro por leyenda haciendo clic en los elementos de leyenda de esta capa desde el TOC.", "fetchError": "No se pudo obtener la información de la leyenda del servicio" }, "templateFormatInfoAlertExample": "La identificación de la característica es ${ properties.datos } ora ${ properties['datos-valor'] }", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 6b2919b7d4..cbec4025fc 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -206,6 +206,7 @@ "enableInteractiveLegendInfo": { "label": "Activer la légende interactive", "info": "Si cette option est activée, le filtre par légende peut être appliqué en cliquant sur les éléments de légende de cette couche depuis la table des matières. Remarque: ce paramètre nécessite des configurations spécifiques sur GeoServer", + "infoWithoutGSNote": "Si cette option est activée, le filtre par légende peut être appliqué en cliquant sur les éléments de légende de cette couche depuis la table des matières.", "fetchError": "Impossible d'obtenir les informations de légende du service" }, "enableLocalizedLayerStyles": { diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 2efbfedf61..a0f6dce85b 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -206,6 +206,7 @@ "enableInteractiveLegendInfo": { "label": "Abilita leggenda interattiva", "info": "Se questa opzione è abilitata, è possibile applicare il filtro per legenda facendo clic sugli elementi della legenda di questo layer dal sommario. Nota: questo parametro richiede configurazioni specifiche su GeoServer", + "infoWithoutGSNote": "Se questa opzione è abilitata, è possibile applicare il filtro per legenda facendo clic sugli elementi della legenda di questo layer dal sommario.", "fetchError": "Impossibile ottenere le informazioni sulla leggenda dal servizio" }, "enableLocalizedLayerStyles": { diff --git a/web/client/utils/filter/converters/geostyler.js b/web/client/utils/filter/converters/geostyler.js index 8fb74347c0..2debbf666f 100644 --- a/web/client/utils/filter/converters/geostyler.js +++ b/web/client/utils/filter/converters/geostyler.js @@ -6,6 +6,8 @@ * LICENSE file in the root directory of this source tree. */ +import { isArray } from "lodash"; + const operatorMap = { '==': '=', '!=': '<>', @@ -44,7 +46,7 @@ export const cql = (geostylerFilter) => { } return ''; } - let geostylerRules = geostylerFilter.body; + const geostylerRules = isArray(geostylerFilter) ? geostylerFilter : geostylerFilter?.body; return parseCondition(geostylerRules); }; From edecb3aeb4c403782a6f17910155b6dae82654e3 Mon Sep 17 00:00:00 2001 From: mahmoudadel54 Date: Fri, 17 Jan 2025 15:21:53 +0200 Subject: [PATCH 4/5] #10736: fix issue due to merge master --- web/client/plugins/TOC/components/DefaultLayer.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/client/plugins/TOC/components/DefaultLayer.jsx b/web/client/plugins/TOC/components/DefaultLayer.jsx index 2a6ddcddde..673660267d 100644 --- a/web/client/plugins/TOC/components/DefaultLayer.jsx +++ b/web/client/plugins/TOC/components/DefaultLayer.jsx @@ -64,6 +64,9 @@ const NodeLegend = ({
  • {visible ? : null}
  • @@ -81,6 +84,7 @@ const NodeLegend = ({ language={config?.language} {...config?.layerOptions?.legendOptions} onChange={onChange} + interactive /> : null} From fbdba1902d274fc8d7538cbcb0c01befb1ddff83 Mon Sep 17 00:00:00 2001 From: mahmoudadel54 Date: Fri, 17 Jan 2025 16:08:59 +0200 Subject: [PATCH 5/5] #10736: fix FE unit test failure --- .../TOC/components/__tests__/VectorLegend-test.jsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx b/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx index de6de83bac..546f26e03a 100644 --- a/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx +++ b/web/client/plugins/TOC/components/__tests__/VectorLegend-test.jsx @@ -11,6 +11,7 @@ import ReactDOM from 'react-dom'; import expect from 'expect'; import VectorLegend from '../VectorLegend'; import { INTERACTIVE_LEGEND_ID } from '../../../../utils/LegendUtils'; +import { setConfigProp } from '../../../../utils/ConfigUtils'; const rules = [ { @@ -152,12 +153,14 @@ const rules = [ describe('VectorLegend module component', () => { beforeEach((done) => { document.body.innerHTML = '
    '; + setConfigProp('miscSettings', { experimentalInteractiveLegend: true }); setTimeout(done); }); afterEach((done) => { ReactDOM.unmountComponentAtNode(document.getElementById('container')); document.body.innerHTML = ''; + setConfigProp('miscSettings', { }); setTimeout(done); }); @@ -452,7 +455,7 @@ describe('VectorLegend module component', () => { const textElement = ruleElements[0].getElementsByTagName('span'); expect(textElement[0].innerHTML).toBe(''); }); - it('tests legend with empty rules', async() => { + it('tests legend with empty rules', () => { const l = { name: 'layer00', title: 'Layer', @@ -469,7 +472,7 @@ describe('VectorLegend module component', () => { expect(legendElem).toBeTruthy(); expect(legendElem.innerText).toBe('layerProperties.interactiveLegend.noLegendData'); }); - it('tests legend with incompatible filter rules', async() => { + it('tests legend with incompatible filter rules', () => { const l = { name: 'layer00', title: 'Layer', @@ -477,6 +480,7 @@ describe('VectorLegend module component', () => { storeIndex: 9, type: 'wfs', url: 'http://localhost:8080/geoserver2/wfs', + enableInteractiveLegend: true, layerFilter: { filters: [{ id: INTERACTIVE_LEGEND_ID, @@ -487,7 +491,7 @@ describe('VectorLegend module component', () => { disabled: false } }; - ReactDOM.render(, document.getElementById("container")); + ReactDOM.render(, document.getElementById("container")); const legendElem = document.querySelector('.ms-legend'); expect(legendElem).toBeTruthy(); const legendRuleElem = document.querySelector('.ms-legend .alert-warning'); @@ -496,7 +500,7 @@ describe('VectorLegend module component', () => { const resetLegendFilter = document.querySelector('.ms-legend .alert-warning button'); expect(resetLegendFilter).toBeTruthy(); }); - it('tests hide warning when layer filter is disabled', async() => { + it('tests hide warning when layer filter is disabled', () => { const l = { name: 'layer00', title: 'Layer',