From c292b40d0dba8852e43775cdb37d6ad0642c294b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com>
Date: Mon, 27 Nov 2023 16:14:04 +0100
Subject: [PATCH] feat: add reaction drawer

---
 .../Map/Drawer/Drawer.component.tsx           |   4 +-
 .../ReactionDrawer.component.test.tsx         | 116 ++++++++++++++++++
 .../ReactionDrawer.component.tsx              |  53 ++++++++
 .../ReactionDrawer.constants.ts               |   1 +
 .../ReactionDrawer/ReactionDrawer.types.ts    |   8 ++
 .../Map/Drawer/ReactionDrawer/index.ts        |   1 +
 .../utils/getFilteredReferences.ts            |   7 ++
 .../utils/getGroupedReferences.ts             |  11 ++
 .../SearchDrawerWrapper.component.test.tsx    |   2 +
 .../handleReactionResults.test.ts             |  17 ++-
 .../mapSingleClick/handleReactionResults.ts   |   5 +-
 src/constants/common.ts                       |   1 +
 src/models/referenceSchema.ts                 |   2 +-
 src/redux/drawer/drawer.constants.ts          |  14 +++
 src/redux/drawer/drawer.reducers.test.ts      |  17 +--
 src/redux/drawer/drawer.reducers.ts           |  10 ++
 src/redux/drawer/drawer.selectors.ts          |  10 ++
 src/redux/drawer/drawer.slice.ts              |  19 +--
 src/redux/drawer/drawer.types.ts              |   8 ++
 src/redux/drawer/drawerFixture.ts             |   4 +
 src/redux/reactions/reactions.selector.ts     |   8 ++
 .../DrawerHeading/DrawerHeading.component.tsx |   4 +-
 src/types/drawerName.ts                       |   3 +-
 src/types/models.ts                           |   2 +
 src/utils/array/groupBy.ts                    |  12 ++
 25 files changed, 300 insertions(+), 39 deletions(-)
 create mode 100644 src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.test.tsx
 create mode 100644 src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx
 create mode 100644 src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.constants.ts
 create mode 100644 src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.types.ts
 create mode 100644 src/components/Map/Drawer/ReactionDrawer/index.ts
 create mode 100644 src/components/Map/Drawer/ReactionDrawer/utils/getFilteredReferences.ts
 create mode 100644 src/components/Map/Drawer/ReactionDrawer/utils/getGroupedReferences.ts
 create mode 100644 src/redux/drawer/drawer.constants.ts
 create mode 100644 src/utils/array/groupBy.ts

diff --git a/src/components/Map/Drawer/Drawer.component.tsx b/src/components/Map/Drawer/Drawer.component.tsx
index 269281bd..abc4e3a2 100644
--- a/src/components/Map/Drawer/Drawer.component.tsx
+++ b/src/components/Map/Drawer/Drawer.component.tsx
@@ -2,6 +2,7 @@ import { DRAWER_ROLE } from '@/components/Map/Drawer/Drawer.constants';
 import { drawerSelector } from '@/redux/drawer/drawer.selectors';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { twMerge } from 'tailwind-merge';
+import { ReactionDrawer } from './ReactionDrawer';
 import { SearchDrawerWrapper as SearchDrawerContent } from './SearchDrawerWrapper';
 import { SubmapsDrawer } from './SubmapsDrawer';
 
@@ -11,13 +12,14 @@ export const Drawer = (): JSX.Element => {
   return (
     <div
       className={twMerge(
-        'absolute bottom-0 left-[88px] top-[104px] z-10 h-calc-drawer w-[432px] -translate-x-full transform bg-white-pearl text-font-500 transition-all duration-500',
+        'absolute bottom-0 left-[88px] top-[104px] z-10 h-calc-drawer w-[432px] -translate-x-full transform border border-divide bg-white-pearl text-font-500 transition-all duration-500',
         isOpen && 'translate-x-0',
       )}
       role={DRAWER_ROLE}
     >
       {isOpen && drawerName === 'search' && <SearchDrawerContent />}
       {isOpen && drawerName === 'submaps' && <SubmapsDrawer />}
+      {isOpen && drawerName === 'reaction' && <ReactionDrawer />}
     </div>
   );
 };
diff --git a/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.test.tsx b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.test.tsx
new file mode 100644
index 00000000..251bc6c0
--- /dev/null
+++ b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.test.tsx
@@ -0,0 +1,116 @@
+import { SECOND } from '@/constants/common';
+import { reactionsFixture } from '@/models/fixtures/reactionFixture';
+import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants';
+import { StoreType } from '@/redux/store';
+import {
+  InitialStoreState,
+  getReduxWrapperWithStore,
+} from '@/utils/testing/getReduxWrapperWithStore';
+import { render, screen } from '@testing-library/react';
+import { ReactionDrawer } from './ReactionDrawer.component';
+import { DEFAULT_REFERENCE_SOURCE } from './ReactionDrawer.constants';
+
+const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
+  const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
+
+  return (
+    render(
+      <Wrapper>
+        <ReactionDrawer />
+      </Wrapper>,
+    ),
+    {
+      store,
+    }
+  );
+};
+
+describe('ReactionDrawer - component', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+    jest.clearAllMocks();
+  });
+
+  describe("when there's NO matching reaction", () => {
+    beforeEach(() =>
+      renderComponent({
+        reactions: {
+          data: [],
+          loading: 'succeeded',
+          error: { message: '', name: '' },
+        },
+        drawer: DRAWER_INITIAL_STATE,
+      }),
+    );
+
+    it('should not show drawer content', () => {
+      expect(screen.queryByText('Reaction:')).toBeNull();
+      expect(screen.queryByText('Type:')).toBeNull();
+      expect(screen.queryByText('Annotations:')).toBeNull();
+      expect(screen.queryByText('Source:')).toBeNull();
+    });
+  });
+
+  describe('when there IS a matching reaction', () => {
+    const reaction = reactionsFixture[SECOND];
+
+    const filteredReferences = reaction.references.filter(
+      ref => ref.link !== null && ref.link !== undefined,
+    );
+
+    const referencesTextHref: [string, string][] = filteredReferences.map(ref => [
+      `${ref.type} (${ref.id})`,
+      ref.link as string,
+    ]);
+
+    const referencesSources: string[] = filteredReferences.map(
+      ref => ref.annotatorClassName || DEFAULT_REFERENCE_SOURCE,
+    );
+
+    beforeEach(() =>
+      renderComponent({
+        reactions: {
+          data: reactionsFixture,
+          loading: 'succeeded',
+          error: { message: '', name: '' },
+        },
+        drawer: {
+          ...DRAWER_INITIAL_STATE,
+          reactionDrawerState: {
+            reactionId: reaction.id,
+          },
+        },
+      }),
+    );
+
+    it('should show drawer header', () => {
+      expect(screen.getByText('Reaction:')).toBeInTheDocument();
+      expect(screen.getByText(reaction.reactionId)).toBeInTheDocument();
+    });
+
+    it('should show drawer reaction type', () => {
+      expect(screen.getByText('Type:')).toBeInTheDocument();
+      expect(screen.getByText(reaction.type)).toBeInTheDocument();
+    });
+
+    it('should show drawer reaction annotations title', () => {
+      expect(screen.getByText('Annotations:')).toBeInTheDocument();
+    });
+
+    it.each(referencesSources)('should show drawer reaction source for source=%s', source => {
+      expect(screen.getByText(`Source: ${source}`, { exact: false })).toBeInTheDocument();
+    });
+
+    it.each(referencesTextHref)(
+      'should show drawer reaction reference with text=%s, href=%s',
+      (refText, href) => {
+        const linkReferenceSpan = screen.getByText(refText, { exact: false });
+        const linkReferenceAnchor = linkReferenceSpan.closest('a');
+
+        expect(linkReferenceSpan).toBeInTheDocument();
+        expect(linkReferenceAnchor).toBeInTheDocument();
+        expect(linkReferenceAnchor?.href).toBe(`${href}/`); // component render adds trailing slash
+      },
+    );
+  });
+});
diff --git a/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx
new file mode 100644
index 00000000..6bbb574f
--- /dev/null
+++ b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.component.tsx
@@ -0,0 +1,53 @@
+import { currentDrawerReactionSelector } from '@/redux/reactions/reactions.selector';
+import { DrawerHeading } from '@/shared/DrawerHeading';
+import { Icon } from '@/shared/Icon';
+import { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+import { DEFAULT_REFERENCE_SOURCE } from './ReactionDrawer.constants';
+import { getFilteredReferences } from './utils/getFilteredReferences';
+import { getGroupedReferences } from './utils/getGroupedReferences';
+
+export const ReactionDrawer = (): React.ReactNode => {
+  const reaction = useSelector(currentDrawerReactionSelector);
+
+  const referencesGrouped = useMemo(() => {
+    const referencesFiltered = getFilteredReferences(reaction);
+    return getGroupedReferences(referencesFiltered);
+  }, [reaction]);
+
+  if (!reaction) {
+    return null;
+  }
+
+  return (
+    <div className="h-full max-h-full">
+      <DrawerHeading
+        title={
+          <>
+            <span className="font-normal">Reaction:</span>&nbsp;{reaction.reactionId}
+          </>
+        }
+      />
+      <div className="flex flex-col gap-6 p-6">
+        <div className="text-sm font-normal">
+          Type: <b className="font-semibold">{reaction.type}</b>
+        </div>
+        <hr className="border-b border-b-divide" />
+        <h3 className="font-semibold">Annotations:</h3>
+        {referencesGrouped.map(({ source, references }) => (
+          <>
+            <h3 className="font-semibold">Source: {source || DEFAULT_REFERENCE_SOURCE}</h3>
+            {references.map(({ id, link, type }) => (
+              <a key={id} href={link} target="_blank">
+                <div className="flex justify-between">
+                  <span>{`${type} (${id})`}</span>
+                  <Icon name="arrow" className="h-6 w-6 fill-font-500" />
+                </div>
+              </a>
+            ))}
+          </>
+        ))}
+      </div>
+    </div>
+  );
+};
diff --git a/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.constants.ts b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.constants.ts
new file mode 100644
index 00000000..4ea80cbf
--- /dev/null
+++ b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.constants.ts
@@ -0,0 +1 @@
+export const DEFAULT_REFERENCE_SOURCE = 'Annotated by curator';
diff --git a/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.types.ts b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.types.ts
new file mode 100644
index 00000000..88a0ca30
--- /dev/null
+++ b/src/components/Map/Drawer/ReactionDrawer/ReactionDrawer.types.ts
@@ -0,0 +1,8 @@
+import { Reference } from '@/types/models';
+
+export type ReferenceFiltered = Omit<Reference, 'link'> & { link: string };
+
+export type ReferenceGrouped = {
+  references: ReferenceFiltered[];
+  source: string;
+}[];
diff --git a/src/components/Map/Drawer/ReactionDrawer/index.ts b/src/components/Map/Drawer/ReactionDrawer/index.ts
new file mode 100644
index 00000000..f440178e
--- /dev/null
+++ b/src/components/Map/Drawer/ReactionDrawer/index.ts
@@ -0,0 +1 @@
+export { ReactionDrawer } from './ReactionDrawer.component';
diff --git a/src/components/Map/Drawer/ReactionDrawer/utils/getFilteredReferences.ts b/src/components/Map/Drawer/ReactionDrawer/utils/getFilteredReferences.ts
new file mode 100644
index 00000000..c466650a
--- /dev/null
+++ b/src/components/Map/Drawer/ReactionDrawer/utils/getFilteredReferences.ts
@@ -0,0 +1,7 @@
+import { Reaction } from '@/types/models';
+import { ReferenceFiltered } from '../ReactionDrawer.types';
+
+export const getFilteredReferences = (reaction: Reaction | undefined): ReferenceFiltered[] =>
+  (reaction?.references || []).filter(
+    (ref): ref is ReferenceFiltered => ref.link !== null && ref.link !== undefined,
+  );
diff --git a/src/components/Map/Drawer/ReactionDrawer/utils/getGroupedReferences.ts b/src/components/Map/Drawer/ReactionDrawer/utils/getGroupedReferences.ts
new file mode 100644
index 00000000..425d9f0c
--- /dev/null
+++ b/src/components/Map/Drawer/ReactionDrawer/utils/getGroupedReferences.ts
@@ -0,0 +1,11 @@
+import { groupBy } from '@/utils/array/groupBy';
+import { ReferenceFiltered, ReferenceGrouped } from '../ReactionDrawer.types';
+
+export const getGroupedReferences = (references: ReferenceFiltered[]): ReferenceGrouped => {
+  const referencesGroupedObject = groupBy(references, ref => ref.annotatorClassName);
+
+  return Object.entries(referencesGroupedObject).map(([source, refs]) => ({
+    source,
+    references: refs,
+  }));
+};
diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx
index 7a35bafb..0d22badc 100644
--- a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx
+++ b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx
@@ -43,6 +43,7 @@ describe('SearchDrawerWrapper - component', () => {
           listOfBioEnitites: [],
           selectedSearchElement: '',
         },
+        reactionDrawerState: {},
       },
     });
 
@@ -61,6 +62,7 @@ describe('SearchDrawerWrapper - component', () => {
           listOfBioEnitites: [],
           selectedSearchElement: '',
         },
+        reactionDrawerState: {},
       },
     });
 
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 21c6524d..28125603 100644
--- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts
+++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.test.ts
@@ -1,5 +1,5 @@
 /* eslint-disable no-magic-numbers */
-import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
+import { FIRST, SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
 import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture';
 import { reactionsFixture } from '@/models/fixtures/reactionFixture';
 import {
@@ -43,15 +43,22 @@ describe('handleReactionResults - util', () => {
     expect(actions[1].type).toEqual('reactions/getByIds/fulfilled');
   });
 
-  it('should run setBioEntityContent to empty array as second action', () => {
+  it('should run openReactionDrawerById to empty array as second action', () => {
     const actions = store.getActions();
     expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY);
-    expect(actions[2].type).toEqual('project/getMultiBioEntity/pending');
+    expect(actions[2].type).toEqual('drawer/openReactionDrawerById');
+    expect(actions[2].payload).toEqual(reactionsFixture[FIRST].id);
   });
 
-  it('should run getBioEntity as third action', () => {
+  it('should run setBioEntityContent to empty array as third action', () => {
     const actions = store.getActions();
     expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY);
-    expect(actions[3].type).toEqual('project/getBioEntityContents/pending');
+    expect(actions[3].type).toEqual('project/getMultiBioEntity/pending');
+  });
+
+  it('should run getBioEntity as fourth action', () => {
+    const actions = store.getActions();
+    expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY);
+    expect(actions[4].type).toEqual('project/getBioEntityContents/pending');
   });
 });
diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts
index 5c5ae1a2..f566b6c1 100644
--- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts
+++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleReactionResults.ts
@@ -1,5 +1,6 @@
 import { FIRST, SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
 import { getMultiBioEntity } from '@/redux/bioEntity/bioEntity.thunks';
+import { openReactionDrawerById } from '@/redux/drawer/drawer.slice';
 import { getReactionsByIds } from '@/redux/reactions/reactions.thunks';
 import { AppDispatch } from '@/redux/store';
 import { ElementSearchResult, Reaction } from '@/types/models';
@@ -15,12 +16,14 @@ export const handleReactionResults =
         return;
       }
 
-      const { products, reactants, modifiers } = payload[FIRST];
+      const reaction = payload[FIRST];
+      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));
 
+      dispatch(openReactionDrawerById(reaction.id));
       await dispatch(
         getMultiBioEntity({
           searchQueries: bioEntitiesIds,
diff --git a/src/constants/common.ts b/src/constants/common.ts
index 1825686b..97c4d672 100644
--- a/src/constants/common.ts
+++ b/src/constants/common.ts
@@ -3,3 +3,4 @@ export const ZERO = 0;
 export const FIRST = 0;
 
 export const ONE = 1;
+export const SECOND = 1;
diff --git a/src/models/referenceSchema.ts b/src/models/referenceSchema.ts
index 30a31e28..44a1e0c6 100644
--- a/src/models/referenceSchema.ts
+++ b/src/models/referenceSchema.ts
@@ -2,7 +2,7 @@ import { z } from 'zod';
 import { articleSchema } from './articleSchema';
 
 export const referenceSchema = z.object({
-  link: z.string().nullable(),
+  link: z.string().url().nullable(),
   article: articleSchema.optional(),
   type: z.string(),
   resource: z.string(),
diff --git a/src/redux/drawer/drawer.constants.ts b/src/redux/drawer/drawer.constants.ts
new file mode 100644
index 00000000..68d612ac
--- /dev/null
+++ b/src/redux/drawer/drawer.constants.ts
@@ -0,0 +1,14 @@
+import { DrawerState } from './drawer.types';
+
+export const DRAWER_INITIAL_STATE: DrawerState = {
+  isOpen: false,
+  drawerName: 'none',
+  searchDrawerState: {
+    currentStep: 0,
+    stepType: 'none',
+    selectedValue: undefined,
+    listOfBioEnitites: [],
+    selectedSearchElement: '',
+  },
+  reactionDrawerState: {},
+};
diff --git a/src/redux/drawer/drawer.reducers.test.ts b/src/redux/drawer/drawer.reducers.test.ts
index 5f5986f5..8b355831 100644
--- a/src/redux/drawer/drawer.reducers.test.ts
+++ b/src/redux/drawer/drawer.reducers.test.ts
@@ -1,7 +1,8 @@
+import { drugFixture } from '@/models/fixtures/drugFixtures';
 import * as toolkitRaw from '@reduxjs/toolkit';
 import { AnyAction } from '@reduxjs/toolkit';
 import type { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore';
-import { drugFixture } from '@/models/fixtures/drugFixtures';
+import { DRAWER_INITIAL_STATE } from './drawer.constants';
 import drawerReducer, {
   closeDrawer,
   displayChemicalsList,
@@ -14,18 +15,6 @@ import drawerReducer, {
 } from './drawer.slice';
 import type { DrawerState } from './drawer.types';
 
-const INITIAL_STATE: DrawerState = {
-  isOpen: false,
-  drawerName: 'none',
-  searchDrawerState: {
-    currentStep: 0,
-    stepType: 'none',
-    selectedValue: undefined,
-    listOfBioEnitites: [],
-    selectedSearchElement: '',
-  },
-};
-
 const STEP = {
   FIRST: 1,
   SECOND: 2,
@@ -56,7 +45,7 @@ describe('drawer reducer', () => {
   it('should match initial state', () => {
     const action = { type: 'unknown' };
 
-    expect(drawerReducer(undefined, action)).toEqual(INITIAL_STATE);
+    expect(drawerReducer(undefined, action)).toEqual(DRAWER_INITIAL_STATE);
   });
 
   it('should update the store after openDrawer action', async () => {
diff --git a/src/redux/drawer/drawer.reducers.ts b/src/redux/drawer/drawer.reducers.ts
index f0546c31..5aefc95b 100644
--- a/src/redux/drawer/drawer.reducers.ts
+++ b/src/redux/drawer/drawer.reducers.ts
@@ -1,6 +1,7 @@
 import { STEP } from '@/constants/searchDrawer';
 import type {
   DrawerState,
+  OpenReactionDrawerByIdAction,
   OpenSearchDrawerWithSelectedTabReducerAction,
 } from '@/redux/drawer/drawer.types';
 import type { DrawerName } from '@/types/drawerName';
@@ -74,3 +75,12 @@ export const displayEntityDetailsReducer = (
   state.searchDrawerState.currentStep = STEP.THIRD;
   state.searchDrawerState.selectedValue = action.payload;
 };
+
+export const openReactionDrawerByIdReducer = (
+  state: DrawerState,
+  action: OpenReactionDrawerByIdAction,
+): void => {
+  state.isOpen = true;
+  state.drawerName = 'reaction';
+  state.reactionDrawerState.reactionId = action.payload;
+};
diff --git a/src/redux/drawer/drawer.selectors.ts b/src/redux/drawer/drawer.selectors.ts
index 66743062..dc8cb74d 100644
--- a/src/redux/drawer/drawer.selectors.ts
+++ b/src/redux/drawer/drawer.selectors.ts
@@ -89,3 +89,13 @@ export const resultListSelector = createSelector(
 export const bioEnititiesResultListSelector = createSelector(rootSelector, state => {
   return state.drawer.searchDrawerState.listOfBioEnitites;
 });
+
+export const reactionDrawerStateSelector = createSelector(
+  drawerSelector,
+  state => state.reactionDrawerState,
+);
+
+export const currentDrawerReactionIdSelector = createSelector(
+  reactionDrawerStateSelector,
+  state => state?.reactionId,
+);
diff --git a/src/redux/drawer/drawer.slice.ts b/src/redux/drawer/drawer.slice.ts
index 1ae3ace2..bf0c2f9f 100644
--- a/src/redux/drawer/drawer.slice.ts
+++ b/src/redux/drawer/drawer.slice.ts
@@ -1,4 +1,3 @@
-import { DrawerState } from '@/redux/drawer/drawer.types';
 import { createSlice } from '@reduxjs/toolkit';
 import {
   closeDrawerReducer,
@@ -9,25 +8,15 @@ import {
   displayGroupedSearchResultsReducer,
   displayMirnaListReducer,
   openDrawerReducer,
+  openReactionDrawerByIdReducer,
   openSearchDrawerWithSelectedTabReducer,
   openSubmapsDrawerReducer,
 } from './drawer.reducers';
-
-const initialState: DrawerState = {
-  isOpen: false,
-  drawerName: 'none',
-  searchDrawerState: {
-    currentStep: 0,
-    stepType: 'none',
-    selectedValue: undefined,
-    listOfBioEnitites: [],
-    selectedSearchElement: '',
-  },
-};
+import { DRAWER_INITIAL_STATE } from './drawer.constants';
 
 const drawerSlice = createSlice({
   name: 'drawer',
-  initialState,
+  initialState: DRAWER_INITIAL_STATE,
   reducers: {
     openDrawer: openDrawerReducer,
     openSearchDrawerWithSelectedTab: openSearchDrawerWithSelectedTabReducer,
@@ -39,6 +28,7 @@ const drawerSlice = createSlice({
     displayBioEntitiesList: displayBioEntitiesListReducer,
     displayGroupedSearchResults: displayGroupedSearchResultsReducer,
     displayEntityDetails: displayEntityDetailsReducer,
+    openReactionDrawerById: openReactionDrawerByIdReducer,
   },
 });
 
@@ -53,6 +43,7 @@ export const {
   displayBioEntitiesList,
   displayGroupedSearchResults,
   displayEntityDetails,
+  openReactionDrawerById,
 } = drawerSlice.actions;
 
 export default drawerSlice.reducer;
diff --git a/src/redux/drawer/drawer.types.ts b/src/redux/drawer/drawer.types.ts
index 5660d534..3046389c 100644
--- a/src/redux/drawer/drawer.types.ts
+++ b/src/redux/drawer/drawer.types.ts
@@ -10,12 +10,20 @@ export type SearchDrawerState = {
   selectedSearchElement: string;
 };
 
+export type ReactionDrawerState = {
+  reactionId?: number;
+};
+
 export type DrawerState = {
   isOpen: boolean;
   drawerName: DrawerName;
   searchDrawerState: SearchDrawerState;
+  reactionDrawerState: ReactionDrawerState;
 };
 
 export type OpenSearchDrawerWithSelectedTabReducerPayload = string;
 export type OpenSearchDrawerWithSelectedTabReducerAction =
   PayloadAction<OpenSearchDrawerWithSelectedTabReducerPayload>;
+
+export type OpenReactionDrawerByIdPayload = number;
+export type OpenReactionDrawerByIdAction = PayloadAction<OpenReactionDrawerByIdPayload>;
diff --git a/src/redux/drawer/drawerFixture.ts b/src/redux/drawer/drawerFixture.ts
index 51874b0e..80dda6fb 100644
--- a/src/redux/drawer/drawerFixture.ts
+++ b/src/redux/drawer/drawerFixture.ts
@@ -10,6 +10,7 @@ export const initialStateFixture: DrawerState = {
     listOfBioEnitites: [],
     selectedSearchElement: '',
   },
+  reactionDrawerState: {},
 };
 
 export const openedDrawerSubmapsFixture: DrawerState = {
@@ -22,6 +23,7 @@ export const openedDrawerSubmapsFixture: DrawerState = {
     listOfBioEnitites: [],
     selectedSearchElement: '',
   },
+  reactionDrawerState: {},
 };
 
 export const drawerSearchStepOneFixture: DrawerState = {
@@ -34,6 +36,7 @@ export const drawerSearchStepOneFixture: DrawerState = {
     listOfBioEnitites: [],
     selectedSearchElement: '',
   },
+  reactionDrawerState: {},
 };
 
 export const drawerSearchDrugsStepTwoFixture: DrawerState = {
@@ -46,4 +49,5 @@ export const drawerSearchDrugsStepTwoFixture: DrawerState = {
     listOfBioEnitites: [],
     selectedSearchElement: '',
   },
+  reactionDrawerState: {},
 };
diff --git a/src/redux/reactions/reactions.selector.ts b/src/redux/reactions/reactions.selector.ts
index 1f907b39..de94e6ad 100644
--- a/src/redux/reactions/reactions.selector.ts
+++ b/src/redux/reactions/reactions.selector.ts
@@ -1,5 +1,6 @@
 import { Reaction } from '@/types/models';
 import { createSelector } from '@reduxjs/toolkit';
+import { currentDrawerReactionIdSelector } from '../drawer/drawer.selectors';
 import { currentModelIdSelector } from '../models/models.selectors';
 import { rootSelector } from '../root/root.selectors';
 
@@ -17,3 +18,10 @@ export const allReactionsSelectorOfCurrentMap = createSelector(
     return reactions.filter(({ modelId }) => modelId === currentModelId);
   },
 );
+
+export const currentDrawerReactionSelector = createSelector(
+  reactionsDataSelector,
+  currentDrawerReactionIdSelector,
+  (reactions, currentDrawerReactionId) =>
+    reactions.find(({ id }) => id === currentDrawerReactionId),
+);
diff --git a/src/shared/DrawerHeading/DrawerHeading.component.tsx b/src/shared/DrawerHeading/DrawerHeading.component.tsx
index 650c76f3..f8327fbe 100644
--- a/src/shared/DrawerHeading/DrawerHeading.component.tsx
+++ b/src/shared/DrawerHeading/DrawerHeading.component.tsx
@@ -1,10 +1,10 @@
+import { closeDrawer } from '@/redux/drawer/drawer.slice';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { IconButton } from '@/shared/IconButton';
-import { closeDrawer } from '@/redux/drawer/drawer.slice';
 import { CLOSE_BUTTON_ROLE } from '../DrawerHeadingBackwardButton/DrawerHeadingBackwardButton.constants';
 
 interface DrawerHeadingProps {
-  title: string;
+  title: string | JSX.Element;
 }
 
 export const DrawerHeading = ({ title }: DrawerHeadingProps): JSX.Element => {
diff --git a/src/types/drawerName.ts b/src/types/drawerName.ts
index 3dcb4bdc..2f793850 100644
--- a/src/types/drawerName.ts
+++ b/src/types/drawerName.ts
@@ -5,4 +5,5 @@ export type DrawerName =
   | 'plugins'
   | 'export'
   | 'legend'
-  | 'submaps';
+  | 'submaps'
+  | 'reaction';
diff --git a/src/types/models.ts b/src/types/models.ts
index 27b6246e..5b997016 100644
--- a/src/types/models.ts
+++ b/src/types/models.ts
@@ -14,6 +14,7 @@ import { overviewImageView } from '@/models/overviewImageView';
 import { projectSchema } from '@/models/project';
 import { reactionSchema } from '@/models/reaction';
 import { reactionLineSchema } from '@/models/reactionLineSchema';
+import { referenceSchema } from '@/models/referenceSchema';
 import { targetSchema } from '@/models/targetSchema';
 import { z } from 'zod';
 
@@ -32,6 +33,7 @@ export type BioEntityContent = z.infer<typeof bioEntityContentSchema>;
 export type BioEntityResponse = z.infer<typeof bioEntityResponseSchema>;
 export type Chemical = z.infer<typeof chemicalSchema>;
 export type Reaction = z.infer<typeof reactionSchema>;
+export type Reference = z.infer<typeof referenceSchema>;
 export type ReactionLine = z.infer<typeof reactionLineSchema>;
 export type ElementSearchResult = z.infer<typeof elementSearchResult>;
 export type ElementSearchResultType = z.infer<typeof elementSearchResultType>;
diff --git a/src/utils/array/groupBy.ts b/src/utils/array/groupBy.ts
new file mode 100644
index 00000000..4fe4d030
--- /dev/null
+++ b/src/utils/array/groupBy.ts
@@ -0,0 +1,12 @@
+/* prettier-ignore */
+export const groupBy = <T>(
+  array: T[],
+  predicate: (value: T, index: number, arr: T[]) => string,
+): { [key: string]: T[] } =>
+    array.reduce(
+      (acc, value, index, arr) => {
+        (acc[predicate(value, index, arr)] ||= []).push(value);
+        return acc;
+      },
+    {} as { [key: string]: T[] },
+    );
-- 
GitLab