diff --git a/web/client/actions/__tests__/mapPopups-test.js b/web/client/actions/__tests__/mapPopups-test.js index 18296feba6..1af4ecc32a 100644 --- a/web/client/actions/__tests__/mapPopups-test.js +++ b/web/client/actions/__tests__/mapPopups-test.js @@ -26,5 +26,9 @@ describe('test map popups action creators', () => { const action = POPUP.cleanPopups(); expect(action.type).toEqual(POPUP.CLEAN_MAP_POPUPS); }); + it('enable hide empty popup option', () => { + const action = POPUP.enableHideEmptyPopupOption(); + expect(action.type).toEqual(POPUP.ENABLE_HIDE_EMPTY_POPUP); + }); }); diff --git a/web/client/actions/mapPopups.js b/web/client/actions/mapPopups.js index ed001897fc..66ab51c315 100644 --- a/web/client/actions/mapPopups.js +++ b/web/client/actions/mapPopups.js @@ -10,6 +10,7 @@ export const ADD_MAP_POPUP = 'MAP:ADD_POPUP'; export const REMOVE_MAP_POPUP = 'MAP:REMOVE_POPUP'; export const CLEAN_MAP_POPUPS = 'MAP:CLEAN_POPUPS'; +export const ENABLE_HIDE_EMPTY_POPUP = 'MAP:ENABLE_HIDE_EMPTY_POPUP'; export const addPopup = (id, options, single = true) => ({ type: ADD_MAP_POPUP, @@ -26,3 +27,7 @@ export const removePopup = (id) => ({ export const cleanPopups = () => ({ type: CLEAN_MAP_POPUPS }); + +export const enableHideEmptyPopupOption = () => ({ + type: ENABLE_HIDE_EMPTY_POPUP +}); diff --git a/web/client/components/data/identify/DefaultViewer.jsx b/web/client/components/data/identify/DefaultViewer.jsx index 8a18d5330c..ff02bfbc6d 100644 --- a/web/client/components/data/identify/DefaultViewer.jsx +++ b/web/client/components/data/identify/DefaultViewer.jsx @@ -40,7 +40,8 @@ class DefaultViewer extends React.Component { renderValidOnly: PropTypes.bool, loaded: PropTypes.bool, isMobile: PropTypes.bool, - disableInfoAlert: PropTypes.bool + disableInfoAlert: PropTypes.bool, + hidePopupIfNoResults: PropTypes.bool }; static defaultProps = { @@ -64,7 +65,8 @@ class DefaultViewer extends React.Component { onPrevious: () => {}, setIndex: () => {}, isMobile: false, - disableInfoAlert: false + disableInfoAlert: false, + hidePopupIfNoResults: false }; shouldComponentUpdate(nextProps) { @@ -147,6 +149,9 @@ class DefaultViewer extends React.Component { renderEmptyPages = () => { const {emptyResponses} = this.getResponseProperties(); if (this.props.missingResponses === 0 && emptyResponses) { + if (this.props.hidePopupIfNoResults) { + return ; + } return (

diff --git a/web/client/components/data/identify/PopupViewer.jsx b/web/client/components/data/identify/PopupViewer.jsx index 399467a0e1..1f16a19afa 100644 --- a/web/client/components/data/identify/PopupViewer.jsx +++ b/web/client/components/data/identify/PopupViewer.jsx @@ -17,6 +17,7 @@ import Viewer from './DefaultViewer'; import {isArray, isUndefined} from 'lodash'; import SwipeHeader from './SwipeHeader'; import { identifyFloatingToolSelector } from '../../../selectors/map'; +import { hideEmptyPopupSelector } from '../../../selectors/mapPopups'; /** * Container that render only the selected result @@ -46,8 +47,8 @@ const selector = createSelector([ generalInfoFormatSelector, showEmptyMessageGFISelector, identifyFloatingToolSelector, - isLoadedResponseSelector], -(responses, validResponses, requests, format, showEmptyMessageGFI, renderValidOnly, loaded) => ({ + isLoadedResponseSelector, hideEmptyPopupSelector], +(responses, validResponses, requests, format, showEmptyMessageGFI, renderValidOnly, loaded, hidePopupIfNoResults ) => ({ responses, validResponses, requests, @@ -55,7 +56,8 @@ const selector = createSelector([ showEmptyMessageGFI, missingResponses: (requests || []).length - (responses || []).length, renderValidOnly, - loaded + loaded, + hidePopupIfNoResults })); diff --git a/web/client/components/data/identify/__tests__/DefaultViewer-test.jsx b/web/client/components/data/identify/__tests__/DefaultViewer-test.jsx index 9fbe93fffd..3c8f19a47e 100644 --- a/web/client/components/data/identify/__tests__/DefaultViewer-test.jsx +++ b/web/client/components/data/identify/__tests__/DefaultViewer-test.jsx @@ -269,4 +269,18 @@ describe('DefaultViewer', () => { expect(gfiViewer.childNodes[1].childNodes.length).toBe(1); }); + it('test DefaultViewer component with hover identify if hidePopupIfNoResults = true', () => { + const responses = []; + ReactDOM.render( + , + document.getElementById("container") + ); + + const container = document.getElementById('container'); + let gfiViewer = container.querySelector('.mapstore-identify-viewer'); + expect(gfiViewer).toBeTruthy(); + expect(gfiViewer.childNodes.length).toBe(1); + expect(document.querySelector(".hidePopupIfNoResults")).toBeTruthy(); + expect(document.querySelector(".hidePopupIfNoResults").innerHTML).toBeFalsy(); + }); }); diff --git a/web/client/components/data/identify/enhancers/__tests__/identify-test.jsx b/web/client/components/data/identify/enhancers/__tests__/identify-test.jsx index 1ba1708dc8..27369942cb 100644 --- a/web/client/components/data/identify/enhancers/__tests__/identify-test.jsx +++ b/web/client/components/data/identify/enhancers/__tests__/identify-test.jsx @@ -187,6 +187,18 @@ describe("test identify enhancers", () => { ); expect(spyIdentifyIsMounted.calls.length).toEqual(1); }); + it("test identifyLifecycle component for call enableHideEmptyPopupOption if hidePopupIfNoResults prop = true", () => { + const Component = identifyLifecycle(() =>
); + const testHandlers = { + enableHideEmptyPopupOption: () => {} + }; + const spyEnableHideEmptyPopupOption = expect.spyOn(testHandlers, 'enableHideEmptyPopupOption'); + ReactDOM.render( + , + document.getElementById("container") + ); + expect(spyEnableHideEmptyPopupOption.calls.length).toEqual(1); + }); it("Identify should run when enabled prop is true and showInMapPopup prop is false", () => { let run = sampleComponentDidMount({enabled: true, showInMapPopup: false}); expect(run.checkIdentifyIsMounted).toBe(true); diff --git a/web/client/components/data/identify/enhancers/identify.js b/web/client/components/data/identify/enhancers/identify.js index 03724ec45e..fd66c04056 100644 --- a/web/client/components/data/identify/enhancers/identify.js +++ b/web/client/components/data/identify/enhancers/identify.js @@ -79,7 +79,9 @@ export const identifyLifecycle = compose( setShowInMapPopup = () => {}, checkIdentifyIsMounted = () => {}, onInitPlugin = () => {}, - pluginCfg = {} + pluginCfg = {}, + enableHideEmptyPopupOption = () => {}, + hidePopupIfNoResults = false } = this.props; // Initialize plugin configuration @@ -91,6 +93,9 @@ export const identifyLifecycle = compose( showAllResponses, highlight: pluginCfg?.highlightEnabledFromTheStart || false }); + if (hidePopupIfNoResults) { + enableHideEmptyPopupOption(true); + } if (enabled || showInMapPopup) { changeMousePointer('pointer'); checkIdentifyIsMounted(true); diff --git a/web/client/epics/__tests__/identify-test.js b/web/client/epics/__tests__/identify-test.js index 613bb39779..7850e04d0f 100644 --- a/web/client/epics/__tests__/identify-test.js +++ b/web/client/epics/__tests__/identify-test.js @@ -965,6 +965,54 @@ describe('identify Epics', () => { testEpic(zoomToVisibleAreaEpic, 3, sentActions, expectedAction, state); }); + it('test zoomToVisibleAreaEpic remove shown marker of identify if no results + existing hideEmptyPopupOption flag = true', (done) => { + // remove previous hook + registerHook('RESOLUTION_HOOK', undefined); + + const state = { + mapInfo: { + centerToMarker: true + }, + mapPopups: { + hideEmptyPopupOption: true + }, + map: {present: {...TEST_MAP_STATE.present, eventListeners: {mousemove: ["identifyFloatingTool"]}}}, + maplayout: { + boundingMapRect: { + left: 500, + bottom: 250 + } + } + }; + + const sentActions = [ + featureInfoClick({ latlng: { lat: 36.95, lng: -79.84 } }), + loadFeatureInfo(1, "no features were found") + ]; + + const expectedAction = actions => { + try { + expect(actions.length).toBe(2); + actions.map((action) => { + switch (action.type) { + case HIDE_MAPINFO_MARKER: + done(); + break; + case UPDATE_CENTER_TO_MARKER: + expect(action.status).toBe('disabled'); + break; + default: + expect(true).toBe(false); + } + }); + } catch (ex) { + done(ex); + } + done(); + }; + + testEpic(zoomToVisibleAreaEpic, 2, sentActions, expectedAction, state); + }); it('onMapClick triggers featureinfo when selected', done => { registerHook(GET_COORDINATES_FROM_PIXEL_HOOK, undefined); diff --git a/web/client/epics/identify.js b/web/client/epics/identify.js index e375336595..2ce441c643 100644 --- a/web/client/epics/identify.js +++ b/web/client/epics/identify.js @@ -51,6 +51,7 @@ import { floatingIdentifyDelaySelector } from '../selectors/localConfig'; import { createControlEnabledSelector, measureSelector } from '../selectors/controls'; import { localizedLayerStylesEnvSelector } from '../selectors/localizedLayerStyles'; import { mouseOutSelector } from '../selectors/mousePosition'; +import { hideEmptyPopupSelector } from '../selectors/mapPopups'; import {getBbox, getCurrentResolution, parseLayoutValue} from '../utils/MapUtils'; import {buildIdentifyRequest, defaultQueryableFilter, filterRequestParams} from '../utils/MapInfoUtils'; import { IDENTIFY_POPUP } from '../components/map/popups'; @@ -250,8 +251,15 @@ export const zoomToVisibleAreaEpic = (action$, store) => .filter(() => centerToMarkerSelector(store.getState())) .switchMap((action) => action$.ofType(LOAD_FEATURE_INFO, ERROR_FEATURE_INFO) - .mergeMap(() => { + .mergeMap((loadFeatInfoAction) => { const state = store.getState(); + const hideIdentifyPopupIfNoResults = hideEmptyPopupSelector(state); + const hoverIdentifyActive = isMouseMoveIdentifyActiveSelector(state); + const noResultFeatures = loadFeatInfoAction.type === LOAD_FEATURE_INFO && loadFeatInfoAction?.data?.includes("no features were found"); + // remove marker in case activated identify hover mode and no fetched results plus existing hideIdentifyPopupIfNoResults = true + if (noResultFeatures && hideIdentifyPopupIfNoResults && hoverIdentifyActive) { + return Rx.Observable.from([updateCenterToMarker('disabled'), hideMapinfoMarker()]); + } const map = mapSelector(state); const mapProjection = projectionSelector(state); const projectionDefs = projectionDefsSelector(state); diff --git a/web/client/plugins/Identify.jsx b/web/client/plugins/Identify.jsx index 30479ae208..fdce0a4edb 100644 --- a/web/client/plugins/Identify.jsx +++ b/web/client/plugins/Identify.jsx @@ -38,6 +38,7 @@ import { checkIdentifyIsMounted, onInitPlugin } from '../actions/mapInfo'; +import { enableHideEmptyPopupOption } from '../actions/mapPopups'; import DefaultViewerComp from '../components/data/identify/DefaultViewer'; import { defaultViewerDefaultProps, defaultViewerHandlers } from '../components/data/identify/enhancers/defaultViewer'; import { identifyLifecycle } from '../components/data/identify/enhancers/identify'; @@ -196,6 +197,7 @@ const identifyDefaultProps = defaultProps({ * @prop cfg.dock {bool} true shows dock panel, false shows modal * @prop cfg.draggable {boolean} draggable info window, when modal * @prop cfg.showHighlightFeatureButton {boolean} show the highlight feature button if the interrogation returned valid features (openlayers only) + * @prop cfg.hidePopupIfNoResults {boolean} hide/show the identify popup in case of no results * @prop cfg.highlightEnabledFromTheStart {boolean} the highlight feature button will be activated by default if true * @prop cfg.viewerOptions.container {expression} the container of the viewer, expression from the context * @prop cfg.viewerOptions.header {expression} the header of the viewer, expression from the context{expression} @@ -265,7 +267,8 @@ const IdentifyPlugin = compose( identifyIndex, defaultViewerHandlers, connect(() => ({}), { - setShowInMapPopup + setShowInMapPopup, + enableHideEmptyPopupOption }), identifyLifecycle )(IdentifyContainer); diff --git a/web/client/plugins/map/index.js b/web/client/plugins/map/index.js index ce519d58a3..6ca6c8765c 100644 --- a/web/client/plugins/map/index.js +++ b/web/client/plugins/map/index.js @@ -35,6 +35,7 @@ import { projectionDefsSelector, isMouseMoveActiveSelector } from '../../selecto import { snappingLayerSelector } from "../../selectors/draw"; +import { mapPopupsSelector } from '../../selectors/mapPopups'; const Empty = () => { return ; }; @@ -101,13 +102,11 @@ const pluginsCreator = (mapType, actions) => { const LLayer = connect(null, {onWarning: warning})( components.Layer || Empty); - const EMPTY_POPUPS = []; const PopupSupport = connect( createSelector( - (state) => state.mapPopups && state.mapPopups.popups || EMPTY_POPUPS, - (popups) => ({ - popups - })), { + mapPopupsSelector, + (popups) => ({popups}) + ), { onPopupClose: removePopup } )(components.PopupSupport || Empty); diff --git a/web/client/reducers/__tests__/mapPopups-test.js b/web/client/reducers/__tests__/mapPopups-test.js index 71d4fc480a..eb31f6385f 100644 --- a/web/client/reducers/__tests__/mapPopups-test.js +++ b/web/client/reducers/__tests__/mapPopups-test.js @@ -37,5 +37,9 @@ describe('mapPopups reducer', () => { expect(state.popups).toExist(); expect(state.popups.length).toBe(0); }); - + it('ENABLE_HIDE_EMPTY_POPUP ', () => { + const state = reducer(initialState, ACTIONS.enableHideEmptyPopupOption()); + expect(state.popups).toExist(); + expect(state.hideEmptyPopupOption).toEqual(true); + }); }); diff --git a/web/client/reducers/mapPopups.js b/web/client/reducers/mapPopups.js index b6cbce9b06..1d8103664b 100644 --- a/web/client/reducers/mapPopups.js +++ b/web/client/reducers/mapPopups.js @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ -import {ADD_MAP_POPUP, REMOVE_MAP_POPUP, CLEAN_MAP_POPUPS} from '../actions/mapPopups'; +import {ADD_MAP_POPUP, REMOVE_MAP_POPUP, CLEAN_MAP_POPUPS, ENABLE_HIDE_EMPTY_POPUP} from '../actions/mapPopups'; import {arrayDelete} from '../utils/ImmutableUtils'; const initialState = {popups: []}; @@ -23,6 +23,11 @@ export default function(state = initialState, action) { case CLEAN_MAP_POPUPS: { return {...state, popups: []}; } + case ENABLE_HIDE_EMPTY_POPUP: { + return { + ...state, hideEmptyPopupOption: true + }; + } default: return state; } diff --git a/web/client/selectors/__tests__/mapPopups-test.js b/web/client/selectors/__tests__/mapPopups-test.js new file mode 100644 index 0000000000..ffa0c75278 --- /dev/null +++ b/web/client/selectors/__tests__/mapPopups-test.js @@ -0,0 +1,36 @@ +/* +* Copyright 2024, 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 { + mapPopupsSelector, + hideEmptyPopupSelector +} from '../mapPopups'; + +describe('Test mapPopups', () => { + it('test mapPopupsSelector', () => { + const popups = mapPopupsSelector({mapPopups: {popups: [{"key": "value"}]}}); + + expect(popups).toExist(); + expect(popups).toEqual([{key: "value"}]); + }); + + it('test hideEmptyPopupSelector true', () => { + const hideEmptyPopupOption = hideEmptyPopupSelector({mapPopups: {popups: [{"key": "value"}], hideEmptyPopupOption: true}}); + + expect(hideEmptyPopupOption).toExist(); + expect(hideEmptyPopupOption).toBe(true); + }); + it('test hideEmptyPopupSelector false', () => { + const hideEmptyPopupOption = hideEmptyPopupSelector({mapPopups: {popups: [{"key": "value"}], hideEmptyPopupOption: false}}); + + expect(hideEmptyPopupOption).toBeFalsy(); + }); +}); diff --git a/web/client/selectors/mapPopups.js b/web/client/selectors/mapPopups.js new file mode 100644 index 0000000000..2515c2f069 --- /dev/null +++ b/web/client/selectors/mapPopups.js @@ -0,0 +1,18 @@ +/* +* Copyright 2024, 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. +*/ + + +/** + * selects mapPopups state + * @name mapPopups + * @memberof selectors + * @static + */ + +export const mapPopupsSelector = (state) => state?.mapPopups && state?.mapPopups?.popups || []; +export const hideEmptyPopupSelector = (state) => state?.mapPopups && state?.mapPopups?.hideEmptyPopupOption || false; diff --git a/web/client/themes/default/less/map-popup.less b/web/client/themes/default/less/map-popup.less index 06fde5d26f..c703a04b8d 100644 --- a/web/client/themes/default/less/map-popup.less +++ b/web/client/themes/default/less/map-popup.less @@ -33,7 +33,9 @@ .map-popup-ol:before { .border-top-color-var(@theme-vars[main-border-color]); } - + .map-popup-ol:has(.hidePopupIfNoResults), .ms-leaflet-popup:has(.hidePopupIfNoResults) { + display: none; + } .ol-popup-closer { .ms-popup-close-button(); }