From 5604869238a9f6762719c14eeea17df1814ad1a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com>
Date: Mon, 18 Mar 2024 21:44:29 +0100
Subject: [PATCH] feat: add loading of reactions pins

---
 .../getBioEntitiesIdsFromReaction.ts          |   9 ++
 .../handleReactionResults.test.ts             |  12 +-
 .../mapSingleClick/handleReactionResults.ts   |  16 +--
 src/redux/bioEntity/bioEntity.thunks.ts       | 108 +-----------------
 src/redux/bioEntity/thunks/getBioEntity.ts    |  86 ++++++++++++++
 .../bioEntity/thunks/getMultiBioEntity.ts     |  49 ++++++++
 src/redux/reactions/reactions.slice.ts        |   5 +-
 src/redux/reactions/reactions.thunks.test.ts  |   4 +-
 src/redux/reactions/reactions.thunks.ts       |   2 +-
 9 files changed, 166 insertions(+), 125 deletions(-)
 create mode 100644 src/components/Map/MapViewer/utils/listeners/mapSingleClick/getBioEntitiesIdsFromReaction.ts
 create mode 100644 src/redux/bioEntity/thunks/getBioEntity.ts
 create mode 100644 src/redux/bioEntity/thunks/getMultiBioEntity.ts

diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/getBioEntitiesIdsFromReaction.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/getBioEntitiesIdsFromReaction.ts
new file mode 100644
index 00000000..d6c2ae75
--- /dev/null
+++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/getBioEntitiesIdsFromReaction.ts
@@ -0,0 +1,9 @@
+import { Reaction } from '@/types/models';
+
+export const getBioEntitiesIdsFromReaction = (reaction: Reaction): string[] => {
+  const { products, reactants, modifiers } = reaction;
+  const productsIds = products.map(p => p.aliasId);
+  const reactantsIds = reactants.map(r => r.aliasId);
+  const modifiersIds = modifiers.map(m => m.aliasId);
+  return [...productsIds, ...reactantsIds, ...modifiersIds].map(identifier => String(identifier));
+};
diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts
index 58ee8bf0..61e8c191 100644
--- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts
+++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts
@@ -83,12 +83,18 @@ describe('handleReactionResults - util', () => {
   it('should run getBioEntityContents fullfilled as fourth action', () => {
     const actions = store.getActions();
     expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY);
-    expect(actions[5].type).toEqual('project/getBioEntityContents/fulfilled');
+    expect(actions[5].type).toEqual('reactions/getByIds/pending');
   });
 
-  it('should run addNumbersToEntityNumberData as fifth action', () => {
+  it('should run getBioEntityContents fullfilled as fourth action', () => {
+    const actions = store.getActions();
+    expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY);
+    expect(actions[6].type).toEqual('project/getBioEntityContents/fulfilled');
+  });
+
+  it('should run getBioEntityContents fullfilled as fourth action', () => {
     const actions = store.getActions();
     expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY);
-    expect(actions[6].type).toEqual('entityNumber/addNumbersToEntityNumberData');
+    expect(actions[7].type).toEqual('entityNumber/addNumbersToEntityNumberData');
   });
 });
diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts
index 564f106b..bfbcaaa3 100644
--- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts
+++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts
@@ -5,25 +5,21 @@ import { getReactionsByIds } from '@/redux/reactions/reactions.thunks';
 import { AppDispatch } from '@/redux/store';
 import { searchFitBounds } from '@/services/pluginsManager/map/triggerSearch/searchFitBounds';
 import { PluginsEventBus } from '@/services/pluginsManager/pluginsEventBus';
-import { ElementSearchResult, Reaction } from '@/types/models';
-import { PayloadAction } from '@reduxjs/toolkit';
+import { ElementSearchResult } from '@/types/models';
+import { getBioEntitiesIdsFromReaction } from './getBioEntitiesIdsFromReaction';
 
 /* prettier-ignore */
 export const handleReactionResults =
   (dispatch: AppDispatch, closestSearchResult: ElementSearchResult, hasFitBounds?: boolean, fitBoundsZoom?: number) =>
     async ({ id }: ElementSearchResult): Promise<void> => {
-      const data = await dispatch(getReactionsByIds([id])) as PayloadAction<Reaction[] | undefined>;
+      const data = await dispatch(getReactionsByIds([id]));
       const payload = data?.payload;
-      if (!data || !payload || payload.length === SIZE_OF_EMPTY_ARRAY) {
+      if (!data || !payload || typeof payload === "string" || payload.data.length === SIZE_OF_EMPTY_ARRAY) {
         return;
       }
 
-      const reaction = payload[FIRST_ARRAY_ELEMENT];
-      const { products, reactants, modifiers } = reaction;
-      const productsIds = products.map(p => p.aliasId);
-      const reactantsIds = reactants.map(r => r.aliasId);
-      const modifiersIds = modifiers.map(m => m.aliasId);
-      const bioEntitiesIds = [...productsIds, ...reactantsIds, ...modifiersIds].map(identifier => String(identifier));
+      const reaction = payload.data[FIRST_ARRAY_ELEMENT];
+      const bioEntitiesIds = getBioEntitiesIdsFromReaction(reaction);
 
       dispatch(openReactionDrawerById(reaction.id));
       await dispatch(
diff --git a/src/redux/bioEntity/bioEntity.thunks.ts b/src/redux/bioEntity/bioEntity.thunks.ts
index 78373523..18fa812c 100644
--- a/src/redux/bioEntity/bioEntity.thunks.ts
+++ b/src/redux/bioEntity/bioEntity.thunks.ts
@@ -1,106 +1,4 @@
-import { bioEntityResponseSchema } from '@/models/bioEntityResponseSchema';
-import { apiPath } from '@/redux/apiPath';
-import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance';
-import { BioEntityContent, BioEntityResponse } from '@/types/models';
-import { PerfectMultiSearchParams, PerfectSearchParams } from '@/types/search';
-import { ThunkConfig } from '@/types/store';
-import { getErrorMessage } from '@/utils/getErrorMessage';
-import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
-import { PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
-import { addNumbersToEntityNumberData } from '../entityNumber/entityNumber.slice';
-import { getReactionsByIds } from '../reactions/reactions.thunks';
-import {
-  BIO_ENTITY_FETCHING_ERROR_PREFIX,
-  MULTI_BIO_ENTITY_FETCHING_ERROR_PREFIX,
-} from './bioEntity.constants';
+import { getBioEntity } from './thunks/getBioEntity';
+import { getMultiBioEntity } from './thunks/getMultiBioEntity';
 
-type GetBioEntityProps = PerfectSearchParams;
-
-export const getBioEntity = createAsyncThunk<
-  BioEntityContent[] | undefined,
-  GetBioEntityProps,
-  ThunkConfig
->(
-  'project/getBioEntityContents',
-  async (
-    { searchQuery, isPerfectMatch, addNumbersToEntityNumber = true },
-    { rejectWithValue, dispatch },
-  ) => {
-    try {
-      const response = await axiosInstanceNewAPI.get<BioEntityResponse>(
-        apiPath.getBioEntityContentsStringWithQuery({ searchQuery, isPerfectMatch }),
-      );
-
-      const isDataValidBioEnity = validateDataUsingZodSchema(
-        response.data,
-        bioEntityResponseSchema,
-      );
-
-      const bioEntityReactionsIds = (response.data?.content || [])
-        .filter(c => c?.bioEntity?.idReaction)
-        .map(c => c?.bioEntity?.id)
-        .filter((id): id is number => typeof id === 'number');
-
-      dispatch(
-        getReactionsByIds({
-          ids: bioEntityReactionsIds,
-          shouldConcat: true,
-        }),
-      );
-
-      if (addNumbersToEntityNumber && response.data.content) {
-        const bioEntityIds = response.data.content.map(b => b.bioEntity.elementId);
-        dispatch(addNumbersToEntityNumberData(bioEntityIds));
-      }
-
-      return isDataValidBioEnity ? response.data.content : undefined;
-    } catch (error) {
-      const errorMessage = getErrorMessage({
-        error,
-        prefix: BIO_ENTITY_FETCHING_ERROR_PREFIX,
-      });
-      return rejectWithValue(errorMessage);
-    }
-  },
-);
-
-type GetMultiBioEntityProps = PerfectMultiSearchParams;
-type GetMultiBioEntityActions = PayloadAction<BioEntityContent[] | undefined | string>[]; // if error thrown, string containing error message is returned
-
-export const getMultiBioEntity = createAsyncThunk<
-  BioEntityContent[],
-  GetMultiBioEntityProps,
-  ThunkConfig
->(
-  'project/getMultiBioEntity',
-  // eslint-disable-next-line consistent-return
-  async ({ searchQueries, isPerfectMatch }, { dispatch, rejectWithValue }) => {
-    try {
-      const asyncGetBioEntityFunctions = searchQueries.map(searchQuery =>
-        dispatch(getBioEntity({ searchQuery, isPerfectMatch, addNumbersToEntityNumber: false })),
-      );
-
-      const bioEntityContentsActions = (await Promise.all(
-        asyncGetBioEntityFunctions,
-      )) as GetMultiBioEntityActions;
-
-      const bioEntityContents = bioEntityContentsActions
-        .map(bioEntityContentsAction => bioEntityContentsAction?.payload || [])
-        .flat()
-        .filter((payload): payload is BioEntityContent => typeof payload !== 'string')
-        .filter(payload => 'bioEntity' in payload || {});
-
-      const bioEntityIds = bioEntityContents.map(b => b.bioEntity.elementId);
-      dispatch(addNumbersToEntityNumberData(bioEntityIds));
-
-      return bioEntityContents;
-    } catch (error) {
-      const errorMessage = getErrorMessage({
-        error,
-        prefix: MULTI_BIO_ENTITY_FETCHING_ERROR_PREFIX,
-      });
-
-      return rejectWithValue(errorMessage);
-    }
-  },
-);
+export { getBioEntity, getMultiBioEntity };
diff --git a/src/redux/bioEntity/thunks/getBioEntity.ts b/src/redux/bioEntity/thunks/getBioEntity.ts
new file mode 100644
index 00000000..a3e9c058
--- /dev/null
+++ b/src/redux/bioEntity/thunks/getBioEntity.ts
@@ -0,0 +1,86 @@
+import { getBioEntitiesIdsFromReaction } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/getBioEntitiesIdsFromReaction';
+import { SIZE_OF_EMPTY_ARRAY, ZERO } from '@/constants/common';
+import { bioEntityResponseSchema } from '@/models/bioEntityResponseSchema';
+import { apiPath } from '@/redux/apiPath';
+import { selectTab } from '@/redux/drawer/drawer.slice';
+import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance';
+import { BioEntityContent, BioEntityResponse } from '@/types/models';
+import { PerfectSearchParams } from '@/types/search';
+import { ThunkConfig } from '@/types/store';
+import { getErrorMessage } from '@/utils/getErrorMessage';
+import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { addNumbersToEntityNumberData } from '../../entityNumber/entityNumber.slice';
+import { getReactionsByIds } from '../../reactions/reactions.thunks';
+import { BIO_ENTITY_FETCHING_ERROR_PREFIX } from '../bioEntity.constants';
+import { getMultiBioEntity } from '../bioEntity.thunks';
+
+type GetBioEntityProps = PerfectSearchParams;
+
+export const getBioEntity = createAsyncThunk<
+  BioEntityContent[] | undefined,
+  GetBioEntityProps,
+  ThunkConfig
+>(
+  'project/getBioEntityContents',
+  async (
+    { searchQuery, isPerfectMatch, addNumbersToEntityNumber = true },
+    { rejectWithValue, dispatch },
+  ) => {
+    try {
+      const response = await axiosInstanceNewAPI.get<BioEntityResponse>(
+        apiPath.getBioEntityContentsStringWithQuery({ searchQuery, isPerfectMatch }),
+      );
+
+      const isDataValidBioEnity = validateDataUsingZodSchema(
+        response.data,
+        bioEntityResponseSchema,
+      );
+
+      const bioEntityReactionsIds = (response.data?.content || [])
+        .filter(c => c?.bioEntity?.idReaction)
+        .map(c => c?.bioEntity?.id)
+        .filter((id): id is number => typeof id === 'number');
+
+      if (bioEntityReactionsIds.length > ZERO) {
+        dispatch(
+          getReactionsByIds({
+            ids: bioEntityReactionsIds,
+            shouldConcat: true,
+          }),
+        ).then(async result => {
+          if (typeof result.payload === 'string') return;
+
+          const reactions = result.payload?.data;
+          if (!reactions || reactions.length === SIZE_OF_EMPTY_ARRAY) return;
+
+          const bioEntitiesIds = reactions
+            .map(reaction => getBioEntitiesIdsFromReaction(reaction))
+            .flat();
+
+          //   dispatch(openReactionDrawerById(reactions[FIRST_ARRAY_ELEMENT].id));
+          dispatch(selectTab(''));
+          await dispatch(
+            getMultiBioEntity({
+              searchQueries: bioEntitiesIds,
+              isPerfectMatch: true,
+            }),
+          );
+        });
+      }
+
+      if (addNumbersToEntityNumber && response.data.content) {
+        const bioEntityIds = response.data.content.map(b => b.bioEntity.elementId);
+        dispatch(addNumbersToEntityNumberData(bioEntityIds));
+      }
+
+      return isDataValidBioEnity ? response.data.content : undefined;
+    } catch (error) {
+      const errorMessage = getErrorMessage({
+        error,
+        prefix: BIO_ENTITY_FETCHING_ERROR_PREFIX,
+      });
+      return rejectWithValue(errorMessage);
+    }
+  },
+);
diff --git a/src/redux/bioEntity/thunks/getMultiBioEntity.ts b/src/redux/bioEntity/thunks/getMultiBioEntity.ts
new file mode 100644
index 00000000..23afeb9c
--- /dev/null
+++ b/src/redux/bioEntity/thunks/getMultiBioEntity.ts
@@ -0,0 +1,49 @@
+import { BioEntityContent } from '@/types/models';
+import { PerfectMultiSearchParams } from '@/types/search';
+import { ThunkConfig } from '@/types/store';
+import { getErrorMessage } from '@/utils/getErrorMessage';
+import { PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
+import { addNumbersToEntityNumberData } from '../../entityNumber/entityNumber.slice';
+import { MULTI_BIO_ENTITY_FETCHING_ERROR_PREFIX } from '../bioEntity.constants';
+import { getBioEntity } from '../bioEntity.thunks';
+
+type GetMultiBioEntityProps = PerfectMultiSearchParams;
+type GetMultiBioEntityActions = PayloadAction<BioEntityContent[] | undefined | string>[]; // if error thrown, string containing error message is returned
+
+export const getMultiBioEntity = createAsyncThunk<
+  BioEntityContent[],
+  GetMultiBioEntityProps,
+  ThunkConfig
+>(
+  'project/getMultiBioEntity',
+  // eslint-disable-next-line consistent-return
+  async ({ searchQueries, isPerfectMatch }, { dispatch, rejectWithValue }) => {
+    try {
+      const asyncGetBioEntityFunctions = searchQueries.map(searchQuery =>
+        dispatch(getBioEntity({ searchQuery, isPerfectMatch, addNumbersToEntityNumber: false })),
+      );
+
+      const bioEntityContentsActions = (await Promise.all(
+        asyncGetBioEntityFunctions,
+      )) as GetMultiBioEntityActions;
+
+      const bioEntityContents = bioEntityContentsActions
+        .map(bioEntityContentsAction => bioEntityContentsAction?.payload || [])
+        .flat()
+        .filter((payload): payload is BioEntityContent => typeof payload !== 'string')
+        .filter(payload => 'bioEntity' in payload || {});
+
+      const bioEntityIds = bioEntityContents.map(b => b.bioEntity.elementId);
+      dispatch(addNumbersToEntityNumberData(bioEntityIds));
+
+      return bioEntityContents;
+    } catch (error) {
+      const errorMessage = getErrorMessage({
+        error,
+        prefix: MULTI_BIO_ENTITY_FETCHING_ERROR_PREFIX,
+      });
+
+      return rejectWithValue(errorMessage);
+    }
+  },
+);
diff --git a/src/redux/reactions/reactions.slice.ts b/src/redux/reactions/reactions.slice.ts
index 1d0d360c..f97e9050 100644
--- a/src/redux/reactions/reactions.slice.ts
+++ b/src/redux/reactions/reactions.slice.ts
@@ -1,9 +1,6 @@
 import { createSlice } from '@reduxjs/toolkit';
 import { REACTIONS_INITIAL_STATE } from './reactions.constants';
-import {
-  getReactionsReducer,
-  resetReactionsDataReducer
-} from './reactions.reducers';
+import { getReactionsReducer, resetReactionsDataReducer } from './reactions.reducers';
 
 export const reactionsSlice = createSlice({
   name: 'reactions',
diff --git a/src/redux/reactions/reactions.thunks.test.ts b/src/redux/reactions/reactions.thunks.test.ts
index 88cfb4c4..d570a341 100644
--- a/src/redux/reactions/reactions.thunks.test.ts
+++ b/src/redux/reactions/reactions.thunks.test.ts
@@ -28,7 +28,7 @@ describe('reactions thunks', () => {
         .reply(HttpStatusCode.Ok, reactionsFixture);
 
       const { payload } = await store.dispatch(getReactionsByIds(ids));
-      expect(payload).toEqual(reactionsFixture);
+      expect(payload).toEqual({ data: reactionsFixture, shouldConcat: false });
     });
 
     it('should return undefined when data response from API is not valid ', async () => {
@@ -44,7 +44,7 @@ describe('reactions thunks', () => {
       mockedAxiosClient.onGet(apiPath.getReactionsWithIds([100])).reply(HttpStatusCode.Ok, []);
 
       const { payload } = await store.dispatch(getReactionsByIds([100]));
-      expect(payload).toEqual([]);
+      expect(payload).toEqual({ data: [], shouldConcat: false });
     });
   });
 });
diff --git a/src/redux/reactions/reactions.thunks.ts b/src/redux/reactions/reactions.thunks.ts
index 93c41f2a..610a83ad 100644
--- a/src/redux/reactions/reactions.thunks.ts
+++ b/src/redux/reactions/reactions.thunks.ts
@@ -16,7 +16,7 @@ type GetReactionsByIdsArgs =
       shouldConcat?: boolean;
     };
 
-type GetReactionsByIdsResult = {
+export type GetReactionsByIdsResult = {
   data: Reaction[];
   shouldConcat: boolean;
 };
-- 
GitLab