From 56bb144c8b321a6f7846e563488582a2e90ba5fa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tadeusz=20Miesi=C4=85c?= <tadeusz.miesiac@gmail.com>
Date: Thu, 21 Dec 2023 21:30:41 +0100
Subject: [PATCH] feat(overlays): add/remove overlay from store

---
 .../OverlayListItem.component.test.tsx        | 64 +++++++++++++------
 .../OverlayListItem.component.tsx             | 16 ++---
 .../OverlayListItem/hooks/useOverlay.ts       | 28 ++++++++
 .../getColorByAvailableProperties.ts          |  4 +-
 .../overlayBioEntity.reducers.test.ts         | 42 ++++++++++++
 .../overlayBioEntity.reducers.ts              | 31 +++++++--
 .../overlayBioEntity.selector.ts              | 16 ++++-
 .../overlayBioEntity.slice.ts                 |  6 +-
 .../overlayBioEntity.types.ts                 | 11 +++-
 9 files changed, 177 insertions(+), 41 deletions(-)
 create mode 100644 src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/hooks/useOverlay.ts
 create mode 100644 src/redux/overlayBioEntity/overlayBioEntity.reducers.test.ts

diff --git a/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.test.tsx b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.test.tsx
index dd3cc6ea..1c2e5658 100644
--- a/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.test.tsx
+++ b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.test.tsx
@@ -47,30 +47,56 @@ describe('OverlayListItem - component', () => {
     expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument();
   });
 
-  it('should trigger view overlays on view button click and switch background to Empty if available', async () => {
-    const OVERLAY_ID = 21;
-    const { store } = renderComponent({
-      map: initialMapStateFixture,
-      backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK },
-      overlayBioEntity: OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK,
-      models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] },
-    });
-    mockedAxiosNewClient
-      .onGet(apiPath.getOverlayBioEntity({ overlayId: OVERLAY_ID, modelId: 5053 }))
-      .reply(HttpStatusCode.Ok, overlayBioEntityFixture);
+  describe('view overlays', () => {
+    it('should trigger view overlays on view button click and switch background to Empty if available', async () => {
+      const OVERLAY_ID = 21;
+      const MODEL_ID = 5053;
+      const { store } = renderComponent({
+        map: initialMapStateFixture,
+        backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK },
+        overlayBioEntity: OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK,
+        models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] },
+      });
+      mockedAxiosNewClient
+        .onGet(apiPath.getOverlayBioEntity({ overlayId: OVERLAY_ID, modelId: MODEL_ID }))
+        .reply(HttpStatusCode.Ok, overlayBioEntityFixture);
+
+      expect(store.getState().map.data.backgroundId).toBe(DEFAULT_BACKGROUND_ID);
 
-    expect(store.getState().map.data.backgroundId).toBe(DEFAULT_BACKGROUND_ID);
+      const ViewButton = screen.getByRole('button', { name: 'View' });
+      await act(() => {
+        ViewButton.click();
+      });
 
-    const ViewButton = screen.getByRole('button', { name: 'View' });
-    await act(() => {
-      ViewButton.click();
+      expect(store.getState().map.data.backgroundId).toBe(EMPTY_BACKGROUND_ID);
+      expect(store.getState().overlayBioEntity.data).toEqual({
+        [OVERLAY_ID]: {
+          [MODEL_ID]: parseOverlayBioEntityToOlRenderingFormat(overlayBioEntityFixture, OVERLAY_ID),
+        },
+      });
     });
+    it('should disable overlay on view button click if overlay is active', async () => {
+      const OVERLAY_ID = 21;
+      const { store } = renderComponent({
+        map: {
+          ...initialMapStateFixture,
+          data: { ...initialMapStateFixture.data, backgroundId: EMPTY_BACKGROUND_ID },
+        },
+        backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK },
+        overlayBioEntity: { ...OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, overlaysId: [OVERLAY_ID] },
+        models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] },
+      });
+
+      const ViewButton = screen.getByRole('button', { name: 'Disable' });
+      await act(() => {
+        ViewButton.click();
+      });
 
-    expect(store.getState().map.data.backgroundId).toBe(EMPTY_BACKGROUND_ID);
-    expect(store.getState().overlayBioEntity.data).toEqual(
-      parseOverlayBioEntityToOlRenderingFormat(overlayBioEntityFixture, OVERLAY_ID),
-    );
+      expect(store.getState().overlayBioEntity.data).toEqual([]);
+      expect(store.getState().overlayBioEntity.overlaysId).toEqual([]);
+    });
   });
+
   // TODO implement when connecting logic to component
   it.skip('should trigger download overlay to PC on download button click', () => {});
 });
diff --git a/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.tsx b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.tsx
index 20f173fe..ab40e7cf 100644
--- a/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.tsx
+++ b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.tsx
@@ -1,7 +1,5 @@
-import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
-import { getOverlayBioEntityForAllModels } from '@/redux/overlayBioEntity/overlayBioEntity.thunk';
 import { Button } from '@/shared/Button';
-import { useEmptyBackground } from './hooks/useEmptyBackground';
+import { useOverlay } from './hooks/useOverlay';
 
 interface OverlayListItemProps {
   name: string;
@@ -10,20 +8,14 @@ interface OverlayListItemProps {
 
 export const OverlayListItem = ({ name, overlayId }: OverlayListItemProps): JSX.Element => {
   const onDownloadOverlay = (): void => {};
-  const dispatch = useAppDispatch();
-  const { setBackgroundtoEmptyIfAvailable } = useEmptyBackground();
-
-  const onViewOverlay = (): void => {
-    setBackgroundtoEmptyIfAvailable();
-    dispatch(getOverlayBioEntityForAllModels({ overlayId }));
-  };
+  const { toggleOverlay, isOverlayActive } = useOverlay(overlayId);
 
   return (
     <li className="flex flex-row flex-nowrap justify-between pl-5 [&:not(:last-of-type)]:mb-4">
       <span>{name}</span>
       <div className="flex flex-row flex-nowrap">
-        <Button variantStyles="ghost" className="mr-4 max-h-8" onClick={onViewOverlay}>
-          View
+        <Button variantStyles="ghost" className="mr-4 max-h-8" onClick={toggleOverlay}>
+          {isOverlayActive ? 'Disable' : 'View'}
         </Button>
         <Button className="max-h-8" variantStyles="ghost" onClick={onDownloadOverlay}>
           Download
diff --git a/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/hooks/useOverlay.ts b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/hooks/useOverlay.ts
new file mode 100644
index 00000000..89f65ee8
--- /dev/null
+++ b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/hooks/useOverlay.ts
@@ -0,0 +1,28 @@
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { isOverlayActiveSelector } from '@/redux/overlayBioEntity/overlayBioEntity.selector';
+import { removeOverlayBioEntityForGivenOverlay } from '@/redux/overlayBioEntity/overlayBioEntity.slice';
+import { getOverlayBioEntityForAllModels } from '@/redux/overlayBioEntity/overlayBioEntity.thunk';
+import { useEmptyBackground } from './useEmptyBackground';
+
+type UseOverlay = {
+  toggleOverlay: () => void;
+  isOverlayActive: boolean;
+};
+
+export const useOverlay = (overlayId: number): UseOverlay => {
+  const dispatch = useAppDispatch();
+  const isOverlayActive = useAppSelector(state => isOverlayActiveSelector(state, overlayId));
+  const { setBackgroundtoEmptyIfAvailable } = useEmptyBackground();
+
+  const toggleOverlay = (): void => {
+    if (isOverlayActive) {
+      dispatch(removeOverlayBioEntityForGivenOverlay({ overlayId }));
+    } else {
+      setBackgroundtoEmptyIfAvailable();
+      dispatch(getOverlayBioEntityForAllModels({ overlayId }));
+    }
+  };
+
+  return { toggleOverlay, isOverlayActive };
+};
diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/getColorByAvailableProperties.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/getColorByAvailableProperties.ts
index b7ac985f..6cbc9883 100644
--- a/src/components/Map/MapViewer/utils/config/overlaysLayer/getColorByAvailableProperties.ts
+++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/getColorByAvailableProperties.ts
@@ -1,7 +1,7 @@
 import { ZERO } from '@/constants/common';
 import type { GetHex3ColorGradientColorWithAlpha } from '@/hooks/useTriColorLerp';
 import { OverlayBioEntityRender } from '@/types/OLrendering';
-import { convertDecimalToHex } from '@/utils/convert/convertDecimalToHex';
+import { convertDecimalToHexColor } from '@/utils/convert/convertDecimalToHex';
 
 export const getColorByAvailableProperties = (
   entity: OverlayBioEntityRender,
@@ -12,7 +12,7 @@ export const getColorByAvailableProperties = (
     return getHexTricolorGradientColorWithAlpha(entity.value || ZERO);
   }
   if (entity.color) {
-    return convertDecimalToHex(entity.color.rgb);
+    return convertDecimalToHexColor(entity.color.rgb);
   }
   return defaultColor;
 };
diff --git a/src/redux/overlayBioEntity/overlayBioEntity.reducers.test.ts b/src/redux/overlayBioEntity/overlayBioEntity.reducers.test.ts
new file mode 100644
index 00000000..ba1740c3
--- /dev/null
+++ b/src/redux/overlayBioEntity/overlayBioEntity.reducers.test.ts
@@ -0,0 +1,42 @@
+import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
+import { HttpStatusCode } from 'axios';
+import {
+  ToolkitStoreWithSingleSlice,
+  createStoreInstanceUsingSliceReducer,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { overlayBioEntityFixture } from '@/models/fixtures/overlayBioEntityFixture';
+import overlayBioEntityReducer from './overlayBioEntity.slice';
+import { getOverlayBioEntity } from './overlayBioEntity.thunk';
+import { parseOverlayBioEntityToOlRenderingFormat } from './overlayBioEntity.utils';
+import { OverlaysBioEntityState } from './overlayBioEntity.types';
+import { apiPath } from '../apiPath';
+
+const mockedNewAxiosClient = mockNetworkNewAPIResponse();
+
+describe('Overlay Bio Entity Reducers', () => {
+  const OVERLAY_ID = 21;
+  const MODEL_ID = 27;
+
+  let store = {} as ToolkitStoreWithSingleSlice<OverlaysBioEntityState>;
+  beforeEach(() => {
+    store = createStoreInstanceUsingSliceReducer('overlayBioEntity', overlayBioEntityReducer);
+  });
+
+  describe('getOverlayBioEntityReducer', () => {
+    it('should update the state correctly when getOverlayBioEntity action is dispatched', async () => {
+      mockedNewAxiosClient
+        .onGet(apiPath.getOverlayBioEntity({ overlayId: OVERLAY_ID, modelId: MODEL_ID }))
+        .reply(HttpStatusCode.Ok, overlayBioEntityFixture);
+
+      const { type } = await store.dispatch(
+        getOverlayBioEntity({ overlayId: OVERLAY_ID, modelId: MODEL_ID }),
+      );
+      const { data } = store.getState().overlayBioEntity;
+
+      expect(type).toBe('overlayBioEntity/getOverlayBioEntity/fulfilled');
+      expect(data[OVERLAY_ID][MODEL_ID]).toEqual(
+        parseOverlayBioEntityToOlRenderingFormat(overlayBioEntityFixture, OVERLAY_ID),
+      );
+    });
+  });
+});
diff --git a/src/redux/overlayBioEntity/overlayBioEntity.reducers.ts b/src/redux/overlayBioEntity/overlayBioEntity.reducers.ts
index da76054b..797bacd7 100644
--- a/src/redux/overlayBioEntity/overlayBioEntity.reducers.ts
+++ b/src/redux/overlayBioEntity/overlayBioEntity.reducers.ts
@@ -1,14 +1,21 @@
 import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
 import { getOverlayBioEntity, getOverlayBioEntityForAllModels } from './overlayBioEntity.thunk';
-import { OverlaysBioEntityState } from './overlayBioEntity.types';
+import {
+  OverlaysBioEntityState,
+  RemoveOverlayBioEntityForGivenOverlayAction,
+} from './overlayBioEntity.types';
 
 export const getOverlayBioEntityReducer = (
   builder: ActionReducerMapBuilder<OverlaysBioEntityState>,
 ): void => {
   builder.addCase(getOverlayBioEntity.fulfilled, (state, action) => {
     if (action.payload) {
-      state.overlaysId = [action.meta.arg.overlayId];
-      state.data.push(...action.payload);
+      const { overlayId, modelId } = action.meta.arg;
+      if (!state.data[action.meta.arg.overlayId]) {
+        state.data[overlayId] = {};
+      }
+
+      state.data[overlayId][modelId] = action.payload;
     }
   });
 };
@@ -16,7 +23,21 @@ export const getOverlayBioEntityReducer = (
 export const getOverlayBioEntityForAllModelsReducer = (
   builder: ActionReducerMapBuilder<OverlaysBioEntityState>,
 ): void => {
-  builder.addCase(getOverlayBioEntityForAllModels.pending, state => {
-    state.data = [];
+  builder.addCase(getOverlayBioEntityForAllModels.pending, (state, action) => {
+    const { overlayId } = action.meta.arg;
+    state.overlaysId.push(overlayId);
+    state.data = {
+      ...state.data, // this is expection to the rule of immutability from redux-toolkit. state.data[overlayId] = {} would add null values up to overlayId value witch leads to mess in the store
+      [overlayId]: {},
+    };
   });
 };
+
+export const removeOverlayBioEntityForGivenOverlayReducer = (
+  state: OverlaysBioEntityState,
+  action: RemoveOverlayBioEntityForGivenOverlayAction,
+): void => {
+  const { overlayId } = action.payload;
+  state.overlaysId = state.overlaysId.filter(id => id !== overlayId);
+  delete state.data[overlayId];
+};
diff --git a/src/redux/overlayBioEntity/overlayBioEntity.selector.ts b/src/redux/overlayBioEntity/overlayBioEntity.selector.ts
index 72c3b359..00803c03 100644
--- a/src/redux/overlayBioEntity/overlayBioEntity.selector.ts
+++ b/src/redux/overlayBioEntity/overlayBioEntity.selector.ts
@@ -12,8 +12,22 @@ export const overlayBioEntityDataSelector = createSelector(
   overlayBioEntity => overlayBioEntity.data,
 );
 
+export const activeOverlaysIdSelector = createSelector(
+  overlayBioEntitySelector,
+  state => state.overlaysId,
+);
+
+const FIRST_ENTITY_INDEX = 0;
+// TODO, improve selector when multioverlay algorithm comes in place
 export const overlayBioEntitiesForCurrentModelSelector = createSelector(
   overlayBioEntityDataSelector,
   currentModelIdSelector,
-  (data, currentModelId) => data.filter(entity => entity.modelId === currentModelId),
+  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+  // @ts-ignore
+  (data, currentModelId) => data[Object.keys(data)[FIRST_ENTITY_INDEX]]?.[currentModelId] ?? [], // temporary solution untill multioverlay algorithm comes in place
+);
+
+export const isOverlayActiveSelector = createSelector(
+  [activeOverlaysIdSelector, (_, overlayId: number): number => overlayId],
+  (overlaysId, overlayId) => overlaysId.includes(overlayId),
 );
diff --git a/src/redux/overlayBioEntity/overlayBioEntity.slice.ts b/src/redux/overlayBioEntity/overlayBioEntity.slice.ts
index f25d3ed6..ae21322c 100644
--- a/src/redux/overlayBioEntity/overlayBioEntity.slice.ts
+++ b/src/redux/overlayBioEntity/overlayBioEntity.slice.ts
@@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit';
 import {
   getOverlayBioEntityForAllModelsReducer,
   getOverlayBioEntityReducer,
+  removeOverlayBioEntityForGivenOverlayReducer,
 } from './overlayBioEntity.reducers';
 import { OverlaysBioEntityState } from './overlayBioEntity.types';
 
@@ -13,11 +14,14 @@ const initialState: OverlaysBioEntityState = {
 export const overlayBioEntitySlice = createSlice({
   name: 'overlayBioEntity',
   initialState,
-  reducers: {},
+  reducers: {
+    removeOverlayBioEntityForGivenOverlay: removeOverlayBioEntityForGivenOverlayReducer,
+  },
   extraReducers: builder => {
     getOverlayBioEntityReducer(builder);
     getOverlayBioEntityForAllModelsReducer(builder);
   },
 });
 
+export const { removeOverlayBioEntityForGivenOverlay } = overlayBioEntitySlice.actions;
 export default overlayBioEntitySlice.reducer;
diff --git a/src/redux/overlayBioEntity/overlayBioEntity.types.ts b/src/redux/overlayBioEntity/overlayBioEntity.types.ts
index 43eeb895..4074058b 100644
--- a/src/redux/overlayBioEntity/overlayBioEntity.types.ts
+++ b/src/redux/overlayBioEntity/overlayBioEntity.types.ts
@@ -1,6 +1,15 @@
 import { OverlayBioEntityRender } from '@/types/OLrendering';
+import { PayloadAction } from '@reduxjs/toolkit';
 
 export type OverlaysBioEntityState = {
   overlaysId: number[];
-  data: OverlayBioEntityRender[];
+  data: {
+    [overlayId: number]: {
+      [modelId: number]: OverlayBioEntityRender[];
+    };
+  };
 };
+
+export type RemoveOverlayBioEntityForGivenOverlayPayload = { overlayId: number };
+export type RemoveOverlayBioEntityForGivenOverlayAction =
+  PayloadAction<RemoveOverlayBioEntityForGivenOverlayPayload>;
-- 
GitLab