From 209e410a609b8e7108a28391472dd65eb8a56ba6 Mon Sep 17 00:00:00 2001 From: RowHeat <40065760+rowheat02@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:39:03 +0545 Subject: [PATCH] fix: on layer remove, clean up the ineteractions (#12043) (cherry picked from commit 4437da3956bb43aeecbe4c7961d347a6d86fca1b) --- .../epics/__tests__/interactions-test.js | 44 ++++++++++++++++- web/client/epics/interactions.js | 47 ++++++++++++++++++- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/web/client/epics/__tests__/interactions-test.js b/web/client/epics/__tests__/interactions-test.js index f55cd3ef72..cb83e58c8e 100644 --- a/web/client/epics/__tests__/interactions-test.js +++ b/web/client/epics/__tests__/interactions-test.js @@ -11,7 +11,7 @@ import { testEpic } from './epicTestUtils'; import { applyFilterWidgetInteractionsEpic, cleanupAndReapplyFilterWidgetInteractionsEpic } from '../interactions'; import { applyFilterWidgetInteractions } from '../../actions/interactions'; import { UPDATE_PROPERTY, DELETE } from '../../actions/widgets'; -import { CHANGE_LAYER_PROPERTIES } from '../../actions/layers'; +import { CHANGE_LAYER_PROPERTIES, removeNode } from '../../actions/layers'; const FILTER_ID = 'filter-1'; const FILTER_WIDGET_ID = 'filter-widget-1'; @@ -186,6 +186,48 @@ describe('interactions epics', () => { }); describe('cleanupAndReapplyFilterWidgetInteractionsEpic', () => { + it('on remove map layer, removes interactions targeting deleted main map layer', (done) => { + const deletedLayerId = 'layer-A'; + const remainingLayerId = 'layer-B'; + const filterWidget = makeFilterWidget({ + interactions: [ + { + id: 'int-layer-a', + plugged: true, + targetType: 'applyStyle', + source: { nodePath: `widgets[${FILTER_WIDGET_ID}].filters[${FILTER_ID}]` }, + target: { nodePath: `map.layers[${deletedLayerId}]` } + }, + { + id: 'int-layer-b', + plugged: true, + targetType: 'applyStyle', + source: { nodePath: `widgets[${FILTER_WIDGET_ID}].filters[${FILTER_ID}]` }, + target: { nodePath: `map.layers[${remainingLayerId}]` } + } + ] + }); + const state = makeState([filterWidget]); + + testEpic( + cleanupAndReapplyFilterWidgetInteractionsEpic, + 1, + [removeNode(deletedLayerId, 'layers')], + (actions) => { + expect(actions.length).toBe(1); + expect(actions[0].type).toBe(UPDATE_PROPERTY); + expect(actions[0].id).toBe(FILTER_WIDGET_ID); + expect(actions[0].key).toBe('interactions'); + const updatedInteractions = actions[0].value || []; + expect(updatedInteractions.length).toBe(1); + expect(updatedInteractions[0].id).toBe('int-layer-b'); + expect(updatedInteractions[0].target.nodePath).toBe(`map.layers[${remainingLayerId}]`); + }, + state, + done + ); + }); + it('on DELETE filter widget, cleans up trace.interactionFilters from chart and dispatches updateWidgetProperty(charts)', (done) => { const filterWidget = makeFilterWidget(); const chartWidget = makeChartWidget({ diff --git a/web/client/epics/interactions.js b/web/client/epics/interactions.js index 5cec1dec83..122087ea28 100644 --- a/web/client/epics/interactions.js +++ b/web/client/epics/interactions.js @@ -12,7 +12,7 @@ import { get } from 'lodash'; import { extractTraceFromWidgetByNodePath, extractLayerIdFromNodePath, isMapLayerPath, TARGET_TYPES } from '../utils/InteractionUtils'; import { updateWidgetProperty, INSERT, UPDATE, DELETE } from '../actions/widgets'; import { getLayerFromId, layersSelector } from '../selectors/layers'; -import { changeLayerProperties } from '../actions/layers'; +import { changeLayerProperties, REMOVE_NODE } from '../actions/layers'; import { defaultLayerFilter } from '../utils/FilterUtils'; import { processFilterToCQL, buildExcludeCQLFilter, buildDefaultCQLFilter } from '../utils/FilterEventUtils'; import { FILTER_SELECTION_MODES } from '../components/widgets/builder/wizard/filter/FilterDataTab/constants'; @@ -622,6 +622,41 @@ function cleanupFiltersByWidgetId(widgetId, state, targetContainer = 'floating') return [...layerActions, ...widgetActions]; } +/** + * Cleanup interactions from filter widgets that reference a deleted main map layer. + * Removes interactions where target node is map.layers[deletedLayerId]. + * @param {string} deletedLayerId - deleted layer id + * @param {object} state - redux state + * @param {string} targetContainer - widget target container + * @returns {array} update actions + */ +function cleanupAfterLayerDeletion(deletedLayerId, state, targetContainer = 'floating') { + if (!deletedLayerId) { + return []; + } + + const actions = []; + const allWidgets = get(state, `widgets.containers[${targetContainer}].widgets`) || []; + const filterWidgets = allWidgets.filter(w => w.widgetType === 'filter'); + + filterWidgets.forEach(filterWidget => { + const interactions = filterWidget.interactions || []; + const filteredInteractions = interactions.filter(interaction => { + const targetNodePath = interaction?.target?.nodePath || ''; + if (!isMapLayerPath(targetNodePath)) { + return true; + } + return extractLayerIdFromNodePath(targetNodePath) !== deletedLayerId; + }); + + if (filteredInteractions.length !== interactions.length) { + actions.push(updateWidgetProperty(filterWidget.id, 'interactions', filteredInteractions, 'replace', targetContainer)); + } + }); + + return actions; +} + /** * Cleanup interactions from filter widgets that reference a deleted widget * Removes interactions where source.nodePath contains the deleted widget ID @@ -831,7 +866,7 @@ export const applyFilterWidgetInteractionsEpic = (action$, store) => { */ export const cleanupAndReapplyFilterWidgetInteractionsEpic = (action$, store) => { return action$ - .ofType(DELETE, INSERT, UPDATE) + .ofType(DELETE, INSERT, UPDATE, REMOVE_NODE) .mergeMap((action) => { const state = store.getState(); let widget = null; @@ -864,6 +899,14 @@ export const cleanupAndReapplyFilterWidgetInteractionsEpic = (action$, store) => return interactionCleanupActions.length > 0 ? Rx.Observable.from(interactionCleanupActions) : Rx.Observable.empty(); + } else if (action.type === REMOVE_NODE) { + if (action.nodeType !== 'layers') { + return Rx.Observable.empty(); + } + const layerCleanupActions = cleanupAfterLayerDeletion(action.node, state, target); + return layerCleanupActions.length > 0 + ? Rx.Observable.from(layerCleanupActions) + : Rx.Observable.empty(); } else if (action.type === INSERT) { widget = action.widget; widgetId = action.id || widget?.id;