diff --git a/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx index 0b3e93aa33fc86e02ada210a11c31bccc52c9010..d3880a0982b7ed610b201de0f4b8d17f853bc4b6 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/AssociatedSubmap/AssociatedSubmap.component.test.tsx @@ -1,23 +1,23 @@ -import { StoreType } from '@/redux/store'; -import { - InitialStoreState, - getReduxWrapperWithStore, -} from '@/utils/testing/getReduxWrapperWithStore'; -import { act, render, screen } from '@testing-library/react'; +import { SIZE_OF_ARRAY_WITH_ONE_ELEMENT, ZERO } from '@/constants/common'; +import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock'; import { BIOENTITY_INITIAL_STATE_MOCK, BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK, } from '@/redux/bioEntity/bioEntity.mock'; -import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants'; -import { MODELS_INITIAL_STATE_MOCK } from '@/redux/models/models.mock'; -import { MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock'; import { initialMapDataFixture, openedMapsInitialValueFixture, openedMapsThreeSubmapsFixture, } from '@/redux/map/map.fixtures'; -import { SIZE_OF_ARRAY_WITH_ONE_ELEMENT, ZERO } from '@/constants/common'; +import { MODELS_INITIAL_STATE_MOCK } from '@/redux/models/models.mock'; +import { StoreType } from '@/redux/store'; +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { act, render, screen } from '@testing-library/react'; import { AssociatedSubmap } from './AssociatedSubmap.component'; const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { @@ -49,6 +49,8 @@ describe('AssociatedSubmap - component', () => { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { bioentityId: bioEntityContentFixture.bioEntity.id, + drugs: {}, + chemicals: {}, }, }, models: { @@ -68,6 +70,8 @@ describe('AssociatedSubmap - component', () => { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { bioentityId: bioEntityContentFixture.bioEntity.id, + drugs: {}, + chemicals: {}, }, }, models: { @@ -96,6 +100,8 @@ describe('AssociatedSubmap - component', () => { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { bioentityId: bioEntityContentFixture.bioEntity.id, + drugs: {}, + chemicals: {}, }, }, models: { @@ -153,6 +159,8 @@ describe('AssociatedSubmap - component', () => { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { bioentityId: bioEntityContentFixture.bioEntity.id, + drugs: {}, + chemicals: {}, }, }, models: { diff --git a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.test.tsx index dbff808742f5ed17863d9ed93a6e18e8fee17f96..b2fad19919c2e14254360a5605259e32333a034c 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.test.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.test.tsx @@ -1,26 +1,31 @@ /* 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 { FIRST_ARRAY_ELEMENT } from '@/constants/common'; import { bioEntitiesContentFixture, bioEntityContentFixture, } from '@/models/fixtures/bioEntityContentsFixture'; -import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; +import { MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock'; import { BIOENTITY_INITIAL_STATE_MOCK, BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK, } from '@/redux/bioEntity/bioEntity.mock'; -import { MODELS_MOCK_SHORT } from '@/models/mocks/modelsMock'; +import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants'; import { MODELS_INITIAL_STATE_MOCK } from '@/redux/models/models.mock'; +import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; +import { AppDispatch, RootState } from '@/redux/store'; +import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; +import { InitialStoreState } from '@/utils/testing/getReduxWrapperWithStore'; +import { act, render, screen } from '@testing-library/react'; +import { MockStoreEnhanced } from 'redux-mock-store'; import { BioEntityDrawer } from './BioEntityDrawer.component'; -const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { - const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); +const renderComponent = ( + initialStoreState: InitialStoreState = {}, +): { store: MockStoreEnhanced<Partial<RootState>, AppDispatch> } => { + const { Wrapper, store } = getReduxStoreWithActionsListener({ + ...INITIAL_STORE_STATE_MOCK, + ...initialStoreState, + }); return ( render( @@ -80,6 +85,8 @@ describe('BioEntityDrawer - component', () => { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { bioentityId: bioEntity.id, + drugs: {}, + chemicals: {}, }, }, }); @@ -114,6 +121,8 @@ describe('BioEntityDrawer - component', () => { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { bioentityId: bioEntityContentFixture.bioEntity.id, + drugs: {}, + chemicals: {}, }, }, }); @@ -148,6 +157,8 @@ describe('BioEntityDrawer - component', () => { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { bioentityId: bioEntityContentFixture.bioEntity.id, + drugs: {}, + chemicals: {}, }, }, }); @@ -173,6 +184,8 @@ describe('BioEntityDrawer - component', () => { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { bioentityId: bioEntity.id, + drugs: {}, + chemicals: {}, }, }, }); @@ -194,6 +207,8 @@ describe('BioEntityDrawer - component', () => { ...DRAWER_INITIAL_STATE, bioEntityDrawerState: { bioentityId: bioEntityContentFixture.bioEntity.id, + drugs: {}, + chemicals: {}, }, }, models: { @@ -204,5 +219,157 @@ describe('BioEntityDrawer - component', () => { expect(screen.getByTestId('associated-submap')).toBeInTheDocument(); }); + + it('should display chemicals list header', () => { + 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, + drugs: {}, + chemicals: {}, + }, + }, + }); + + expect(screen.getByText('Drugs for target')).toBeInTheDocument(); + }); + + it('should display drugs list header', () => { + 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, + drugs: {}, + chemicals: {}, + }, + }, + }); + + expect(screen.getByText('Chemicals for target', { exact: false })).toBeInTheDocument(); + }); + + it('should fetch drugs on drugs for target click', () => { + const { store } = 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, + drugs: {}, + chemicals: {}, + }, + }, + }); + + const button = screen.getByText('Drugs for target', { exact: false }); + act(() => { + button.click(); + }); + + expect(store.getActions()[0].type).toBe('drawer/getDrugsForBioEntityDrawerTarget/pending'); + }); + + it('should fetch chemicals on chemicals for target click', () => { + const { store } = 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, + drugs: {}, + chemicals: {}, + }, + }, + }); + + const button = screen.getByText('Chemicals for target', { exact: false }); + act(() => { + button.click(); + }); + + expect(store.getActions()[0].type).toBe( + 'drawer/getChemicalsForBioEntityDrawerTarget/pending', + ); + }); }); }); diff --git a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx index 6f3af87274b155a787252d1b46c6501c8f16ed52..2ee7eb5b5a6eb3e445c572fe5fa593062bd482a2 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx @@ -1,13 +1,37 @@ import { ZERO } from '@/constants/common'; -import { searchedFromMapBioEntityElement } from '@/redux/bioEntity/bioEntity.selectors'; +import { + searchedFromMapBioEntityElement, + searchedFromMapBioEntityElementRelatedSubmapSelector, +} from '@/redux/bioEntity/bioEntity.selectors'; +import { + getChemicalsForBioEntityDrawerTarget, + getDrugsForBioEntityDrawerTarget, +} from '@/redux/drawer/drawer.thunks'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { DrawerHeading } from '@/shared/DrawerHeading'; +import { ElementSearchResultType } from '@/types/models'; +import { CollapsibleSection } from '../ExportDrawer/CollapsibleSection'; import { AnnotationItem } from './AnnotationItem'; import { AssociatedSubmap } from './AssociatedSubmap'; +import { ChemicalsList } from './ChemicalsList'; +import { DrugsList } from './DrugsList'; import { OverlayData } from './OverlayData'; +const TARGET_PREFIX: ElementSearchResultType = `ALIAS`; + export const BioEntityDrawer = (): React.ReactNode => { + const dispatch = useAppDispatch(); const bioEntityData = useAppSelector(searchedFromMapBioEntityElement); + const relatedSubmap = useAppSelector(searchedFromMapBioEntityElementRelatedSubmapSelector); + const currentTargetId = bioEntityData?.id ? `${TARGET_PREFIX}:${bioEntityData.id}` : ''; + + const fetchChemicalsForTarget = (): void => { + dispatch(getChemicalsForBioEntityDrawerTarget(currentTargetId)); + }; + const fetchDrugsForTarget = (): void => { + dispatch(getDrugsForBioEntityDrawerTarget(currentTargetId)); + }; if (!bioEntityData) { return null; @@ -48,6 +72,16 @@ export const BioEntityDrawer = (): React.ReactNode => { /> ))} <AssociatedSubmap /> + {!relatedSubmap && ( + <> + <CollapsibleSection title="Drugs for target" onOpened={fetchDrugsForTarget}> + <DrugsList /> + </CollapsibleSection> + <CollapsibleSection title="Chemicals for target" onOpened={fetchChemicalsForTarget}> + <ChemicalsList /> + </CollapsibleSection> + </> + )} <OverlayData /> </div> </div> diff --git a/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/ChemicalsList.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/ChemicalsList.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..03adc8fd596b1a8890cdd2163b0adbb9c03c27bb --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/ChemicalsList.component.test.tsx @@ -0,0 +1,141 @@ +import { DEFAULT_FETCH_DATA } from '@/constants/fetchData'; +import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture'; +import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; +import { StoreType } from '@/redux/store'; +import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { render, screen } from '@testing-library/react'; +import { ChemicalsList } from './ChemicalsList.component'; + +const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); + + return ( + render( + <Wrapper> + <ChemicalsList /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('ChemicalsList - component', () => { + describe('when chemicals data is loading', () => { + beforeEach(() => { + const bioEntityId = 5000; + + renderComponent({ + drawer: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioEntityDrawerState: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioentityId: bioEntityId, + drugs: {}, + chemicals: { + [`${bioEntityId}`]: { + ...DEFAULT_FETCH_DATA, + loading: 'pending', + data: [], + }, + }, + }, + }, + }); + }); + + it('should show loading indicator', () => { + expect(screen.getByAltText('spinner icon')).toBeInTheDocument(); + }); + }); + + describe('when chemicals data is empty', () => { + beforeEach(() => { + const bioEntityId = 5000; + + renderComponent({ + drawer: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioEntityDrawerState: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioentityId: bioEntityId, + drugs: {}, + chemicals: { + [`${bioEntityId}`]: { + ...DEFAULT_FETCH_DATA, + loading: 'succeeded', + data: [], + }, + }, + }, + }, + }); + }); + + it('should show text with info', () => { + expect(screen.getByText('List is empty')).toBeInTheDocument(); + }); + }); + + describe('when chemicals data is present and valid', () => { + beforeEach(() => { + const bioEntityId = 5000; + + renderComponent({ + drawer: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioEntityDrawerState: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioentityId: bioEntityId, + drugs: {}, + chemicals: { + [`${bioEntityId}`]: { + ...DEFAULT_FETCH_DATA, + loading: 'succeeded', + data: chemicalsFixture, + }, + }, + }, + }, + }); + }); + + it.each(chemicalsFixture)('should show bio entitity card', chemical => { + expect(screen.getByText(chemical.name)).toBeInTheDocument(); + }); + }); + + describe('when chemicals data is present but for different bio entity id', () => { + beforeEach(() => { + const bioEntityId = 5000; + + renderComponent({ + drawer: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioEntityDrawerState: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioentityId: bioEntityId, + drugs: {}, + chemicals: { + '2137': { + ...DEFAULT_FETCH_DATA, + loading: 'succeeded', + data: chemicalsFixture, + }, + }, + }, + }, + }); + }); + + it.each(chemicalsFixture)('should not show bio entitity card', chemical => { + expect(screen.queryByText(chemical.name)).not.toBeInTheDocument(); + }); + + it('should show text with info', () => { + expect(screen.getByText('List is empty')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/ChemicalsList.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/ChemicalsList.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c7cb5b9f3b6a184a8e64043a8ac12e64dcf1bdc8 --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/ChemicalsList.component.tsx @@ -0,0 +1,24 @@ +import { ZERO } from '@/constants/common'; +import { currentSearchedBioEntityChemicalsSelector } from '@/redux/drawer/drawer.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { LoadingIndicator } from '@/shared/LoadingIndicator'; +import { BioEntitiesPinsListItem } from '../../SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem'; + +export const ChemicalsList = (): JSX.Element => { + const chemicals = useAppSelector(currentSearchedBioEntityChemicalsSelector); + const chemicalsData = chemicals.data || []; + const isPending = chemicals.loading === 'pending'; + + if (isPending) { + return <LoadingIndicator />; + } + + return ( + <div> + {chemicalsData.map(chemical => ( + <BioEntitiesPinsListItem key={`${chemical.id}`} pin={chemical} name={chemical.name} /> + ))} + {chemicalsData.length === ZERO && 'List is empty'} + </div> + ); +}; diff --git a/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/index.ts b/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee38f9ad5aaec7b7117d7567023311fbba3b3664 --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/ChemicalsList/index.ts @@ -0,0 +1 @@ +export { ChemicalsList } from './ChemicalsList.component'; diff --git a/src/components/Map/Drawer/BioEntityDrawer/DrugsList/DrugsList.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/DrugsList/DrugsList.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0ec503c4738362835d851bfd95d0fe0e10f08c54 --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/DrugsList/DrugsList.component.test.tsx @@ -0,0 +1,141 @@ +import { DEFAULT_FETCH_DATA } from '@/constants/fetchData'; +import { drugsFixture } from '@/models/fixtures/drugFixtures'; +import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; +import { StoreType } from '@/redux/store'; +import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { render, screen } from '@testing-library/react'; +import { DrugsList } from './DrugsList.component'; + +const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); + + return ( + render( + <Wrapper> + <DrugsList /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('DrugsList - component', () => { + describe('when drugs data is loading', () => { + beforeEach(() => { + const bioEntityId = 5000; + + renderComponent({ + drawer: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioEntityDrawerState: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioentityId: bioEntityId, + drugs: { + [`${bioEntityId}`]: { + ...DEFAULT_FETCH_DATA, + loading: 'pending', + data: [], + }, + }, + chemicals: {}, + }, + }, + }); + }); + + it('should show loading indicator', () => { + expect(screen.getByAltText('spinner icon')).toBeInTheDocument(); + }); + }); + + describe('when drugs data is empty', () => { + beforeEach(() => { + const bioEntityId = 5000; + + renderComponent({ + drawer: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioEntityDrawerState: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioentityId: bioEntityId, + drugs: { + [`${bioEntityId}`]: { + ...DEFAULT_FETCH_DATA, + loading: 'succeeded', + data: [], + }, + }, + chemicals: {}, + }, + }, + }); + }); + + it('should show text with info', () => { + expect(screen.getByText('List is empty')).toBeInTheDocument(); + }); + }); + + describe('when drugs data is present and valid', () => { + beforeEach(() => { + const bioEntityId = 5000; + + renderComponent({ + drawer: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioEntityDrawerState: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioentityId: bioEntityId, + drugs: { + [`${bioEntityId}`]: { + ...DEFAULT_FETCH_DATA, + loading: 'succeeded', + data: drugsFixture, + }, + }, + chemicals: {}, + }, + }, + }); + }); + + it.each(drugsFixture)('should show bio entitity card', drug => { + expect(screen.getByText(drug.name)).toBeInTheDocument(); + }); + }); + + describe('when drugs data is present but for different bio entity id', () => { + beforeEach(() => { + const bioEntityId = 5000; + + renderComponent({ + drawer: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioEntityDrawerState: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioentityId: bioEntityId, + drugs: { + '2137': { + ...DEFAULT_FETCH_DATA, + loading: 'succeeded', + data: drugsFixture, + }, + }, + chemicals: {}, + }, + }, + }); + }); + + it.each(drugsFixture)('should not show bio entitity card', drug => { + expect(screen.queryByText(drug.name)).not.toBeInTheDocument(); + }); + + it('should show text with info', () => { + expect(screen.getByText('List is empty')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/Map/Drawer/BioEntityDrawer/DrugsList/DrugsList.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/DrugsList/DrugsList.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..da8cf74fd112b3143271ee9c3cba4cd7837cfb83 --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/DrugsList/DrugsList.component.tsx @@ -0,0 +1,24 @@ +import { ZERO } from '@/constants/common'; +import { currentSearchedBioEntityDrugsSelector } from '@/redux/drawer/drawer.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { LoadingIndicator } from '@/shared/LoadingIndicator'; +import { BioEntitiesPinsListItem } from '../../SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem'; + +export const DrugsList = (): JSX.Element => { + const drugs = useAppSelector(currentSearchedBioEntityDrugsSelector); + const drugsData = drugs.data || []; + const isPending = drugs.loading === 'pending'; + + if (isPending) { + return <LoadingIndicator />; + } + + return ( + <div> + {drugsData.map(drug => ( + <BioEntitiesPinsListItem key={`${drug.id}`} pin={drug} name={drug.name} /> + ))} + {drugsData.length === ZERO && 'List is empty'} + </div> + ); +}; diff --git a/src/components/Map/Drawer/BioEntityDrawer/DrugsList/index.ts b/src/components/Map/Drawer/BioEntityDrawer/DrugsList/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1389bcc43344eb31aa61881829dc39eb406686d0 --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/DrugsList/index.ts @@ -0,0 +1 @@ +export { DrugsList } from './DrugsList.component'; diff --git a/src/components/Map/Drawer/ExportDrawer/CollapsibleSection/CollapsibleSection.component.tsx b/src/components/Map/Drawer/ExportDrawer/CollapsibleSection/CollapsibleSection.component.tsx index b0d478bba09f05a0b119a0b3ce84163684055a70..2dc750a9bc35c59817b96191ed83478d7b453d84 100644 --- a/src/components/Map/Drawer/ExportDrawer/CollapsibleSection/CollapsibleSection.component.tsx +++ b/src/components/Map/Drawer/ExportDrawer/CollapsibleSection/CollapsibleSection.component.tsx @@ -1,3 +1,4 @@ +import { ZERO } from '@/constants/common'; import { Accordion, AccordionItem, @@ -5,22 +6,35 @@ import { AccordionItemHeading, AccordionItemPanel, } from '@/shared/Accordion'; +import { ID } from 'react-accessible-accordion/dist/types/components/ItemContext'; type CollapsibleSectionProps = { title: string; children: React.ReactNode; + onOpened?(): void; }; export const CollapsibleSection = ({ title, children, -}: CollapsibleSectionProps): React.ReactNode => ( - <Accordion allowZeroExpanded> - <AccordionItem> - <AccordionItemHeading> - <AccordionItemButton>{title}</AccordionItemButton> - </AccordionItemHeading> - <AccordionItemPanel>{children}</AccordionItemPanel> - </AccordionItem> - </Accordion> -); + onOpened, +}: CollapsibleSectionProps): React.ReactNode => { + const handleOnChange = (ids: ID[]): void => { + const hasBeenOpened = ids.length > ZERO; + + if (hasBeenOpened && onOpened) { + onOpened(); + } + }; + + return ( + <Accordion allowZeroExpanded onChange={handleOnChange}> + <AccordionItem> + <AccordionItemHeading> + <AccordionItemButton>{title}</AccordionItemButton> + </AccordionItemHeading> + <AccordionItemPanel>{children}</AccordionItemPanel> + </AccordionItem> + </Accordion> + ); +}; diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsList.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsList.component.test.tsx index a03b5b4c2a7858b346e7aec1c24fe32307ad684e..3231142299bab33be030c43c9870a65305762cfc 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsList.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsList.component.test.tsx @@ -1,12 +1,12 @@ /* eslint-disable no-magic-numbers */ -import { render, screen } from '@testing-library/react'; +import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { StoreType } from '@/redux/store'; +import { BioEntityContent } from '@/types/models'; import { InitialStoreState, getReduxWrapperWithStore, } from '@/utils/testing/getReduxWrapperWithStore'; -import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; -import { StoreType } from '@/redux/store'; -import { BioEntityContent } from '@/types/models'; +import { render, screen } from '@testing-library/react'; import { BioEntitiesPinsList } from './BioEntitiesPinsList.component'; const renderComponent = ( @@ -30,7 +30,10 @@ const renderComponent = ( describe('BioEntitiesPinsList - component ', () => { it('should display list of bio entites elements', () => { renderComponent(bioEntitiesContentFixture); + const bioEntitiesWithFullName = bioEntitiesContentFixture.filter(({ bioEntity }) => + Boolean(bioEntity.fullName), + ); - expect(screen.getAllByTestId('bio-entity-name')).toHaveLength(10); + expect(screen.getAllByTestId('bio-entity-name')).toHaveLength(bioEntitiesWithFullName.length); }); }); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx index d10ba567c9fff0d174a8d04d32da4a212e978ce6..c5e4e4029fa87e2bed4477c4666c8d41729959e6 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.test.tsx @@ -1,22 +1,24 @@ /* eslint-disable no-magic-numbers */ -import { render, screen } from '@testing-library/react'; +import { DEFAULT_MAX_ZOOM } from '@/constants/map'; +import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { MAP_INITIAL_STATE } from '@/redux/map/map.constants'; +import { AppDispatch, RootState, StoreType } from '@/redux/store'; +import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener'; import { InitialStoreState, getReduxWrapperWithStore, } from '@/utils/testing/getReduxWrapperWithStore'; -import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; -import { StoreType } from '@/redux/store'; -import { BioEntity } from '@/types/models'; +import { render, screen } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; -import { MAP_INITIAL_STATE } from '@/redux/map/map.constants'; -import { DEFAULT_MAX_ZOOM } from '@/constants/map'; +import { MockStoreEnhanced } from 'redux-mock-store'; import { BioEntitiesPinsListItem } from './BioEntitiesPinsListItem.component'; +import { PinListBioEntity } from './BioEntitiesPinsListItem.types'; const BIO_ENTITY = bioEntitiesContentFixture[0].bioEntity; const renderComponent = ( name: string, - pin: BioEntity, + pin: PinListBioEntity, initialStoreState: InitialStoreState = {}, ): { store: StoreType } => { const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); @@ -33,6 +35,25 @@ const renderComponent = ( ); }; +const renderComponentWithActionListener = ( + name: string, + pin: PinListBioEntity, + initialStoreState: InitialStoreState = {}, +): { store: MockStoreEnhanced<Partial<RootState>, AppDispatch> } => { + const { Wrapper, store } = getReduxStoreWithActionsListener(initialStoreState); + + return ( + render( + <Wrapper> + <BioEntitiesPinsListItem name={name} pin={pin} /> + </Wrapper>, + ), + { + store, + } + ); +}; + describe('BioEntitiesPinsListItem - component ', () => { it('should display name of bio entity element', () => { renderComponent(BIO_ENTITY.name, BIO_ENTITY); @@ -56,7 +77,7 @@ describe('BioEntitiesPinsListItem - component ', () => { renderComponent(bioEntity.name, bioEntity); - expect(screen.getAllByTestId('bio-entity-symbol')[0].textContent).toHaveLength(0); + expect(screen.queryAllByTestId('bio-entity-symbol')).toHaveLength(0); }); it('should display string type of bio entity element', () => { renderComponent(BIO_ENTITY.name, BIO_ENTITY); @@ -129,4 +150,120 @@ describe('BioEntitiesPinsListItem - component ', () => { z: DEFAULT_MAX_ZOOM, }); }); + + it('should not center map to pin coordinates after click on pin icon if pin has no coords', async () => { + const { store } = renderComponent( + BIO_ENTITY.name, + { + ...BIO_ENTITY, + x: undefined, + y: undefined, + }, + { + map: { + ...MAP_INITIAL_STATE, + data: { + ...MAP_INITIAL_STATE.data, + modelId: 5052, + size: { + width: 256, + height: 256, + tileSize: 256, + minZoom: 1, + maxZoom: 1, + }, + position: { + initial: { + x: 0, + y: 0, + z: 2, + }, + last: { + x: 1, + y: 1, + z: 3, + }, + }, + }, + }, + }, + ); + const button = screen.getByTestId('center-to-pin-button'); + expect(button).toBeInTheDocument(); + + act(() => { + button.click(); + }); + + expect(store.getState().map.data.position.last).toEqual({ + x: 1, + y: 1, + z: 3, + }); + }); + + it('should dispatch get search data and open drawer on fullName click', async () => { + const { store } = renderComponentWithActionListener( + BIO_ENTITY.name, + { + ...BIO_ENTITY, + x: undefined, + y: undefined, + }, + { + map: { + ...MAP_INITIAL_STATE, + data: { + ...MAP_INITIAL_STATE.data, + modelId: 5052, + size: { + width: 256, + height: 256, + tileSize: 256, + minZoom: 1, + maxZoom: 1, + }, + position: { + initial: { + x: 0, + y: 0, + z: 2, + }, + last: { + x: 1, + y: 1, + z: 3, + }, + }, + }, + }, + }, + ); + const button = screen.getByText(BIO_ENTITY.name); + expect(button).toBeInTheDocument(); + + act(() => { + button.click(); + }); + + const actions = store.getActions(); + + expect(actions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + payload: undefined, + type: 'project/getSearchData/pending', + }), + ]), + ); + + expect(actions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + payload: BIO_ENTITY.name, + type: 'drawer/openSearchDrawerWithSelectedTab', + }), + ]), + ); + }); }); diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.tsx index e0e46854747a9fa20aaaca0f769325cd534ddc39..ceff8eb02308d091e466e63055f2a09211bbd4a5 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.component.tsx @@ -1,13 +1,23 @@ -import { Icon } from '@/shared/Icon'; -import { BioEntity } from '@/types/models'; +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import { + getDefaultSearchTab, + getSearchValuesArrayAndTrimToSeven, +} from '@/components/FunctionalArea/TopBar/SearchBar/SearchBar.utils'; +import { DEFAULT_MAX_ZOOM } from '@/constants/map'; +import { openSearchDrawerWithSelectedTab } from '@/redux/drawer/drawer.slice'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { setMapPosition } from '@/redux/map/map.slice'; -import { DEFAULT_MAX_ZOOM } from '@/constants/map'; +import { getSearchData } from '@/redux/search/search.thunks'; +import { Icon } from '@/shared/Icon'; +import { twMerge } from 'tailwind-merge'; import { getPinColor } from '../../../ResultsList/PinsList/PinsListItem/PinsListItem.component.utils'; +import { PinListBioEntity } from './BioEntitiesPinsListItem.types'; +import { isPinWithCoordinates } from './BioEntitiesPinsListItem.utils'; interface BioEntitiesPinsListItemProps { name: string; - pin: BioEntity; + pin: PinListBioEntity; } export const BioEntitiesPinsListItem = ({ @@ -15,8 +25,13 @@ export const BioEntitiesPinsListItem = ({ pin, }: BioEntitiesPinsListItemProps): JSX.Element => { const dispatch = useAppDispatch(); + const pinHasCoords = isPinWithCoordinates(pin); const handleCenterMapToPin = (): void => { + if (!pinHasCoords) { + return; + } + dispatch( setMapPosition({ x: pin.x, @@ -26,33 +41,49 @@ export const BioEntitiesPinsListItem = ({ ); }; + const handleSearchMapForPin = (): void => { + const searchValues = getSearchValuesArrayAndTrimToSeven(name); + dispatch(getSearchData({ searchQueries: searchValues, isPerfectMatch: true })); + dispatch(openSearchDrawerWithSelectedTab(getDefaultSearchTab(searchValues))); + }; + return ( <div className="mb-4 flex w-full flex-col gap-3 rounded-lg border-[1px] border-solid border-greyscale-500 p-4"> <div className="flex w-full flex-row items-center gap-2"> <button type="button" onClick={handleCenterMapToPin} - className="mr-2 shrink-0" + className={twMerge('mr-2 shrink-0', !pinHasCoords && 'cursor-default')} data-testid="center-to-pin-button" > <Icon name="pin" className={getPinColor('bioEntity')} /> </button> <p> - {pin.stringType}: <span className="w-full font-bold">{name}</span> + {pin.stringType ? `${pin.stringType}: ` : ''} + <span + className="w-full cursor-pointer font-bold underline" + onClick={handleSearchMapForPin} + > + {name} + </span> </p> </div> - <p className="font-bold leading-6"> - Full name:{' '} - <span className="w-full font-normal" data-testid="bio-entity-name"> - {pin.fullName || ``} - </span> - </p> - <p className="font-bold leading-6"> - Symbol:{' '} - <span className="w-full font-normal" data-testid="bio-entity-symbol"> - {pin.symbol || ``} - </span> - </p> + {pin.fullName && ( + <p className="font-bold leading-6"> + Full name:{' '} + <span className="w-full font-normal" data-testid="bio-entity-name"> + {pin.fullName} + </span> + </p> + )} + {pin.symbol && ( + <p className="font-bold leading-6"> + Symbol:{' '} + <span className="w-full font-normal" data-testid="bio-entity-symbol"> + {pin.symbol} + </span> + </p> + )} <p className="font-bold leading-6"> Synonyms: <span className="w-full font-normal">{pin.synonyms.join(', ')}</span> </p> diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.types.ts b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1e6cce5b1aa6d7af4cf29fde5c4626ebe230335 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.types.ts @@ -0,0 +1,14 @@ +import { BioEntity } from '@/types/models'; + +export type PinListBioEntity = Pick<BioEntity, 'synonyms' | 'references'> & { + symbol?: BioEntity['symbol']; + stringType?: BioEntity['stringType']; + fullName?: BioEntity['fullName']; + x?: BioEntity['x']; + y?: BioEntity['y']; +}; + +export type PinListBioEntityWithCoords = PinListBioEntity & { + x: BioEntity['x']; + y: BioEntity['y']; +}; diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.utils.ts b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac04a9083867762cc3aedeabc3804c3c3bccdb03 --- /dev/null +++ b/src/components/Map/Drawer/SearchDrawerWrapper/BioEntitiesResultsList/BioEntitiesPinsList/BioEntitiesPinsListItem/BioEntitiesPinsListItem.utils.ts @@ -0,0 +1,5 @@ +import { PinListBioEntity, PinListBioEntityWithCoords } from './BioEntitiesPinsListItem.types'; + +export const isPinWithCoordinates = (pin: PinListBioEntity): pin is PinListBioEntityWithCoords => { + return Boolean(pin?.x && pin?.y); +}; 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 94bc618d7d632fd681c45a9f96b2523fd6b8dffc..24af3ef7543bba17574ae3df30766b182d405346 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/ResultsList/ResultsList.component.test.tsx @@ -1,11 +1,11 @@ /* eslint-disable no-magic-numbers */ -import { act, render, screen } from '@testing-library/react'; +import { drugsFixture } from '@/models/fixtures/drugFixtures'; +import { StoreType } from '@/redux/store'; import { InitialStoreState, getReduxWrapperWithStore, } from '@/utils/testing/getReduxWrapperWithStore'; -import { StoreType } from '@/redux/store'; -import { drugsFixture } from '@/models/fixtures/drugFixtures'; +import { act, render, screen } from '@testing-library/react'; import { ResultsList } from './ResultsList.component'; const INITIAL_STATE: InitialStoreState = { @@ -25,7 +25,10 @@ const INITIAL_STATE: InitialStoreState = { selectedSearchElement: 'aspirin', }, reactionDrawerState: {}, - bioEntityDrawerState: {}, + bioEntityDrawerState: { + drugs: {}, + chemicals: {}, + }, overlayDrawerState: { currentStep: 0, }, diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx index c36eb3a97ab71c8c936439e223f01849b459a275..3ad3dbabe05b7683c49f4916d616c884b0e4aa47 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/SearchDrawerWrapper.component.test.tsx @@ -44,7 +44,10 @@ describe('SearchDrawerWrapper - component', () => { selectedSearchElement: '', }, reactionDrawerState: {}, - bioEntityDrawerState: {}, + bioEntityDrawerState: { + drugs: {}, + chemicals: {}, + }, overlayDrawerState: { currentStep: 0, }, @@ -67,7 +70,10 @@ describe('SearchDrawerWrapper - component', () => { selectedSearchElement: '', }, reactionDrawerState: {}, - bioEntityDrawerState: {}, + bioEntityDrawerState: { + drugs: {}, + chemicals: {}, + }, overlayDrawerState: { currentStep: 0, }, diff --git a/src/constants/fetchData.ts b/src/constants/fetchData.ts new file mode 100644 index 0000000000000000000000000000000000000000..692b2f95f67743bf3b4937aa94aa3c7398dfa37f --- /dev/null +++ b/src/constants/fetchData.ts @@ -0,0 +1,8 @@ +import { FetchDataState } from '@/types/fetchDataState'; +import { DEFAULT_ERROR } from './errors'; + +export const DEFAULT_FETCH_DATA: FetchDataState<[]> = { + data: [], + loading: 'idle', + error: DEFAULT_ERROR, +}; diff --git a/src/models/targetSearchNameResult.ts b/src/models/targetSearchNameResult.ts new file mode 100644 index 0000000000000000000000000000000000000000..721a63eb10494ed33a59b8dda419b7887eb17d46 --- /dev/null +++ b/src/models/targetSearchNameResult.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +export const targetSearchNameResult = z.object({ + name: z.string(), +}); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index f7816e937ba3aa3631e79975ef7e7fa48c48e93b..19aac51cbd7e5a42dad6e5cb907a10053e14b038 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -36,6 +36,10 @@ export const apiPath = { `projects/${PROJECT_ID}/models/*/bioEntities/reactions/?id=${ids.join(',')}&size=1000`, getDrugsStringWithQuery: (searchQuery: string): string => `projects/${PROJECT_ID}/drugs:search?query=${searchQuery}`, + getDrugsStringWithColumnsTarget: (columns: string, target: string): string => + `projects/${PROJECT_ID}/drugs:search?columns=${columns}&target=${target}`, + getChemicalsStringWithColumnsTarget: (columns: string, target: string): string => + `projects/${PROJECT_ID}/chemicals:search?columns=${columns}&target=${target}`, getModelsString: (): string => `projects/${PROJECT_ID}/models/`, getChemicalsStringWithQuery: (searchQuery: string): string => `projects/${PROJECT_ID}/chemicals:search?query=${searchQuery}`, diff --git a/src/redux/drawer/drawer.constants.ts b/src/redux/drawer/drawer.constants.ts index f1035f976654ddf96e73684f3e23181e1b2722e4..33890a23abf2b03c24b47f53326ea00cf37024f8 100644 --- a/src/redux/drawer/drawer.constants.ts +++ b/src/redux/drawer/drawer.constants.ts @@ -11,7 +11,10 @@ export const DRAWER_INITIAL_STATE: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, - bioEntityDrawerState: {}, + bioEntityDrawerState: { + drugs: {}, + chemicals: {}, + }, overlayDrawerState: { currentStep: 0, }, diff --git a/src/redux/drawer/drawer.reducers.ts b/src/redux/drawer/drawer.reducers.ts index 8a60b60fab8b52fdd93845ff79470c2f480d794d..3a72aa534ed977ae551c3a289ae05c4a29a3582a 100644 --- a/src/redux/drawer/drawer.reducers.ts +++ b/src/redux/drawer/drawer.reducers.ts @@ -6,7 +6,11 @@ import type { OpenSearchDrawerWithSelectedTabReducerAction, } from '@/redux/drawer/drawer.types'; import type { DrawerName } from '@/types/drawerName'; -import type { PayloadAction } from '@reduxjs/toolkit'; +import type { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; +import { + getChemicalsForBioEntityDrawerTarget, + getDrugsForBioEntityDrawerTarget, +} from './drawer.thunks'; export const openDrawerReducer = (state: DrawerState, action: PayloadAction<DrawerName>): void => { state.isOpen = true; @@ -109,3 +113,59 @@ export const openBioEntityDrawerByIdReducer = ( state.bioEntityDrawerState.bioentityId = action.payload; state.searchDrawerState.selectedSearchElement = action.payload.toString(); }; + +export const getBioEntityDrugsForTargetReducers = ( + builder: ActionReducerMapBuilder<DrawerState>, +): void => { + builder.addCase(getDrugsForBioEntityDrawerTarget.pending, state => { + const bioEntityId = state.bioEntityDrawerState.bioentityId || ''; + state.bioEntityDrawerState.drugs[bioEntityId] = { + ...state.bioEntityDrawerState.drugs[bioEntityId], + loading: 'pending', + }; + }); + builder.addCase(getDrugsForBioEntityDrawerTarget.fulfilled, (state, action) => { + const bioEntityId = state.bioEntityDrawerState.bioentityId || ''; + state.bioEntityDrawerState.drugs[bioEntityId] = { + ...state.bioEntityDrawerState.drugs[bioEntityId], + data: action.payload || [], + loading: 'succeeded', + }; + }); + builder.addCase(getDrugsForBioEntityDrawerTarget.rejected, state => { + const bioEntityId = state.bioEntityDrawerState.bioentityId || ''; + state.bioEntityDrawerState.drugs[bioEntityId] = { + ...state.bioEntityDrawerState.drugs[bioEntityId], + loading: 'failed', + }; + // TODO to discuss manage state of failure + }); +}; + +export const getBioEntityChemicalsForTargetReducers = ( + builder: ActionReducerMapBuilder<DrawerState>, +): void => { + builder.addCase(getChemicalsForBioEntityDrawerTarget.pending, state => { + const bioEntityId = state.bioEntityDrawerState.bioentityId || ''; + state.bioEntityDrawerState.chemicals[bioEntityId] = { + ...state.bioEntityDrawerState.chemicals[bioEntityId], + loading: 'pending', + }; + }); + builder.addCase(getChemicalsForBioEntityDrawerTarget.fulfilled, (state, action) => { + const bioEntityId = state.bioEntityDrawerState.bioentityId || ''; + state.bioEntityDrawerState.chemicals[bioEntityId] = { + ...state.bioEntityDrawerState.chemicals[bioEntityId], + data: action.payload || [], + loading: 'succeeded', + }; + }); + builder.addCase(getChemicalsForBioEntityDrawerTarget.rejected, state => { + const bioEntityId = state.bioEntityDrawerState.bioentityId || ''; + state.bioEntityDrawerState.chemicals[bioEntityId] = { + ...state.bioEntityDrawerState.chemicals[bioEntityId], + loading: 'failed', + }; + // TODO to discuss manage state of failure + }); +}; diff --git a/src/redux/drawer/drawer.selectors.ts b/src/redux/drawer/drawer.selectors.ts index 26e9a90b8f01ba8f2e353c8aa8310ff9917f9fcd..30d9831c715223c6e286b67c630e1b823e299541 100644 --- a/src/redux/drawer/drawer.selectors.ts +++ b/src/redux/drawer/drawer.selectors.ts @@ -1,3 +1,4 @@ +import { DEFAULT_FETCH_DATA } from '@/constants/fetchData'; import { rootSelector } from '@/redux/root/root.selectors'; import { assertNever } from '@/utils/assertNever'; import { createSelector } from '@reduxjs/toolkit'; @@ -46,6 +47,24 @@ export const currentSearchedBioEntityId = createSelector( state => state.bioentityId, ); +export const currentSearchedBioEntityDrugsSelector = createSelector( + bioEntityDrawerStateSelector, + currentSearchedBioEntityId, + (state, currentBioEntityId) => + currentBioEntityId && state.drugs[currentBioEntityId] + ? state.drugs[currentBioEntityId] + : DEFAULT_FETCH_DATA, +); + +export const currentSearchedBioEntityChemicalsSelector = createSelector( + bioEntityDrawerStateSelector, + currentSearchedBioEntityId, + (state, currentBioEntityId) => + currentBioEntityId && state.chemicals[currentBioEntityId] + ? state.chemicals[currentBioEntityId] + : DEFAULT_FETCH_DATA, +); + export const resultListSelector = createSelector( rootSelector, currentStepTypeSelector, diff --git a/src/redux/drawer/drawer.slice.ts b/src/redux/drawer/drawer.slice.ts index 98e073aea596150c61886bba0af7636abb6af68e..99d928dba8b5ae28f1c860bf4bd5ad15cd9af396 100644 --- a/src/redux/drawer/drawer.slice.ts +++ b/src/redux/drawer/drawer.slice.ts @@ -1,21 +1,23 @@ import { createSlice } from '@reduxjs/toolkit'; +import { DRAWER_INITIAL_STATE } from './drawer.constants'; import { closeDrawerReducer, + displayAddOverlaysDrawerReducer, displayBioEntitiesListReducer, displayChemicalsListReducer, displayDrugsListReducer, displayEntityDetailsReducer, displayGroupedSearchResultsReducer, + getBioEntityChemicalsForTargetReducers, + getBioEntityDrugsForTargetReducers, + openBioEntityDrawerByIdReducer, openDrawerReducer, openOverlaysDrawerReducer, - openBioEntityDrawerByIdReducer, openReactionDrawerByIdReducer, openSearchDrawerWithSelectedTabReducer, openSubmapsDrawerReducer, selectTabReducer, - displayAddOverlaysDrawerReducer, } from './drawer.reducers'; -import { DRAWER_INITIAL_STATE } from './drawer.constants'; const drawerSlice = createSlice({ name: 'drawer', @@ -36,6 +38,10 @@ const drawerSlice = createSlice({ openReactionDrawerById: openReactionDrawerByIdReducer, openBioEntityDrawerById: openBioEntityDrawerByIdReducer, }, + extraReducers: builder => { + getBioEntityDrugsForTargetReducers(builder); + getBioEntityChemicalsForTargetReducers(builder); + }, }); export const { diff --git a/src/redux/drawer/drawer.thunks.ts b/src/redux/drawer/drawer.thunks.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e60536f9d9218e75db2e516c8edbf649e43e050 --- /dev/null +++ b/src/redux/drawer/drawer.thunks.ts @@ -0,0 +1,77 @@ +import { chemicalSchema } from '@/models/chemicalSchema'; +import { drugSchema } from '@/models/drugSchema'; +import { targetSearchNameResult } from '@/models/targetSearchNameResult'; +import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; +import { Chemical, Drug, TargetSearchNameResult } from '@/types/models'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { apiPath } from '../apiPath'; + +const QUERY_COLUMN_NAME = 'name'; + +const getDrugsNamesForTarget = async (target: string): Promise<TargetSearchNameResult[]> => { + const response = await axiosInstanceNewAPI.get<TargetSearchNameResult[]>( + apiPath.getDrugsStringWithColumnsTarget(QUERY_COLUMN_NAME, target), + ); + + const isDataValid = (obj: unknown): obj is TargetSearchNameResult => + validateDataUsingZodSchema(obj, targetSearchNameResult); + + return response.data.filter(isDataValid); +}; + +const getDrugsByName = async (drugName: string): Promise<Drug[]> => { + const response = await axiosInstanceNewAPI.get<Drug[]>(apiPath.getDrugsStringWithQuery(drugName)); + + const isDataValid = (obj: unknown): obj is Drug => validateDataUsingZodSchema(obj, drugSchema); + + return response.data.filter(isDataValid); +}; + +export const getDrugsForBioEntityDrawerTarget = createAsyncThunk( + 'drawer/getDrugsForBioEntityDrawerTarget', + async (target: string): Promise<Drug[]> => { + const drugsNames = await getDrugsNamesForTarget(target); + const drugsArrays = await Promise.all( + drugsNames.map(({ name }) => getDrugsByName(encodeURIComponent(name))), + ); + const drugs = drugsArrays.flat(); + + return drugs; + }, +); + +const getChemicalsNamesForTarget = async (target: string): Promise<TargetSearchNameResult[]> => { + const response = await axiosInstanceNewAPI.get<TargetSearchNameResult[]>( + apiPath.getChemicalsStringWithColumnsTarget(QUERY_COLUMN_NAME, target), + ); + + const isDataValid = (obj: unknown): obj is TargetSearchNameResult => + validateDataUsingZodSchema(obj, targetSearchNameResult); + + return response.data.filter(isDataValid); +}; + +const getChemicalsByName = async (chemicalName: string): Promise<Chemical[]> => { + const response = await axiosInstanceNewAPI.get<Chemical[]>( + apiPath.getChemicalsStringWithQuery(chemicalName), + ); + + const isDataValid = (obj: unknown): obj is Chemical => + validateDataUsingZodSchema(obj, chemicalSchema); + + return response.data.filter(isDataValid); +}; + +export const getChemicalsForBioEntityDrawerTarget = createAsyncThunk( + 'drawer/getChemicalsForBioEntityDrawerTarget', + async (target: string): Promise<Chemical[]> => { + const chemicalsNames = await getChemicalsNamesForTarget(target); + const chemicalsArrays = await Promise.all( + chemicalsNames.map(({ name }) => getChemicalsByName(encodeURIComponent(name))), + ); + const chemicals = chemicalsArrays.flat(); + + return chemicals; + }, +); diff --git a/src/redux/drawer/drawer.types.ts b/src/redux/drawer/drawer.types.ts index 075fcd1961a96c79e242b41a9a81273e4f56f9a5..f4104f258ef17d1dd0eb416746adfde7e2bb91cb 100644 --- a/src/redux/drawer/drawer.types.ts +++ b/src/redux/drawer/drawer.types.ts @@ -1,4 +1,5 @@ import type { DrawerName } from '@/types/drawerName'; +import { KeyedFetchDataState } from '@/types/fetchDataState'; import { BioEntityContent, Chemical, Drug } from '@/types/models'; import { PayloadAction } from '@reduxjs/toolkit'; @@ -20,6 +21,8 @@ export type ReactionDrawerState = { export type BioEntityDrawerState = { bioentityId?: number; + drugs: KeyedFetchDataState<Drug[], []>; + chemicals: KeyedFetchDataState<Chemical[], []>; }; export type DrawerState = { diff --git a/src/redux/drawer/drawerFixture.ts b/src/redux/drawer/drawerFixture.ts index 29b32d3aeb1d5f53ff6ee34abd86d325d6a71e8b..fc7138abf5033cc3c2b233d85092dd17f1788d88 100644 --- a/src/redux/drawer/drawerFixture.ts +++ b/src/redux/drawer/drawerFixture.ts @@ -11,7 +11,10 @@ export const initialStateFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, - bioEntityDrawerState: {}, + bioEntityDrawerState: { + drugs: {}, + chemicals: {}, + }, overlayDrawerState: { currentStep: 0, }, @@ -28,7 +31,10 @@ export const openedDrawerSubmapsFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, - bioEntityDrawerState: {}, + bioEntityDrawerState: { + drugs: {}, + chemicals: {}, + }, overlayDrawerState: { currentStep: 0, }, @@ -45,7 +51,10 @@ export const drawerSearchStepOneFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, - bioEntityDrawerState: {}, + bioEntityDrawerState: { + drugs: {}, + chemicals: {}, + }, overlayDrawerState: { currentStep: 0, }, @@ -62,7 +71,10 @@ export const drawerSearchDrugsStepTwoFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, - bioEntityDrawerState: {}, + bioEntityDrawerState: { + drugs: {}, + chemicals: {}, + }, overlayDrawerState: { currentStep: 0, }, @@ -79,7 +91,10 @@ export const drawerSearchChemicalsStepTwoFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, - bioEntityDrawerState: {}, + bioEntityDrawerState: { + drugs: {}, + chemicals: {}, + }, overlayDrawerState: { currentStep: 0, }, @@ -96,7 +111,10 @@ export const drawerOverlaysStepOneFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, - bioEntityDrawerState: {}, + bioEntityDrawerState: { + drugs: {}, + chemicals: {}, + }, overlayDrawerState: { currentStep: 2, }, @@ -113,7 +131,10 @@ export const openedExportDrawerFixture: DrawerState = { selectedSearchElement: '', }, reactionDrawerState: {}, - bioEntityDrawerState: {}, + bioEntityDrawerState: { + drugs: {}, + chemicals: {}, + }, overlayDrawerState: { currentStep: 0, }, diff --git a/src/types/fetchDataState.ts b/src/types/fetchDataState.ts index 0ee6719c0f752174a70267f8c6d4291668f13409..95131583ccfa4bccf3f8641b8ba3f206ba1ea39e 100644 --- a/src/types/fetchDataState.ts +++ b/src/types/fetchDataState.ts @@ -15,3 +15,5 @@ export type MultiFetchDataState<T> = { loading: Loading; error: Error; }; + +export type KeyedFetchDataState<T, T2 = undefined> = Record<string, FetchDataState<T, T2>>; diff --git a/src/types/models.ts b/src/types/models.ts index fbc40b495a27ae375e1242592391e07fa6d12f22..df6237adf86972e2f120f9dd8a9e9d1000332e8f 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -48,6 +48,7 @@ import { referenceSchema } from '@/models/referenceSchema'; import { sessionSchemaValid } from '@/models/sessionValidSchema'; import { statisticsSchema } from '@/models/statisticsSchema'; import { targetSchema } from '@/models/targetSchema'; +import { targetSearchNameResult } from '@/models/targetSearchNameResult'; import { z } from 'zod'; export type Project = z.infer<typeof projectSchema>; @@ -96,3 +97,4 @@ export type ExportNetwork = z.infer<typeof exportNetworkchema>; export type ExportElements = z.infer<typeof exportElementsSchema>; export type MinervaPlugin = z.infer<typeof pluginSchema>; // Plugin type interfers with global Plugin type export type GeneVariant = z.infer<typeof geneVariant>; +export type TargetSearchNameResult = z.infer<typeof targetSearchNameResult>;