diff --git a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..64308bbfcfc98bbf3151d8ae1820afc4cf3529d9 --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.test.tsx @@ -0,0 +1,181 @@ +/* eslint-disable no-magic-numbers */ +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { render, screen } from '@testing-library/react'; +import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants'; +import { StoreType } from '@/redux/store'; +import { + bioEntitiesContentFixture, + bioEntityContentFixture, +} from '@/models/fixtures/bioEntityContentsFixture'; +import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; +import { BioEntityDrawer } from './BioEntityDrawer.component'; + +const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); + + return ( + render( + <Wrapper> + <BioEntityDrawer /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('BioEntityDrawer - component', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + describe("when there's NO matching bioEntity", () => { + beforeEach(() => + renderComponent({ + bioEntity: { + data: [], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + drawer: DRAWER_INITIAL_STATE, + }), + ); + + it('should not show drawer content', () => { + expect(screen.queryByText('Compartment:')).toBeNull(); + expect(screen.queryByText('Full name:')).toBeNull(); + expect(screen.queryByText('Annotations:')).toBeNull(); + expect(screen.queryByText('Source:')).toBeNull(); + }); + }); + describe('when there IS a matching bioEntity', () => { + const { bioEntity } = bioEntitiesContentFixture[FIRST_ARRAY_ELEMENT]; + + it('should show drawer header', () => { + renderComponent({ + bioEntity: { + data: [ + { + searchQueryElement: '', + loading: 'succeeded', + error: { name: '', message: '' }, + data: bioEntitiesContentFixture, + }, + ], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + drawer: { + ...DRAWER_INITIAL_STATE, + bioEntityDrawerState: { + bioentityId: bioEntity.id, + }, + }, + }); + + expect(screen.getByText(bioEntity.stringType, { exact: false })).toBeInTheDocument(); + expect(screen.getByText(bioEntity.name, { exact: false })).toBeInTheDocument(); + }); + + it('should show drawer bioEntity full name', () => { + renderComponent({ + bioEntity: { + data: [ + { + searchQueryElement: '', + loading: 'succeeded', + error: { name: '', message: '' }, + data: [ + { + ...bioEntityContentFixture, + bioEntity: { + ...bioEntityContentFixture.bioEntity, + fullName: 'BioEntity Full Name', + }, + }, + ], + }, + ], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + drawer: { + ...DRAWER_INITIAL_STATE, + bioEntityDrawerState: { + bioentityId: bioEntityContentFixture.bioEntity.id, + }, + }, + }); + + expect(screen.getByText('Full name:', { exact: false })).toBeInTheDocument(); + expect(screen.getByText('BioEntity Full Name', { exact: false })).toBeInTheDocument(); + }); + + it("should not show drawer bioEntity full name if it doesn't exists", () => { + renderComponent({ + bioEntity: { + data: [ + { + searchQueryElement: '', + loading: 'succeeded', + error: { name: '', message: '' }, + data: [ + { + ...bioEntityContentFixture, + bioEntity: { + ...bioEntityContentFixture.bioEntity, + fullName: null, + }, + }, + ], + }, + ], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + drawer: { + ...DRAWER_INITIAL_STATE, + bioEntityDrawerState: { + bioentityId: bioEntityContentFixture.bioEntity.id, + }, + }, + }); + + expect(screen.queryByText('Full name:')).toBeNull(); + }); + + it('should show list of annotations ', () => { + renderComponent({ + bioEntity: { + data: [ + { + searchQueryElement: '', + loading: 'succeeded', + error: { name: '', message: '' }, + data: bioEntitiesContentFixture, + }, + ], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + drawer: { + ...DRAWER_INITIAL_STATE, + bioEntityDrawerState: { + bioentityId: bioEntity.id, + }, + }, + }); + + expect(screen.getByText('Annotations:')).toBeInTheDocument(); + expect(screen.getByText(bioEntity.references[0].type, { exact: false })).toBeInTheDocument(); + expect( + screen.getByText(bioEntity.references[0].resource, { exact: false }), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..98b0c7bfa08413e9469876df4b55a825cface7ef --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx @@ -0,0 +1,56 @@ +import { DrawerHeading } from '@/shared/DrawerHeading'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { searchedFromMapBioEntityElement } from '@/redux/bioEntity/bioEntity.selectors'; +import { Icon } from '@/shared/Icon'; + +export const BioEntityDrawer = (): React.ReactNode => { + const bioEntityData = useAppSelector(searchedFromMapBioEntityElement); + + if (!bioEntityData) { + return null; + } + + return ( + <div className="h-full max-h-full" data-testid="bioentity-drawer"> + <DrawerHeading + title={ + <> + <span className="font-normal">{bioEntityData.stringType}:</span> + {bioEntityData.name} + </> + } + /> + <div className="flex flex-col gap-6 p-6"> + <div className="text-sm font-normal"> + Compartment: <b className="font-semibold">{bioEntityData.compartment}</b> + </div> + {bioEntityData.fullName && ( + <div className="text-sm font-normal"> + Full name: <b className="font-semibold">{bioEntityData.fullName}</b> + </div> + )} + <h3 className="font-semibold">Annotations:</h3> + {bioEntityData.references.map(reference => { + return ( + <a + className="pl-3 text-sm font-normal" + href={reference.link?.toString()} + key={reference.id} + target="_blank" + > + <div className="flex justify-between"> + <span> + Source:{' '} + <b className="font-semibold"> + {reference?.type} ({reference.resource}) + </b> + </span> + <Icon name="arrow" className="h-6 w-6 fill-font-500" /> + </div> + </a> + ); + })} + </div> + </div> + ); +}; diff --git a/src/components/Map/Drawer/Drawer.component.test.tsx b/src/components/Map/Drawer/Drawer.component.test.tsx index a0cbbdfc2e08e3a39b9b1cd658d5e895f0e811c3..fb4be23d8760e48608e43a944bc35515b5d6d739 100644 --- a/src/components/Map/Drawer/Drawer.component.test.tsx +++ b/src/components/Map/Drawer/Drawer.component.test.tsx @@ -1,6 +1,7 @@ import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; import { reactionsFixture } from '@/models/fixtures/reactionFixture'; import { + openBioEntityDrawerById, openReactionDrawerById, openSearchDrawerWithSelectedTab, openSubmapsDrawer, @@ -13,6 +14,7 @@ import { } from '@/utils/testing/getReduxWrapperWithStore'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import type {} from 'redux-thunk/extend-redux'; +import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { Drawer } from './Drawer.component'; const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => { @@ -109,4 +111,33 @@ describe('Drawer - component', () => { await waitFor(() => expect(screen.getByTestId('reaction-drawer')).toBeInTheDocument()); }); }); + + describe('bioEntity drawer', () => { + it.skip('should open drawer and display bioEntity', async () => { + const { id } = bioEntitiesContentFixture[FIRST_ARRAY_ELEMENT].bioEntity; + + const { store } = renderComponent({ + bioEntity: { + data: [ + { + searchQueryElement: '', + loading: 'succeeded', + error: { name: '', message: '' }, + data: bioEntitiesContentFixture, + }, + ], + loading: 'succeeded', + error: { message: '', name: '' }, + }, + }); + + expect(screen.queryByTestId('bioentity-drawer')).not.toBeInTheDocument(); + + await act(() => { + store.dispatch(openBioEntityDrawerById(id)); + }); + + await waitFor(() => expect(screen.getByTestId('bioentity-drawer')).toBeInTheDocument()); + }); + }); }); diff --git a/src/components/Map/Drawer/Drawer.component.tsx b/src/components/Map/Drawer/Drawer.component.tsx index abc4e3a2880001d5e5463ec393963f139b54b308..f47cc969b69a6d31352f9ca949b13c682584347d 100644 --- a/src/components/Map/Drawer/Drawer.component.tsx +++ b/src/components/Map/Drawer/Drawer.component.tsx @@ -5,6 +5,7 @@ import { twMerge } from 'tailwind-merge'; import { ReactionDrawer } from './ReactionDrawer'; import { SearchDrawerWrapper as SearchDrawerContent } from './SearchDrawerWrapper'; import { SubmapsDrawer } from './SubmapsDrawer'; +import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component'; export const Drawer = (): JSX.Element => { const { isOpen, drawerName } = useAppSelector(drawerSelector); @@ -20,6 +21,7 @@ export const Drawer = (): JSX.Element => { {isOpen && drawerName === 'search' && <SearchDrawerContent />} {isOpen && drawerName === 'submaps' && <SubmapsDrawer />} {isOpen && drawerName === 'reaction' && <ReactionDrawer />} + {isOpen && drawerName === 'bio-entity' && <BioEntityDrawer />} </div> ); }; diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx index eef88b04603620436f21da8f217bd4d4350c2904..a533a5b83bc7ca1eb96e6d1210df63868420d05d 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx @@ -25,6 +25,7 @@ const INITIAL_STATE: InitialStoreState = { selectedSearchElement: 'aspirin', }, reactionDrawerState: {}, + bioEntityDrawerState: {}, }, drugs: { data: [ diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx index 0d22badc45be8d6f9cd6cf4c68102330b6f399b6..9477d59d8d24cfd5d3af9bec571e13694a482409 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx @@ -44,6 +44,7 @@ describe('SearchDrawerWrapper - component', () => { selectedSearchElement: '', }, reactionDrawerState: {}, + bioEntityDrawerState: {}, }, }); @@ -63,6 +64,7 @@ describe('SearchDrawerWrapper - component', () => { selectedSearchElement: '', }, reactionDrawerState: {}, + bioEntityDrawerState: {}, }, }); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts index 8c3eb1e6f95f2461b398610cf139e1096e50d81c..44ac11c9c8e12c0b7a4d357f7851aae3292ee68d 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.test.ts @@ -1,4 +1,4 @@ -import { FIRST_ARRAY_ELEMENT, SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; +import { FIRST_ARRAY_ELEMENT, SECOND_ARRAY_ELEMENT, SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { ELEMENT_SEARCH_RESULT_MOCK_ALIAS } from '@/models/mocks/elementSearchResultMock'; import { apiPath } from '@/redux/apiPath'; @@ -27,11 +27,19 @@ describe('handleAliasResults - util', () => { handleAliasResults(dispatch)(ELEMENT_SEARCH_RESULT_MOCK_ALIAS); }); - it('should run getBioEntityAction', async () => { + it('should run openBioEntityDrawerById as first action', async () => { await waitFor(() => { const actions = store.getActions(); expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY); - expect(actions[FIRST_ARRAY_ELEMENT].type).toEqual('project/getMultiBioEntity/pending'); + expect(actions[FIRST_ARRAY_ELEMENT].type).toEqual('drawer/openBioEntityDrawerById'); + }); + }); + + it('should run getMultiBioEntity as second action', async () => { + await waitFor(() => { + const actions = store.getActions(); + expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY); + expect(actions[SECOND_ARRAY_ELEMENT].type).toEqual('project/getMultiBioEntity/pending'); }); }); }); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts index 024f627edf2bd8f150a2bb19e738005a433917e3..b14875831efa1192261f2f9dc7f3099a87c41a03 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/handleAliasResults.ts @@ -1,4 +1,5 @@ import { getMultiBioEntity } from '@/redux/bioEntity/bioEntity.thunks'; +import { openBioEntityDrawerById } from '@/redux/drawer/drawer.slice'; import { AppDispatch } from '@/redux/store'; import { ElementSearchResult } from '@/types/models'; @@ -6,6 +7,8 @@ import { ElementSearchResult } from '@/types/models'; export const handleAliasResults = (dispatch: AppDispatch) => async ({ id }: ElementSearchResult): Promise<void> => { + + dispatch(openBioEntityDrawerById(id)); dispatch( getMultiBioEntity({ searchQueries: [id.toString()], diff --git a/src/redux/bioEntity/bioEntity.selectors.ts b/src/redux/bioEntity/bioEntity.selectors.ts index ee2abd0fe3710f10509c05d82bfbb071f03e8e41..dfb1c82afeb17fbedda804ca81e13e02c3cd7923 100644 --- a/src/redux/bioEntity/bioEntity.selectors.ts +++ b/src/redux/bioEntity/bioEntity.selectors.ts @@ -3,7 +3,10 @@ import { rootSelector } from '@/redux/root/root.selectors'; import { MultiSearchData } from '@/types/fetchDataState'; import { BioEntity, BioEntityContent } from '@/types/models'; import { createSelector } from '@reduxjs/toolkit'; -import { currentSelectedSearchElement } from '../drawer/drawer.selectors'; +import { + currentSearchedBioEntityId, + currentSelectedSearchElement, +} from '../drawer/drawer.selectors'; import { currentModelIdSelector, modelsDataSelector } from '../models/models.selectors'; export const bioEntitySelector = createSelector(rootSelector, state => state.bioEntity); @@ -17,6 +20,17 @@ export const bioEntitiesForSelectedSearchElement = createSelector( ), ); +export const searchedFromMapBioEntityElement = createSelector( + bioEntitiesForSelectedSearchElement, + currentSearchedBioEntityId, + (bioEntitiesState, currentBioEntityId): BioEntity | undefined => { + return ( + bioEntitiesState && + bioEntitiesState.data?.find(({ bioEntity }) => bioEntity.id === currentBioEntityId)?.bioEntity + ); + }, +); + export const loadingBioEntityStatusSelector = createSelector( bioEntitiesForSelectedSearchElement, state => state?.loading, diff --git a/src/redux/drawer/drawer.constants.ts b/src/redux/drawer/drawer.constants.ts index 68d612ac9444f668bcf19cbcbfde1efee4b1c430..c04c02963c448309b61fa0d854c93875012d8514 100644 --- a/src/redux/drawer/drawer.constants.ts +++ b/src/redux/drawer/drawer.constants.ts @@ -11,4 +11,5 @@ export const DRAWER_INITIAL_STATE: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, + bioEntityDrawerState: {}, }; diff --git a/src/redux/drawer/drawer.reducers.ts b/src/redux/drawer/drawer.reducers.ts index 6c335e96aee46ca2a0e7613b4a00a9780c798b11..bd9131823d3160fcb886c4bf2c0ae02fab2bf1b3 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, + OpenBioEntityDrawerByIdAction, OpenReactionDrawerByIdAction, OpenSearchDrawerWithSelectedTabReducerAction, } from '@/redux/drawer/drawer.types'; @@ -86,3 +87,13 @@ export const openReactionDrawerByIdReducer = ( state.drawerName = 'reaction'; state.reactionDrawerState.reactionId = action.payload; }; + +export const openBioEntityDrawerByIdReducer = ( + state: DrawerState, + action: OpenBioEntityDrawerByIdAction, +): void => { + state.isOpen = true; + state.drawerName = 'bio-entity'; + state.bioEntityDrawerState.bioentityId = action.payload; + state.searchDrawerState.selectedSearchElement = action.payload.toString(); +}; diff --git a/src/redux/drawer/drawer.selectors.ts b/src/redux/drawer/drawer.selectors.ts index 6a6677167285b0287ad74d1b14f287cc69c63628..060a652a62b8ffb78d487d9cc81d0fc530169368 100644 --- a/src/redux/drawer/drawer.selectors.ts +++ b/src/redux/drawer/drawer.selectors.ts @@ -36,6 +36,16 @@ export const currentStepTypeSelector = createSelector( state => state.stepType, ); +export const bioEntityDrawerStateSelector = createSelector( + drawerSelector, + state => state.bioEntityDrawerState, +); + +export const currentSearchedBioEntityId = createSelector( + bioEntityDrawerStateSelector, + state => state.bioentityId, +); + export const resultListSelector = createSelector( rootSelector, currentStepTypeSelector, diff --git a/src/redux/drawer/drawer.slice.ts b/src/redux/drawer/drawer.slice.ts index 6bf629a37a5f08a8e5deeb169ed9a1b1e9aba25b..e15cf4db8d83549baf52bddc37548c8d53b27d8c 100644 --- a/src/redux/drawer/drawer.slice.ts +++ b/src/redux/drawer/drawer.slice.ts @@ -7,6 +7,7 @@ import { displayEntityDetailsReducer, displayGroupedSearchResultsReducer, openDrawerReducer, + openBioEntityDrawerByIdReducer, openReactionDrawerByIdReducer, openSearchDrawerWithSelectedTabReducer, openSubmapsDrawerReducer, @@ -29,6 +30,7 @@ const drawerSlice = createSlice({ displayGroupedSearchResults: displayGroupedSearchResultsReducer, displayEntityDetails: displayEntityDetailsReducer, openReactionDrawerById: openReactionDrawerByIdReducer, + openBioEntityDrawerById: openBioEntityDrawerByIdReducer, }, }); @@ -44,6 +46,7 @@ export const { displayGroupedSearchResults, displayEntityDetails, openReactionDrawerById, + openBioEntityDrawerById, } = drawerSlice.actions; export default drawerSlice.reducer; diff --git a/src/redux/drawer/drawer.types.ts b/src/redux/drawer/drawer.types.ts index 4afafc2e2c7f8c09ff611f8aec0a8236e33d99d0..44ed65164ace31db7a1ca07561382e8c6d61ffa7 100644 --- a/src/redux/drawer/drawer.types.ts +++ b/src/redux/drawer/drawer.types.ts @@ -14,11 +14,16 @@ export type ReactionDrawerState = { reactionId?: number; }; +export type BioEntityDrawerState = { + bioentityId?: number; +}; + export type DrawerState = { isOpen: boolean; drawerName: DrawerName; searchDrawerState: SearchDrawerState; reactionDrawerState: ReactionDrawerState; + bioEntityDrawerState: BioEntityDrawerState; }; export type OpenSearchDrawerWithSelectedTabReducerPayload = string; @@ -27,3 +32,6 @@ export type OpenSearchDrawerWithSelectedTabReducerAction = export type OpenReactionDrawerByIdPayload = number; export type OpenReactionDrawerByIdAction = PayloadAction<OpenReactionDrawerByIdPayload>; + +export type OpenBioEntityDrawerByIdPayload = number; +export type OpenBioEntityDrawerByIdAction = PayloadAction<OpenBioEntityDrawerByIdPayload>; diff --git a/src/redux/drawer/drawerFixture.ts b/src/redux/drawer/drawerFixture.ts index 37ec0adc3d51eb90641bd6ec9b00525e3fb8f393..818a545cc6c8ca209660932a423b9196fac90de2 100644 --- a/src/redux/drawer/drawerFixture.ts +++ b/src/redux/drawer/drawerFixture.ts @@ -11,6 +11,7 @@ export const initialStateFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, + bioEntityDrawerState: {}, }; export const openedDrawerSubmapsFixture: DrawerState = { @@ -24,6 +25,7 @@ export const openedDrawerSubmapsFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, + bioEntityDrawerState: {}, }; export const drawerSearchStepOneFixture: DrawerState = { @@ -37,6 +39,7 @@ export const drawerSearchStepOneFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, + bioEntityDrawerState: {}, }; export const drawerSearchDrugsStepTwoFixture: DrawerState = { @@ -50,6 +53,7 @@ export const drawerSearchDrugsStepTwoFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, + bioEntityDrawerState: {}, }; export const drawerSearchChemicalsStepTwoFixture: DrawerState = { @@ -63,4 +67,5 @@ export const drawerSearchChemicalsStepTwoFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, + bioEntityDrawerState: {}, }; diff --git a/src/types/drawerName.ts b/src/types/drawerName.ts index 2f7938506e57bacf7160c83abce8d4886aeba945..d34edf92b24a530f1055aa5fe92aa29d26820e8d 100644 --- a/src/types/drawerName.ts +++ b/src/types/drawerName.ts @@ -6,4 +6,5 @@ export type DrawerName = | 'export' | 'legend' | 'submaps' - | 'reaction'; + | 'reaction' + | 'bio-entity';