From cbdc5d730e33bb650597c7385fa205b2d190ee9f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl>
Date: Thu, 14 Nov 2024 09:52:54 +0100
Subject: [PATCH] feature(vector-map): implement context menu

---
 .../MapViewerVector.constants.ts              |  2 +
 .../clickHandleReaction.test.ts}              |  8 +-
 .../clickHandleReaction.ts}                   |  2 +-
 .../leftClickHandleAlias.test.ts              |  4 +-
 .../mouseLeftClick}/leftClickHandleAlias.ts   |  0
 .../mouseLeftClick}/onMapLeftClick.test.ts    | 16 ++--
 .../mouseLeftClick}/onMapLeftClick.ts         |  8 +-
 .../mouseRightClick/onMapRightClick.test.ts   | 95 +++++++++++++++++++
 .../mouseRightClick/onMapRightClick.ts        | 56 +++++++++++
 .../rightClickHandleAlias.test.ts             | 28 ++++++
 .../mouseRightClick/rightClickHandleAlias.ts  | 16 ++++
 .../listeners/useOlMapVectorListeners.test.ts |  4 +-
 .../listeners/useOlMapVectorListeners.ts      | 46 ++++++++-
 .../reactionsLayer/useOlMapReactionsLayer.ts  | 15 +--
 .../utils/listeners/useOlMapListeners.ts      |  4 +-
 src/models/fixtures/modelElementFixture.ts    |  9 ++
 src/models/modelElementSchema.ts              |  4 +-
 src/redux/bioEntity/bioEntity.reducers.ts     | 16 ++++
 src/redux/bioEntity/bioEntity.slice.ts        |  5 +-
 .../bioEntity/mapModelElementToBioEntity.ts   | 51 ++++++++++
 20 files changed, 353 insertions(+), 36 deletions(-)
 rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick/leftClickHandleReaction.test.ts => mouseClick/clickHandleReaction.test.ts} (89%)
 rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick/leftClickHandleReaction.ts => mouseClick/clickHandleReaction.ts} (98%)
 rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick => mouseClick/mouseLeftClick}/leftClickHandleAlias.test.ts (92%)
 rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick => mouseClick/mouseLeftClick}/leftClickHandleAlias.ts (100%)
 rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick => mouseClick/mouseLeftClick}/onMapLeftClick.test.ts (87%)
 rename src/components/Map/MapViewer/MapViewerVector/listeners/{mapLeftClick => mouseClick/mouseLeftClick}/onMapLeftClick.ts (87%)
 create mode 100644 src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts
 create mode 100644 src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts
 create mode 100644 src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts
 create mode 100644 src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts
 create mode 100644 src/models/fixtures/modelElementFixture.ts
 create mode 100644 src/utils/bioEntity/mapModelElementToBioEntity.ts

diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts
index 4ac8a6fb..9a036ac1 100644
--- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants.ts
@@ -1,5 +1,7 @@
 import { Color, ShapeRelAbs, ShapeRelAbsBezierPoint } from '@/types/models';
 
+export const VECTOR_MAP_LAYER_TYPE = 'vectorMapLayer';
+
 export const WHITE_COLOR: Color = {
   alpha: 255,
   rgb: 16777215,
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.test.ts
similarity index 89%
rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.test.ts
rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.test.ts
index 1bca3522..f6aab10a 100644
--- a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.test.ts
@@ -1,5 +1,4 @@
 /* eslint-disable no-magic-numbers */
-import { leftClickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction';
 import { openReactionDrawerById, selectTab } from '@/redux/drawer/drawer.slice';
 import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus';
 import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds';
@@ -10,12 +9,13 @@ import { apiPath } from '@/redux/apiPath';
 import { HttpStatusCode } from 'axios';
 import { bioEntityFixture } from '@/models/fixtures/bioEntityFixture';
 import { FEATURE_TYPE } from '@/constants/features';
+import { clickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction';
 
 const mockedAxiosClient = mockNetworkNewAPIResponse();
 jest.mock('../../../../../../services/pluginsManager/map/triggerSearch/searchFitBounds');
 const eventBusDispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent');
 
-describe('leftClickHandleReaction', () => {
+describe('clickHandleReaction', () => {
   let dispatch: jest.Mock;
   let modelId = 1;
   let reactionId = 1;
@@ -54,7 +54,7 @@ describe('leftClickHandleReaction', () => {
         )
         .reply(HttpStatusCode.Ok, bioEntityFixture);
     });
-    await leftClickHandleReaction(dispatch, hasFitBounds)(feature, modelId);
+    await clickHandleReaction(dispatch, hasFitBounds)(feature, modelId);
     expect(dispatch).toHaveBeenCalledTimes(4);
     expect(dispatch).toHaveBeenCalledWith(openReactionDrawerById(reactionId));
     expect(dispatch).toHaveBeenCalledWith(selectTab(''));
@@ -68,7 +68,7 @@ describe('leftClickHandleReaction', () => {
       unwrap: jest.fn().mockResolvedValue(mockBioEntities),
     }));
 
-    await leftClickHandleReaction(dispatch, false)(feature, modelId);
+    await clickHandleReaction(dispatch, false)(feature, modelId);
 
     expect(searchFitBounds).not.toHaveBeenCalled();
   });
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.ts
similarity index 98%
rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.ts
rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.ts
index 1481cee4..676d0b49 100644
--- a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction.ts
@@ -15,7 +15,7 @@ import { getBioEntitiesIdsFromReaction } from '@/components/Map/MapViewer/utils/
 import { FEATURE_TYPE } from '@/constants/features';
 
 /* prettier-ignore */
-export const leftClickHandleReaction =
+export const clickHandleReaction =
   (dispatch: AppDispatch, hasFitBounds = false) =>
     async (feature: FeatureLike, modelId: number): Promise<void> => {
       const id = feature.get('id');
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.test.ts
similarity index 92%
rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias.test.ts
rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.test.ts
index 82b8dfc2..46ca1e40 100644
--- a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.test.ts
@@ -1,12 +1,12 @@
 /* eslint-disable no-magic-numbers */
-import { leftClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias';
+import { leftClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias';
 import { openBioEntityDrawerById, selectTab } from '@/redux/drawer/drawer.slice';
 import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus';
 import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds';
 import { Feature } from 'ol';
 import { FEATURE_TYPE } from '@/constants/features';
 
-jest.mock('../../../../../../services/pluginsManager/map/triggerSearch/searchFitBounds');
+jest.mock('../../../../../../../services/pluginsManager/map/triggerSearch/searchFitBounds');
 const eventBusDispatchEventSpy = jest.spyOn(PluginsEventBus, 'dispatchEvent');
 
 describe('leftClickHandleAlias', () => {
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.ts
similarity index 100%
rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias.ts
rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias.ts
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts
similarity index 87%
rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.test.ts
rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts
index e7fa9bc7..166b70df 100644
--- a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts
@@ -5,16 +5,16 @@ import { resetReactionsData } from '@/redux/reactions/reactions.slice';
 import { clearBioEntitiesData } from '@/redux/bioEntity/bioEntity.slice';
 import { handleFeaturesClick } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick';
 import Map from 'ol/Map';
-import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick';
+import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick';
 import { Comment } from '@/types/models';
 import { Layer } from 'ol/layer';
 import SimpleGeometry from 'ol/geom/SimpleGeometry';
 import { Feature } from 'ol';
 import { FEATURE_TYPE } from '@/constants/features';
 import * as leftClickHandleAlias from './leftClickHandleAlias';
-import * as leftClickHandleReaction from './leftClickHandleReaction';
+import * as clickHandleReaction from '../clickHandleReaction';
 
-jest.mock('../../../utils/listeners/mapSingleClick/handleFeaturesClick', () => ({
+jest.mock('../../../../utils/listeners/mapSingleClick/handleFeaturesClick', () => ({
   handleFeaturesClick: jest.fn(),
 }));
 jest.mock('./leftClickHandleAlias', () => ({
@@ -22,11 +22,11 @@ jest.mock('./leftClickHandleAlias', () => ({
   ...jest.requireActual('./leftClickHandleAlias'),
 }));
 const leftClickHandleAliasSpy = jest.spyOn(leftClickHandleAlias, 'leftClickHandleAlias');
-jest.mock('./leftClickHandleReaction', () => ({
+jest.mock('../clickHandleReaction', () => ({
   __esModule: true,
-  ...jest.requireActual('./leftClickHandleReaction'),
+  ...jest.requireActual('../clickHandleReaction'),
 }));
-const leftClickHandleReactionSpy = jest.spyOn(leftClickHandleReaction, 'leftClickHandleReaction');
+const clickHandleReactionSpy = jest.spyOn(clickHandleReaction, 'clickHandleReaction');
 
 describe('onMapLeftClick', () => {
   const modelId = 1;
@@ -89,7 +89,7 @@ describe('onMapLeftClick', () => {
     expect(leftClickHandleAliasSpy).toHaveBeenCalledWith(dispatch);
   });
 
-  it('calls leftClickHandleReaction if feature type is REACTION', async () => {
+  it('calls clickHandleReaction if feature type is REACTION', async () => {
     const mockBioEntities = [{ id: 1, name: 'BioEntity 1' }];
     const dispatch = jest.fn(() => ({
       unwrap: jest.fn().mockResolvedValue(mockBioEntities),
@@ -108,6 +108,6 @@ describe('onMapLeftClick', () => {
       comments,
     )(event, mapInstance);
 
-    expect(leftClickHandleReactionSpy).toHaveBeenCalledWith(dispatch);
+    expect(clickHandleReactionSpy).toHaveBeenCalledWith(dispatch);
   });
 });
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts
similarity index 87%
rename from src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.ts
rename to src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts
index aa481cd3..7b047dda 100644
--- a/src/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts
@@ -8,12 +8,12 @@ import { latLngToPoint } from '@/utils/map/latLngToPoint';
 import { FeatureLike } from 'ol/Feature';
 import { closeDrawer } from '@/redux/drawer/drawer.slice';
 import { clearBioEntitiesData } from '@/redux/bioEntity/bioEntity.slice';
-import { leftClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleAlias';
+import { leftClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/leftClickHandleAlias';
 import { handleFeaturesClick } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleFeaturesClick';
 import { resetReactionsData } from '@/redux/reactions/reactions.slice';
-import { leftClickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/leftClickHandleReaction';
 import { handleDataReset } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset';
 import { FEATURE_TYPE } from '@/constants/features';
+import { clickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction';
 
 /* prettier-ignore */
 export const onMapLeftClick =
@@ -25,7 +25,7 @@ export const onMapLeftClick =
       dispatch(updateLastClick({ coordinates: point, modelId }));
 
       let featureAtPixel: FeatureLike | undefined;
-      mapInstance.forEachFeatureAtPixel(pixel, (feature) => {
+      mapInstance.forEachFeatureAtPixel(pixel, (feature, ) => {
         if(feature.get('id') && [...Object.values(FEATURE_TYPE)].includes(feature.get('type'))) {
           featureAtPixel = feature;
           return true;
@@ -55,6 +55,6 @@ export const onMapLeftClick =
       if(type === FEATURE_TYPE.ALIAS) {
         await leftClickHandleAlias(dispatch)(featureAtPixel, modelId);
       } else if (type === FEATURE_TYPE.REACTION) {
-        await leftClickHandleReaction(dispatch)(featureAtPixel, modelId);
+        await clickHandleReaction(dispatch)(featureAtPixel, modelId);
       }
     };
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts
new file mode 100644
index 00000000..a96f6601
--- /dev/null
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.test.ts
@@ -0,0 +1,95 @@
+/* eslint-disable no-magic-numbers */
+import { updateLastRightClick } from '@/redux/map/map.slice';
+import Map from 'ol/Map';
+import { onMapRightClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick';
+import { Layer } from 'ol/layer';
+import { openContextMenu } from '@/redux/contextMenu/contextMenu.slice';
+import { Source } from 'ol/source';
+import VectorLayer from 'ol/layer/Vector';
+import VectorSource from 'ol/source/Vector';
+import { Feature } from 'ol';
+import { FEATURE_TYPE } from '@/constants/features';
+import { modelElementsFixture } from '@/models/fixtures/modelElementsFixture';
+import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
+import * as rightClickHandleAlias from './rightClickHandleAlias';
+import * as clickHandleReaction from '../clickHandleReaction';
+
+jest.mock('./rightClickHandleAlias', () => ({
+  __esModule: true,
+  ...jest.requireActual('./rightClickHandleAlias'),
+}));
+const rightClickHandleAliasSpy = jest.spyOn(rightClickHandleAlias, 'rightClickHandleAlias');
+jest.mock('../clickHandleReaction', () => ({
+  __esModule: true,
+  ...jest.requireActual('../clickHandleReaction'),
+}));
+const clickHandleReactionSpy = jest.spyOn(clickHandleReaction, 'clickHandleReaction');
+
+describe('onMapRightClick', () => {
+  const modelId = 1;
+  let mapInstance: Map;
+  const event = { coordinate: [100, 50], pixel: [200, 100] };
+  const mapSize = {
+    width: 90,
+    height: 90,
+    tileSize: 256,
+    minZoom: 2,
+    maxZoom: 9,
+  };
+  let vectorLayer: VectorLayer;
+  let vectorSource: VectorSource;
+
+  beforeEach(() => {
+    const dummyElement = document.createElement('div');
+    mapInstance = new Map({ target: dummyElement });
+    vectorSource = new VectorSource({});
+    vectorLayer = new VectorLayer({
+      source: vectorSource,
+    });
+    vectorLayer.set('type', VECTOR_MAP_LAYER_TYPE);
+    jest.clearAllMocks();
+  });
+
+  it('calls rightClickHandleAlias if feature type is ALIAS', async () => {
+    const dispatch = jest.fn();
+    const modelElement = modelElementsFixture.content[0];
+    jest.spyOn(mapInstance, 'getAllLayers').mockImplementation((): Layer<Source>[] => {
+      return [vectorLayer];
+    });
+    jest.spyOn(vectorLayer, 'isVisible').mockImplementation((): boolean => {
+      return true;
+    });
+    jest.spyOn(vectorLayer, 'getSource').mockImplementation((): VectorSource => {
+      return vectorSource;
+    });
+    jest.spyOn(vectorSource, 'getClosestFeatureToCoordinate').mockImplementation((): Feature => {
+      return new Feature({ id: modelElement.id, type: FEATURE_TYPE.ALIAS });
+    });
+    await onMapRightClick(mapSize, modelId, dispatch, [modelElement])(event, mapInstance);
+
+    expect(dispatch).toHaveBeenCalledWith(updateLastRightClick(expect.any(Object)));
+    expect(dispatch).toHaveBeenCalledWith(openContextMenu(event.pixel));
+    expect(rightClickHandleAliasSpy).toHaveBeenCalledWith(dispatch);
+  });
+
+  it('calls rightClickHandleAlias if feature type is REACTION', async () => {
+    const dispatch = jest.fn();
+    jest.spyOn(mapInstance, 'getAllLayers').mockImplementation((): Layer<Source>[] => {
+      return [vectorLayer];
+    });
+    jest.spyOn(vectorLayer, 'isVisible').mockImplementation((): boolean => {
+      return true;
+    });
+    jest.spyOn(vectorLayer, 'getSource').mockImplementation((): VectorSource => {
+      return vectorSource;
+    });
+    jest.spyOn(vectorSource, 'getClosestFeatureToCoordinate').mockImplementation((): Feature => {
+      return new Feature({ id: 1, type: FEATURE_TYPE.REACTION });
+    });
+    await onMapRightClick(mapSize, modelId, dispatch, [])(event, mapInstance);
+
+    expect(dispatch).toHaveBeenCalledWith(updateLastRightClick(expect.any(Object)));
+    expect(dispatch).toHaveBeenCalledWith(openContextMenu(event.pixel));
+    expect(clickHandleReactionSpy).toHaveBeenCalledWith(dispatch);
+  });
+});
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts
new file mode 100644
index 00000000..c4673a83
--- /dev/null
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick.ts
@@ -0,0 +1,56 @@
+import { MapSize } from '@/redux/map/map.types';
+import { AppDispatch } from '@/redux/store';
+import { Feature, Map, MapBrowserEvent } from 'ol';
+import { updateLastRightClick } from '@/redux/map/map.slice';
+import { toLonLat } from 'ol/proj';
+import { latLngToPoint } from '@/utils/map/latLngToPoint';
+import { handleDataReset } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset';
+import { FEATURE_TYPE } from '@/constants/features';
+import VectorLayer from 'ol/layer/Vector';
+import VectorSource from 'ol/source/Vector';
+import { openContextMenu } from '@/redux/contextMenu/contextMenu.slice';
+import { ModelElement } from '@/types/models';
+import { rightClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias';
+import { clickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction';
+import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
+
+/* prettier-ignore */
+export const onMapRightClick =
+  (mapSize: MapSize, modelId: number, dispatch: AppDispatch, modelElements: Array<ModelElement>) =>
+    async ({ coordinate, pixel }: Pick<MapBrowserEvent<UIEvent>, 'coordinate' | 'pixel'>, mapInstance: Map): Promise<void> => {
+
+      const [lng, lat] = toLonLat(coordinate);
+      const point = latLngToPoint([lat, lng], mapSize);
+      dispatch(updateLastRightClick({ coordinates: point, modelId }));
+
+      let foundFeature: Feature | undefined;
+      mapInstance.getAllLayers().forEach(layer => {
+        if(layer.isVisible() && layer instanceof VectorLayer) {
+          if (layer.get('type') === VECTOR_MAP_LAYER_TYPE) {
+            const source = layer.getSource();
+            if (source instanceof VectorSource) {
+              foundFeature = source.getClosestFeatureToCoordinate(coordinate, (feature) => {
+                return [FEATURE_TYPE.ALIAS, FEATURE_TYPE.REACTION].includes(feature.get('type'));
+              });
+            }
+          }
+        }
+      });
+      if(!foundFeature) {
+        return;
+      }
+      dispatch(handleDataReset);
+      dispatch(openContextMenu(pixel));
+
+      const type = foundFeature.get('type');
+      const id = foundFeature.get('id');
+      if(type === FEATURE_TYPE.ALIAS) {
+        const modelElement = modelElements.find(element => element.id === id);
+        if(!modelElement) {
+          return;
+        }
+        await rightClickHandleAlias(dispatch)(id, modelElement);
+      } else if (type === FEATURE_TYPE.REACTION) {
+        await clickHandleReaction(dispatch)(foundFeature, modelId);
+      }
+    };
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts
new file mode 100644
index 00000000..2b936519
--- /dev/null
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.test.ts
@@ -0,0 +1,28 @@
+/* eslint-disable no-magic-numbers */
+import { rightClickHandleAlias } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias';
+import { modelElementFixture } from '@/models/fixtures/modelElementFixture';
+import { setBioEntityContents } from '@/redux/bioEntity/bioEntity.slice';
+import { addNumbersToEntityNumberData } from '@/redux/entityNumber/entityNumber.slice';
+import { setCurrentSelectedBioEntityId } from '@/redux/contextMenu/contextMenu.slice';
+
+jest.mock('../../../../../../../services/pluginsManager/map/triggerSearch/searchFitBounds');
+
+describe('rightClickHandleAlias', () => {
+  let dispatch: jest.Mock;
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+    dispatch = jest.fn();
+  });
+
+  it('dispatches setBioEntityContents, addNumbersToEntityNumberData and setCurrentSelectedBioEntityId', async () => {
+    await rightClickHandleAlias(dispatch)(modelElementFixture.id, modelElementFixture);
+    expect(dispatch).toHaveBeenCalledTimes(3);
+
+    expect(dispatch).toHaveBeenCalledWith(setBioEntityContents(expect.any(Object)));
+    expect(dispatch).toHaveBeenCalledWith(
+      addNumbersToEntityNumberData([modelElementFixture.elementId]),
+    );
+    expect(dispatch).toHaveBeenCalledWith(setCurrentSelectedBioEntityId(modelElementFixture.id));
+  });
+});
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts
new file mode 100644
index 00000000..4968eea6
--- /dev/null
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/rightClickHandleAlias.ts
@@ -0,0 +1,16 @@
+import { setCurrentSelectedBioEntityId } from '@/redux/contextMenu/contextMenu.slice';
+import { AppDispatch } from '@/redux/store';
+import { ModelElement } from '@/types/models';
+import { setBioEntityContents } from '@/redux/bioEntity/bioEntity.slice';
+import { addNumbersToEntityNumberData } from '@/redux/entityNumber/entityNumber.slice';
+import { mapModelElementToBioEntity } from '@/utils/bioEntity/mapModelElementToBioEntity';
+
+/* prettier-ignore */
+export const rightClickHandleAlias =
+  (dispatch: AppDispatch) =>
+    async (id: number, modelElement: ModelElement): Promise<void> => {
+      const bioEntity = mapModelElementToBioEntity(modelElement);
+      dispatch(setBioEntityContents({ bioEntity, perfect: true }));
+      dispatch(addNumbersToEntityNumberData([bioEntity.elementId]));
+      dispatch(setCurrentSelectedBioEntityId(id));
+    };
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts
index 64691511..8b772615 100644
--- a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.test.ts
@@ -4,9 +4,9 @@ import { View } from 'ol';
 import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
 import { initialMapDataFixture, openedMapsThreeSubmapsFixture } from '@/redux/map/map.fixtures';
 import { useOlMapVectorListeners } from '@/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners';
-import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick';
+import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick';
 
-jest.mock('./mapLeftClick/onMapLeftClick', () => ({
+jest.mock('./mouseClick/mouseLeftClick/onMapLeftClick', () => ({
   __esModule: true,
   onMapLeftClick: jest.fn(),
 }));
diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts
index ab9dd6a3..2a0af8ea 100644
--- a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts
@@ -5,22 +5,29 @@ import { mapDataSizeSelector } from '@/redux/map/map.selectors';
 import { currentModelIdSelector, vectorRenderingSelector } from '@/redux/models/models.selectors';
 import { MapInstance } from '@/types/map';
 import { unByKey } from 'ol/Observable';
-import { useEffect } from 'react';
+import { useEffect, useRef } from 'react';
 import { useSelector } from 'react-redux';
 import { useDebouncedCallback } from 'use-debounce';
 import { allCommentsSelectorOfCurrentMap } from '@/redux/comment/comment.selectors';
-import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mapLeftClick/onMapLeftClick';
+import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { Coordinate } from 'ol/coordinate';
+import { Pixel } from 'ol/pixel';
+import { onMapRightClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick';
+import { modelElementsSelector } from '@/redux/modelElements/modelElements.selector';
 
-interface UseOlMapListenersInput {
+interface UseOlMapVectorListenersInput {
   mapInstance: MapInstance;
 }
 
-export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapListenersInput): void => {
+export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapVectorListenersInput): void => {
   const mapSize = useSelector(mapDataSizeSelector);
   const modelId = useSelector(currentModelIdSelector);
   const isResultDrawerOpen = useSelector(resultDrawerOpen);
+  const modelElements = useSelector(modelElementsSelector);
   const dispatch = useAppDispatch();
+  const coordinate = useRef<Coordinate>([]);
+  const pixel = useRef<Pixel>([]);
 
   const comments = useSelector(allCommentsSelectorOfCurrentMap);
   const vectorRendering = useAppSelector(vectorRenderingSelector);
@@ -31,6 +38,14 @@ export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapListenersInput)
     { leading: false },
   );
 
+  const handleRightClick = useDebouncedCallback(
+    onMapRightClick(mapSize, modelId, dispatch, modelElements?.content || []),
+    OPTIONS.clickPersistTime,
+    {
+      leading: false,
+    },
+  );
+
   useEffect(() => {
     if (!mapInstance || !vectorRendering) {
       return;
@@ -43,4 +58,27 @@ export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapListenersInput)
     // eslint-disable-next-line consistent-return
     return () => unByKey(key);
   }, [mapInstance, handleMapLeftClick, vectorRendering]);
+
+  useEffect(() => {
+    if (!mapInstance || !vectorRendering) {
+      return;
+    }
+
+    const rightClickEvent = (e: MouseEvent): Promise<void> | undefined => {
+      e.preventDefault();
+
+      coordinate.current = mapInstance.getEventCoordinate(e);
+      pixel.current = mapInstance.getEventPixel(e);
+
+      return handleRightClick(
+        { coordinate: coordinate.current, pixel: pixel.current },
+        mapInstance,
+      );
+    };
+
+    mapInstance.getViewport().addEventListener('contextmenu', rightClickEvent);
+
+    // eslint-disable-next-line consistent-return
+    return () => mapInstance.getViewport().removeEventListener('contextmenu', rightClickEvent);
+  }, [mapInstance, handleRightClick, vectorRendering]);
 };
diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts
index e2d4fca4..00ff8cb8 100644
--- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts
+++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts
@@ -28,6 +28,7 @@ import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils
 import Reaction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction';
 import { newReactionsDataSelector } from '@/redux/newReactions/newReactions.selectors';
 import { getNewReactions } from '@/redux/newReactions/newReactions.thunks';
+import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants';
 
 export const useOlMapReactionsLayer = ({
   mapInstance,
@@ -182,11 +183,11 @@ export const useOlMapReactionsLayer = ({
     });
   }, [features]);
 
-  return useMemo(
-    () =>
-      new VectorLayer({
-        source: vectorSource,
-      }),
-    [vectorSource],
-  );
+  return useMemo(() => {
+    const vectorLayer = new VectorLayer({
+      source: vectorSource,
+    });
+    vectorLayer.set('type', VECTOR_MAP_LAYER_TYPE);
+    return vectorLayer;
+  }, [vectorSource]);
 };
diff --git a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts
index d10e31d5..e7df6b03 100644
--- a/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts
+++ b/src/components/Map/MapViewer/utils/listeners/useOlMapListeners.ts
@@ -126,7 +126,7 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput)
   }, [mapInstance, handleMapSingleClick, vectorRendering]);
 
   useEffect(() => {
-    if (!mapInstance) {
+    if (!mapInstance || vectorRendering) {
       return;
     }
 
@@ -143,5 +143,5 @@ export const useOlMapListeners = ({ view, mapInstance }: UseOlMapListenersInput)
 
     // eslint-disable-next-line consistent-return
     return () => mapInstance.getViewport().removeEventListener('contextmenu', rightClickEvent);
-  }, [mapInstance, handleRightClick]);
+  }, [mapInstance, handleRightClick, vectorRendering]);
 };
diff --git a/src/models/fixtures/modelElementFixture.ts b/src/models/fixtures/modelElementFixture.ts
new file mode 100644
index 00000000..0936dceb
--- /dev/null
+++ b/src/models/fixtures/modelElementFixture.ts
@@ -0,0 +1,9 @@
+import { ZOD_SEED } from '@/constants';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createFixture } from 'zod-fixture';
+import { modelElementSchema } from '@/models/modelElementSchema';
+
+export const modelElementFixture = createFixture(modelElementSchema, {
+  seed: ZOD_SEED,
+  array: { min: 3, max: 3 },
+});
diff --git a/src/models/modelElementSchema.ts b/src/models/modelElementSchema.ts
index 98f28825..b95b6f6a 100644
--- a/src/models/modelElementSchema.ts
+++ b/src/models/modelElementSchema.ts
@@ -7,9 +7,11 @@ import { glyphSchema } from '@/models/glyphSchema';
 
 export const modelElementSchema = z.object({
   id: z.number(),
-  model: z.number().nullable(),
+  model: z.number(),
   glyph: glyphSchema.nullable(),
   submodel: submodelSchema.nullable(),
+  compartment: z.number().nullable(),
+  elementId: z.string(),
   x: z.number(),
   y: z.number(),
   z: z.number(),
diff --git a/src/redux/bioEntity/bioEntity.reducers.ts b/src/redux/bioEntity/bioEntity.reducers.ts
index 3f68a154..deadfae3 100644
--- a/src/redux/bioEntity/bioEntity.reducers.ts
+++ b/src/redux/bioEntity/bioEntity.reducers.ts
@@ -1,6 +1,7 @@
 import { DEFAULT_ERROR } from '@/constants/errors';
 import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit';
 import { getBioEntityById } from '@/redux/bioEntity/thunks/getBioEntity';
+import { BioEntityContent } from '@/types/models';
 import { BIOENTITY_SUBMAP_CONNECTIONS_INITIAL_STATE } from './bioEntity.constants';
 import { getBioEntity, getMultiBioEntity } from './bioEntity.thunks';
 import { BioEntityContentsState } from './bioEntity.types';
@@ -120,3 +121,18 @@ export const toggleIsContentTabOpenedReducer = (
 ): void => {
   state.isContentTabOpened = action.payload;
 };
+
+export const setBioEntityContentsReducer = (
+  state: BioEntityContentsState,
+  action: PayloadAction<BioEntityContent>,
+): void => {
+  state.data = [
+    {
+      data: [action.payload],
+      loading: 'succeeded',
+      error: DEFAULT_ERROR,
+      searchQueryElement: action.payload.bioEntity.id.toString(),
+    },
+  ];
+  state.loading = 'succeeded';
+};
diff --git a/src/redux/bioEntity/bioEntity.slice.ts b/src/redux/bioEntity/bioEntity.slice.ts
index 728467a9..40f2bbb9 100644
--- a/src/redux/bioEntity/bioEntity.slice.ts
+++ b/src/redux/bioEntity/bioEntity.slice.ts
@@ -5,6 +5,7 @@ import {
   getBioEntityContentsReducer,
   getMultiBioEntityContentsReducer,
   getSubmapConnectionsBioEntityReducer,
+  setBioEntityContentsReducer,
   toggleIsContentTabOpenedReducer,
 } from './bioEntity.reducers';
 
@@ -14,6 +15,7 @@ export const bioEntityContentsSlice = createSlice({
   reducers: {
     clearBioEntitiesData: clearBioEntitiesDataReducer,
     toggleIsContentTabOpened: toggleIsContentTabOpenedReducer,
+    setBioEntityContents: setBioEntityContentsReducer,
   },
   extraReducers: builder => {
     getBioEntityContentsReducer(builder);
@@ -22,6 +24,7 @@ export const bioEntityContentsSlice = createSlice({
   },
 });
 
-export const { clearBioEntitiesData, toggleIsContentTabOpened } = bioEntityContentsSlice.actions;
+export const { clearBioEntitiesData, toggleIsContentTabOpened, setBioEntityContents } =
+  bioEntityContentsSlice.actions;
 
 export default bioEntityContentsSlice.reducer;
diff --git a/src/utils/bioEntity/mapModelElementToBioEntity.ts b/src/utils/bioEntity/mapModelElementToBioEntity.ts
new file mode 100644
index 00000000..e8be81f2
--- /dev/null
+++ b/src/utils/bioEntity/mapModelElementToBioEntity.ts
@@ -0,0 +1,51 @@
+import { BioEntity, ModelElement } from '@/types/models';
+
+export function mapModelElementToBioEntity(modelElement: ModelElement): BioEntity {
+  return {
+    id: modelElement.id,
+    name: modelElement.name,
+    model: modelElement.model,
+    elementId: modelElement.elementId,
+    references: modelElement.references,
+    z: modelElement.z,
+    notes: modelElement.notes,
+    symbol: modelElement.symbol,
+    homodimer: modelElement.homodimer,
+    nameX: modelElement.nameX,
+    nameY: modelElement.nameY,
+    nameWidth: modelElement.nameWidth,
+    nameHeight: modelElement.nameHeight,
+    nameVerticalAlign: modelElement.nameVerticalAlign,
+    nameHorizontalAlign: modelElement.nameHorizontalAlign,
+    width: modelElement.width,
+    height: modelElement.height,
+    visibilityLevel: modelElement.visibilityLevel,
+    transparencyLevel: modelElement.transparencyLevel,
+    synonyms: modelElement.synonyms,
+    formerSymbols: modelElement.formerSymbols,
+    fullName: modelElement.fullName,
+    abbreviation: modelElement.abbreviation,
+    formula: modelElement.formula,
+    glyph: modelElement.glyph,
+    activity: modelElement.activity,
+    hypothetical: modelElement.hypothetical,
+    boundaryCondition: modelElement.boundaryCondition,
+    constant: modelElement.constant,
+    initialAmount: modelElement.initialAmount,
+    initialConcentration: modelElement.initialConcentration,
+    charge: modelElement.charge,
+    substanceUnits: modelElement.substanceUnits,
+    onlySubstanceUnits: modelElement.onlySubstanceUnits,
+    modificationResidues: modelElement.modificationResidues,
+    complex: modelElement.complex,
+    submodel: modelElement.submodel,
+    x: modelElement.x,
+    y: modelElement.y,
+    lineWidth: modelElement.lineWidth,
+    fontColor: modelElement.fontColor,
+    fontSize: modelElement.fontSize,
+    fillColor: modelElement.fillColor,
+    borderColor: modelElement.borderColor,
+    sboTerm: modelElement.sboTerm,
+  } as BioEntity;
+}
-- 
GitLab