From d14aa91dc5e23c9c557850ff506be35640455e98 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com>
Date: Wed, 27 Mar 2024 19:58:54 +0100
Subject: [PATCH] feat: add base multisearch implementation

---
 .../MapViewer/utils/config/getCanvasIcon.ts   |  9 +--
 .../pinsLayer/getBioEntitiesFeatures.ts       |  3 +
 .../pinsLayer/getBioEntitySingleFeature.ts    | 18 +++++-
 .../utils/config/pinsLayer/getPinStyle.ts     | 11 +++-
 .../config/pinsLayer/useOlMapPinsLayer.ts     | 24 ++++++--
 .../pinIconClick/useHandlePinIconClick.ts     | 38 ++++++++++++
 .../utils/listeners/useOlMapListeners.ts      |  6 +-
 src/constants/canvas.ts                       |  4 +-
 src/redux/bioEntity/bioEntity.selectors.ts    | 61 +++++++++++++++++++
 src/redux/chemicals/chemicals.selectors.ts    | 42 +++++++++++++
 src/redux/drugs/drugs.selectors.ts            | 42 +++++++++++++
 .../pluginsEventBus.constants.ts              |  2 +
 .../pluginsEventBus/pluginsEventBus.ts        | 16 ++++-
 .../pluginsEventBus/pluginsEventBus.types.ts  |  2 +
 src/types/elements.ts                         |  4 ++
 15 files changed, 264 insertions(+), 18 deletions(-)
 create mode 100644 src/components/Map/MapViewer/utils/listeners/pinIconClick/useHandlePinIconClick.ts
 create mode 100644 src/types/elements.ts

diff --git a/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts b/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts
index 7d3e6925..6b4b4b29 100644
--- a/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts
+++ b/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts
@@ -1,4 +1,4 @@
-import { PIN_PATH2D, PIN_SIZE } from '@/constants/canvas';
+import { PIN_PATH2D, PIN_SIZE, TEXT_COLOR } from '@/constants/canvas';
 import { HALF, ONE_AND_HALF, QUARTER, THIRD, TWO_AND_HALF } from '@/constants/dividers';
 import { DEFAULT_FONT_FAMILY } from '@/constants/font';
 import { Point } from '@/types/map';
@@ -12,6 +12,7 @@ const BIG_TEXT_VALUE = 100;
 interface Args {
   color: string;
   value: number;
+  textColor?: string;
 }
 
 export const drawPinOnCanvas = (
@@ -42,7 +43,7 @@ export const getTextPosition = (textWidth: number, textHeight: number): Point =>
 });
 
 export const drawNumberOnCanvas = (
-  { value }: Pick<Args, 'value'>,
+  { value, textColor }: Pick<Args, 'value' | 'textColor'>,
   ctx: CanvasRenderingContext2D,
 ): void => {
   const text = `${value}`;
@@ -53,7 +54,7 @@ export const drawNumberOnCanvas = (
   const textHeight = textMetrics.fontBoundingBoxAscent + textMetrics.fontBoundingBoxDescent;
   const { x, y } = getTextPosition(textWidth, textHeight);
 
-  ctx.fillStyle = 'white';
+  ctx.fillStyle = textColor || TEXT_COLOR;
   ctx.textBaseline = 'top';
   ctx.font = `${fontSize}px ${DEFAULT_FONT_FAMILY}`;
   ctx.fillText(text, x, y);
@@ -70,7 +71,7 @@ export const getCanvasIcon = (
 
   drawPinOnCanvas(args, ctx);
   if (args?.value !== undefined) {
-    drawNumberOnCanvas({ value: args.value }, ctx);
+    drawNumberOnCanvas({ value: args.value, textColor: args?.textColor }, ctx);
   }
 
   return canvas;
diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts
index f45b6ea6..4e11dc14 100644
--- a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts
+++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitiesFeatures.ts
@@ -11,10 +11,12 @@ export const getBioEntitiesFeatures = (
     pointToProjection,
     type,
     entityNumber,
+    activeIds,
   }: {
     pointToProjection: UsePointToProjectionResult;
     type: PinType;
     entityNumber: EntityNumber;
+    activeIds: (string | number)[];
   },
 ): Feature[] => {
   return bioEntites.map(bioEntity =>
@@ -23,6 +25,7 @@ export const getBioEntitiesFeatures = (
       type,
       // pin's index number
       value: entityNumber?.[bioEntity.elementId],
+      isActive: activeIds.includes(bioEntity.id),
     }),
   );
 };
diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.ts
index 3290b1a0..cdb44412 100644
--- a/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.ts
+++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getBioEntitySingleFeature.ts
@@ -1,27 +1,41 @@
-import { PINS_COLORS } from '@/constants/canvas';
+import { PINS_COLORS, TEXT_COLOR } from '@/constants/canvas';
 import { BioEntity } from '@/types/models';
 import { PinType } from '@/types/pin';
+import { addAlphaToHexString } from '@/utils/convert/addAlphaToHexString';
 import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection';
 import { Feature } from 'ol';
 import { getPinFeature } from './getPinFeature';
 import { getPinStyle } from './getPinStyle';
 
+const INACTIVE_ELEMENT_OPACITY = 0.5;
+
 export const getBioEntitySingleFeature = (
   bioEntity: BioEntity,
   {
     pointToProjection,
     type,
     value,
+    isActive,
   }: {
     pointToProjection: UsePointToProjectionResult;
     type: PinType;
     value: number;
+    isActive: boolean;
   },
 ): Feature => {
+  const color = isActive
+    ? PINS_COLORS[type]
+    : addAlphaToHexString(PINS_COLORS.bioEntity, INACTIVE_ELEMENT_OPACITY);
+
+  const textColor = isActive
+    ? TEXT_COLOR
+    : addAlphaToHexString(TEXT_COLOR, INACTIVE_ELEMENT_OPACITY);
+
   const feature = getPinFeature(bioEntity, pointToProjection);
   const style = getPinStyle({
-    color: PINS_COLORS[type],
+    color,
     value,
+    textColor,
   });
 
   feature.setStyle(style);
diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinStyle.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinStyle.ts
index 77ee5c7f..3ee13cb9 100644
--- a/src/components/Map/MapViewer/utils/config/pinsLayer/getPinStyle.ts
+++ b/src/components/Map/MapViewer/utils/config/pinsLayer/getPinStyle.ts
@@ -4,7 +4,15 @@ import Icon from 'ol/style/Icon';
 import Style from 'ol/style/Style';
 import { getCanvasIcon } from '../getCanvasIcon';
 
-export const getPinStyle = ({ value, color }: { value?: number; color: string }): Style =>
+export const getPinStyle = ({
+  value,
+  color,
+  textColor,
+}: {
+  value?: number;
+  color: string;
+  textColor?: string;
+}): Style =>
   new Style({
     image: new Icon({
       displacement: [ZERO, PIN_SIZE.height],
@@ -13,6 +21,7 @@ export const getPinStyle = ({ value, color }: { value?: number; color: string })
       img: getCanvasIcon({
         color,
         value,
+        textColor,
       }),
     }),
   });
diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts
index b7b05f6c..29e095ba 100644
--- a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts
+++ b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts
@@ -1,7 +1,14 @@
 /* eslint-disable no-magic-numbers */
-import { searchedBioEntitesSelectorOfCurrentMap } from '@/redux/bioEntity/bioEntity.selectors';
-import { searchedChemicalsBioEntitesOfCurrentMapSelector } from '@/redux/chemicals/chemicals.selectors';
-import { searchedDrugsBioEntitesOfCurrentMapSelector } from '@/redux/drugs/drugs.selectors';
+import {
+  allBioEntitesSelectorOfCurrentMap,
+  allVisibleBioEntitiesIdsSelector
+} from '@/redux/bioEntity/bioEntity.selectors';
+import {
+  allChemicalsBioEntitesOfCurrentMapSelector
+} from '@/redux/chemicals/chemicals.selectors';
+import {
+  allDrugsBioEntitesOfCurrentMapSelector
+} from '@/redux/drugs/drugs.selectors';
 import { entityNumberDataSelector } from '@/redux/entityNumber/entityNumber.selectors';
 import { markersPinsOfCurrentMapDataSelector } from '@/redux/markers/markers.selectors';
 import { usePointToProjection } from '@/utils/map/usePointToProjection';
@@ -16,9 +23,10 @@ import { getMarkersFeatures } from './getMarkersFeatures';
 
 export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>> => {
   const pointToProjection = usePointToProjection();
-  const contentBioEntites = useSelector(searchedBioEntitesSelectorOfCurrentMap);
-  const chemicalsBioEntities = useSelector(searchedChemicalsBioEntitesOfCurrentMapSelector);
-  const drugsBioEntities = useSelector(searchedDrugsBioEntitesOfCurrentMapSelector);
+  const activeIds = useSelector(allVisibleBioEntitiesIdsSelector);
+  const contentBioEntites = useSelector(allBioEntitesSelectorOfCurrentMap);
+  const chemicalsBioEntities = useSelector(allChemicalsBioEntitesOfCurrentMapSelector);
+  const drugsBioEntities = useSelector(allDrugsBioEntitesOfCurrentMapSelector);
   const markersEntities = useSelector(markersPinsOfCurrentMapDataSelector);
   const entityNumber = useSelector(entityNumberDataSelector);
 
@@ -29,16 +37,19 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>
           pointToProjection,
           type: 'bioEntity',
           entityNumber,
+          activeIds,
         }),
         getBioEntitiesFeatures(chemicalsBioEntities, {
           pointToProjection,
           type: 'chemicals',
           entityNumber,
+          activeIds,
         }),
         getBioEntitiesFeatures(drugsBioEntities, {
           pointToProjection,
           type: 'drugs',
           entityNumber,
+          activeIds,
         }),
         getMarkersFeatures(markersEntities, { pointToProjection }),
       ].flat(),
@@ -49,6 +60,7 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>
       pointToProjection,
       markersEntities,
       entityNumber,
+      activeIds,
     ],
   );
 
diff --git a/src/components/Map/MapViewer/utils/listeners/pinIconClick/useHandlePinIconClick.ts b/src/components/Map/MapViewer/utils/listeners/pinIconClick/useHandlePinIconClick.ts
new file mode 100644
index 00000000..b7be9f91
--- /dev/null
+++ b/src/components/Map/MapViewer/utils/listeners/pinIconClick/useHandlePinIconClick.ts
@@ -0,0 +1,38 @@
+import { allBioEntitiesElementsIdsSelector } from '@/redux/bioEntity/bioEntity.selectors';
+import { currentSelectedSearchElement } from '@/redux/drawer/drawer.selectors';
+import { selectTab } from '@/redux/drawer/drawer.slice';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { PluginsEventBus as EventBus } from '@/services/pluginsManager/pluginsEventBus';
+import { ClickedPinIcon } from '@/services/pluginsManager/pluginsEventBus/pluginsEventBus.types';
+import isUUID from 'is-uuid';
+import { useCallback, useEffect } from 'react';
+
+export const useHandlePinIconClick = (): void => {
+  const dispatch = useAppDispatch();
+  const currentTab = useAppSelector(currentSelectedSearchElement);
+  const idsTabs = useAppSelector(allBioEntitiesElementsIdsSelector);
+
+  const onPinIconClick = useCallback(
+    ({ id }: ClickedPinIcon): void => {
+      const newTab = idsTabs[id];
+      const isTabAlreadySelected = newTab === currentTab;
+      const isMarker = isUUID.anyNonNil(`${id}`);
+
+      if (!newTab || isTabAlreadySelected || isMarker) {
+        return;
+      }
+
+      dispatch(selectTab(idsTabs[id]));
+    },
+    [idsTabs, dispatch, currentTab],
+  );
+
+  useEffect(() => {
+    EventBus.addLocalListener('onPinIconClick', onPinIconClick);
+
+    return () => {
+      EventBus.removeLocalListener('onPinIconClick', onPinIconClick);
+    };
+  }, [onPinIconClick]);
+};
diff --git a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts
index 86e88297..187fc502 100644
--- a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts
+++ b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts
@@ -1,4 +1,6 @@
 import { DEFAULT_ZOOM, OPTIONS } from '@/constants/map';
+import { searchDistanceValSelector } from '@/redux/configuration/configuration.selectors';
+import { resultDrawerOpen } from '@/redux/drawer/drawer.selectors';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import {
   mapDataLastZoomValue,
@@ -14,12 +16,11 @@ import { Pixel } from 'ol/pixel';
 import { useEffect, useRef } from 'react';
 import { useSelector } from 'react-redux';
 import { useDebouncedCallback } from 'use-debounce';
-import { searchDistanceValSelector } from '@/redux/configuration/configuration.selectors';
-import { resultDrawerOpen } from '@/redux/drawer/drawer.selectors';
 import { onMapRightClick } from './mapRightClick/onMapRightClick';
 import { onMapSingleClick } from './mapSingleClick/onMapSingleClick';
 import { onMapPositionChange } from './onMapPositionChange';
 import { onPointerMove } from './onPointerMove';
+import { useHandlePinIconClick } from './pinIconClick/useHandlePinIconClick';
 
 interface UseOlMapListenersInput {
   view: View;
@@ -36,6 +37,7 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput)
   const coordinate = useRef<Coordinate>([]);
   const pixel = useRef<Pixel>([]);
   const dispatch = useAppDispatch();
+  useHandlePinIconClick(modelId);
 
   const handleRightClick = useDebouncedCallback(
     onMapRightClick(mapSize, modelId, dispatch),
diff --git a/src/constants/canvas.ts b/src/constants/canvas.ts
index 0bcb2708..00eeeff6 100644
--- a/src/constants/canvas.ts
+++ b/src/constants/canvas.ts
@@ -11,7 +11,7 @@ export const PIN_SIZE = {
 
 export const PINS_COLORS: Record<PinType, string> = {
   drugs: '#F48C41',
-  chemicals: '#640CE3',
+  chemicals: '#008325',
   bioEntity: '#106AD7',
 };
 
@@ -22,4 +22,6 @@ export const PINS_COLOR_WITH_NONE: Record<PinTypeWithNone, string> = {
 
 export const LINE_COLOR = '#00AAFF';
 
+export const TEXT_COLOR = '#FFFFFF';
+
 export const LINE_WIDTH = 6;
diff --git a/src/redux/bioEntity/bioEntity.selectors.ts b/src/redux/bioEntity/bioEntity.selectors.ts
index 8bf9877d..f3e8c2d2 100644
--- a/src/redux/bioEntity/bioEntity.selectors.ts
+++ b/src/redux/bioEntity/bioEntity.selectors.ts
@@ -1,10 +1,12 @@
 import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
 import { rootSelector } from '@/redux/root/root.selectors';
+import { ElementIdTabObj } from '@/types/elements';
 import { MultiSearchData } from '@/types/fetchDataState';
 import { BioEntity, BioEntityContent, MapModel } from '@/types/models';
 import { createSelector } from '@reduxjs/toolkit';
 import {
   allChemicalsBioEntitesOfAllMapsSelector,
+  allChemicalsIdTabSelectorOfCurrentMap,
   searchedChemicalsBioEntitesOfCurrentMapSelector,
 } from '../chemicals/chemicals.selectors';
 import { currentSelectedBioEntityIdSelector } from '../contextMenu/contextMenu.selector';
@@ -14,6 +16,7 @@ import {
 } from '../drawer/drawer.selectors';
 import {
   allDrugsBioEntitesOfAllMapsSelector,
+  allDrugsIdTabSelectorOfCurrentMap,
   searchedDrugsBioEntitesOfCurrentMapSelector,
 } from '../drugs/drugs.selectors';
 import { currentModelIdSelector, modelsDataSelector } from '../models/models.selectors';
@@ -92,6 +95,44 @@ export const searchedBioEntitesSelectorOfCurrentMap = createSelector(
   },
 );
 
+export const allBioEntitesSelectorOfCurrentMap = createSelector(
+  bioEntitySelector,
+  currentModelIdSelector,
+  (bioEntities, currentModelId): BioEntity[] => {
+    if (!bioEntities) {
+      return [];
+    }
+
+    return (bioEntities?.data || [])
+      .map(({ data }) => data || [])
+      .flat()
+      .filter(({ bioEntity }) => bioEntity.model === currentModelId)
+      .map(({ bioEntity }) => bioEntity);
+  },
+);
+
+export const allBioEntitesIdTabSelectorOfCurrentMap = createSelector(
+  bioEntitySelector,
+  currentModelIdSelector,
+  (bioEntities, currentModelId): ElementIdTabObj => {
+    if (!bioEntities) {
+      return {};
+    }
+
+    return Object.fromEntries(
+      (bioEntities?.data || [])
+        .map(({ data, searchQueryElement }): [typeof data, string] => [data, searchQueryElement])
+        .map(([data, tab]) =>
+          (data || [])
+            .flat()
+            .filter(({ bioEntity }) => bioEntity.model === currentModelId)
+            .map(d => [d.bioEntity.id, tab]),
+        )
+        .flat(),
+    );
+  },
+);
+
 export const numberOfBioEntitiesSelector = createSelector(
   bioEntitiesForSelectedSearchElement,
   state => (state?.data ? state.data.length : SIZE_OF_EMPTY_ARRAY),
@@ -129,6 +170,13 @@ export const allVisibleBioEntitiesSelector = createSelector(
   },
 );
 
+export const allVisibleBioEntitiesIdsSelector = createSelector(
+  allVisibleBioEntitiesSelector,
+  (elements): (string | number)[] => {
+    return elements.map(e => e.id);
+  },
+);
+
 export const allContentBioEntitesSelectorOfAllMaps = createSelector(
   bioEntitySelector,
   (bioEntities): BioEntity[] => {
@@ -152,6 +200,19 @@ export const allBioEntitiesSelector = createSelector(
   },
 );
 
+export const allBioEntitiesElementsIdsSelector = createSelector(
+  allBioEntitesIdTabSelectorOfCurrentMap,
+  allChemicalsIdTabSelectorOfCurrentMap,
+  allDrugsIdTabSelectorOfCurrentMap,
+  (content, chemicals, drugs): ElementIdTabObj => {
+    return {
+      ...content,
+      ...chemicals,
+      ...drugs,
+    };
+  },
+);
+
 export const currentDrawerBioEntitySelector = createSelector(
   allBioEntitiesSelector,
   currentSearchedBioEntityId,
diff --git a/src/redux/chemicals/chemicals.selectors.ts b/src/redux/chemicals/chemicals.selectors.ts
index bb8d4aee..d8394dc4 100644
--- a/src/redux/chemicals/chemicals.selectors.ts
+++ b/src/redux/chemicals/chemicals.selectors.ts
@@ -1,5 +1,6 @@
 import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
 import { rootSelector } from '@/redux/root/root.selectors';
+import { ElementId, ElementIdTabObj, Tab } from '@/types/elements';
 import { MultiSearchData } from '@/types/fetchDataState';
 import { BioEntity, Chemical } from '@/types/models';
 import { createSelector } from '@reduxjs/toolkit';
@@ -41,6 +42,47 @@ export const searchedChemicalsBioEntitesOfCurrentMapSelector = createSelector(
   },
 );
 
+export const allChemicalsBioEntitesOfCurrentMapSelector = createSelector(
+  chemicalsSelector,
+  currentModelIdSelector,
+  (chemicalsState, currentModelId): BioEntity[] => {
+    return (chemicalsState?.data || [])
+      .map(({ data }) => data || [])
+      .flat()
+      .map(({ targets }) => targets.map(({ targetElements }) => targetElements))
+      .flat()
+      .flat()
+      .filter(bioEntity => bioEntity.model === currentModelId);
+  },
+);
+
+export const allChemicalsIdTabSelectorOfCurrentMap = createSelector(
+  chemicalsSelector,
+  currentModelIdSelector,
+  (chemicalsState, currentModelId): ElementIdTabObj => {
+    if (!chemicalsState) {
+      return {};
+    }
+
+    return Object.fromEntries(
+      (chemicalsState?.data || [])
+        .map(({ data, searchQueryElement }): [typeof data, string] => [data, searchQueryElement])
+        .map(([data, tab]) =>
+          (data || []).map(({ targets }): [ElementId, Tab][] =>
+            targets
+              .map(({ targetElements }) => targetElements)
+              .flat()
+              .flat()
+              .filter(bioEntity => bioEntity.model === currentModelId)
+              .map(bioEntity => [bioEntity.id, tab]),
+          ),
+        )
+        .flat()
+        .flat(),
+    );
+  },
+);
+
 export const allChemicalsBioEntitesOfAllMapsSelector = createSelector(
   chemicalsSelector,
   (chemicalsState): BioEntity[] => {
diff --git a/src/redux/drugs/drugs.selectors.ts b/src/redux/drugs/drugs.selectors.ts
index f5c74de2..df2b10ae 100644
--- a/src/redux/drugs/drugs.selectors.ts
+++ b/src/redux/drugs/drugs.selectors.ts
@@ -1,5 +1,6 @@
 import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
 import { rootSelector } from '@/redux/root/root.selectors';
+import { ElementId, ElementIdTabObj, Tab } from '@/types/elements';
 import { MultiSearchData } from '@/types/fetchDataState';
 import { BioEntity, Drug } from '@/types/models';
 import { createSelector } from '@reduxjs/toolkit';
@@ -55,6 +56,47 @@ export const searchedDrugsBioEntitesOfCurrentMapSelector = createSelector(
   },
 );
 
+export const allDrugsBioEntitesOfCurrentMapSelector = createSelector(
+  drugsSelector,
+  currentModelIdSelector,
+  (drugsState, currentModelId): BioEntity[] => {
+    return (drugsState?.data || [])
+      .map(({ data }) => data || [])
+      .flat()
+      .map(({ targets }) => targets.map(({ targetElements }) => targetElements))
+      .flat()
+      .flat()
+      .filter(bioEntity => bioEntity.model === currentModelId);
+  },
+);
+
+export const allDrugsIdTabSelectorOfCurrentMap = createSelector(
+  drugsSelector,
+  currentModelIdSelector,
+  (drugsState, currentModelId): ElementIdTabObj => {
+    if (!drugsState) {
+      return {};
+    }
+
+    return Object.fromEntries(
+      (drugsState?.data || [])
+        .map(({ data, searchQueryElement }): [typeof data, string] => [data, searchQueryElement])
+        .map(([data, tab]) =>
+          (data || []).map(({ targets }): [ElementId, Tab][] =>
+            targets
+              .map(({ targetElements }) => targetElements)
+              .flat()
+              .flat()
+              .filter(bioEntity => bioEntity.model === currentModelId)
+              .map(bioEntity => [bioEntity.id, tab]),
+          ),
+        )
+        .flat()
+        .flat(),
+    );
+  },
+);
+
 export const allDrugsBioEntitesOfAllMapsSelector = createSelector(
   drugsSelector,
   (drugsState): BioEntity[] => {
diff --git a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts
index e3491132..54310481 100644
--- a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts
+++ b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.constants.ts
@@ -30,3 +30,5 @@ export const ALLOWED_PLUGINS_EVENTS = Object.values(PLUGINS_EVENTS).flatMap(obj
 );
 
 export const LISTENER_NOT_FOUND = -1;
+
+export const LOCAL_LISTENER_ID = 'local';
diff --git a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts
index 9ff0bbce..5ce80301 100644
--- a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts
+++ b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.ts
@@ -1,6 +1,12 @@
 /* eslint-disable no-magic-numbers */
 import { CreatedOverlay, MapOverlay } from '@/types/models';
 import { showToast } from '@/utils/showToast';
+import { ERROR_INVALID_EVENT_TYPE, ERROR_PLUGIN_CRASH } from '../errorMessages';
+import {
+  ALLOWED_PLUGINS_EVENTS,
+  LISTENER_NOT_FOUND,
+  LOCAL_LISTENER_ID,
+} from './pluginsEventBus.constants';
 import type {
   CenteredCoordinates,
   ClickedBioEntity,
@@ -13,8 +19,6 @@ import type {
   SearchData,
   ZoomChanged,
 } from './pluginsEventBus.types';
-import { ALLOWED_PLUGINS_EVENTS, LISTENER_NOT_FOUND } from './pluginsEventBus.constants';
-import { ERROR_INVALID_EVENT_TYPE, ERROR_PLUGIN_CRASH } from '../errorMessages';
 
 export function dispatchEvent(type: 'onPluginUnload', data: PluginUnloaded): void;
 export function dispatchEvent(type: 'onAddDataOverlay', createdOverlay: CreatedOverlay): void;
@@ -67,6 +71,10 @@ export const PluginsEventBus: PluginsEventBusType = {
     });
   },
 
+  addLocalListener: (type: Events, callback: (data: unknown) => void) => {
+    PluginsEventBus.addListener(LOCAL_LISTENER_ID, LOCAL_LISTENER_ID, type, callback);
+  },
+
   removeListener: (hash: string, type: Events, callback: unknown) => {
     const eventIndex = PluginsEventBus.events.findIndex(
       event => event.hash === hash && event.type === type && event.callback === callback,
@@ -79,6 +87,10 @@ export const PluginsEventBus: PluginsEventBusType = {
     }
   },
 
+  removeLocalListener: (type: Events, callback: unknown) => {
+    PluginsEventBus.removeListener(LOCAL_LISTENER_ID, type, callback);
+  },
+
   removeAllListeners: (hash: string) => {
     PluginsEventBus.events = PluginsEventBus.events.filter(event => event.hash !== hash);
   },
diff --git a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts
index fa6d70e4..ff61d186 100644
--- a/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts
+++ b/src/services/pluginsManager/pluginsEventBus/pluginsEventBus.types.ts
@@ -102,7 +102,9 @@ export type PluginsEventBusType = {
     type: Events,
     callback: (data: unknown) => void,
   ) => void;
+  addLocalListener: <T>(type: Events, callback: (data: T) => void) => void;
   removeListener: (hash: string, type: Events, callback: unknown) => void;
+  removeLocalListener: <T>(type: Events, callback: T) => void;
   removeAllListeners: (hash: string) => void;
   dispatchEvent: typeof dispatchEvent;
 };
diff --git a/src/types/elements.ts b/src/types/elements.ts
new file mode 100644
index 00000000..0bcc0967
--- /dev/null
+++ b/src/types/elements.ts
@@ -0,0 +1,4 @@
+export type ElementId = string | number;
+export type Tab = string;
+export type ElementIdTab = [ElementId, Tab];
+export type ElementIdTabObj = Record<ElementId, Tab>;
-- 
GitLab