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> {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