Skip to content
Snippets Groups Projects
Commit 631bc198 authored by Mateusz Bolewski's avatar Mateusz Bolewski
Browse files

Feature/search bioentities details

parent fcd1b12f
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!60Feature/search bioentities details
Showing
with 212 additions and 110 deletions
import { BioEntityContent } from '@/types/models';
import { BioEntitiesPinsListItem } from './BioEntitiesPinsListItem';
interface BioEntitiesPinsListProps {
bioEnititesPins: BioEntityContent[];
}
export const BioEntitiesPinsList = ({ bioEnititesPins }: BioEntitiesPinsListProps): JSX.Element => {
return (
<ul className="h-[calc(100vh-198px)] overflow-auto px-6 py-2">
{bioEnititesPins &&
bioEnititesPins.map(result => (
<BioEntitiesPinsListItem
key={result.bioEntity.name}
name={result.bioEntity.name}
pin={result.bioEntity}
/>
))}
</ul>
);
};
import { twMerge } from 'tailwind-merge';
import { Icon } from '@/shared/Icon';
import { MirnaItems } from '@/types/models';
import { getPinColor } from './PinsListItem.component.utils';
import { PinType } from '../PinsList.types';
import { BioEntity } from '@/types/models';
import { getPinColor } from '../../../ResultsList/PinsList/PinsListItem/PinsListItem.component.utils';
interface MirnaPinsListItemProps {
interface BioEntitiesPinsListItemProps {
name: string;
type: PinType;
pin: MirnaItems;
pin: BioEntity;
}
export const MirnaPinsListItem = ({ name, type, pin }: MirnaPinsListItemProps): JSX.Element => {
export const BioEntitiesPinsListItem = ({
name,
pin,
}: BioEntitiesPinsListItemProps): JSX.Element => {
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">
<Icon name="pin" className={twMerge('mr-2 shrink-0', getPinColor(type))} />
<Icon name="pin" className={twMerge('mr-2 shrink-0', getPinColor('bioEntity'))} />
<p>
Full name: <span className="w-full font-bold">{name}</span>
{pin.stringType}: <span className="w-full font-bold">{name}</span>
</p>
</div>
<ul className="leading-6">
<div className="font-bold">Elements:</div>
{pin.targetParticipants.map(element => {
return (
<li key={element.id} className="my-2 px-2">
<a
href={element.link}
target="_blank"
className="cursor-pointer text-primary-500 underline"
>
{element.type} ({element.resource})
</a>
</li>
);
})}
</ul>
<p className="font-bold leading-6">
Full name: <span className="w-full font-normal">{pin.fullName}</span>
</p>
<p className="font-bold leading-6">
Symbol: <span className="w-full font-normal">{pin.symbol}</span>
</p>
<p className="font-bold leading-6">
Synonyms: <span className="w-full font-normal">{pin.synonyms.join(', ')}</span>
</p>
<ul className="leading-6">
<div className="font-bold">References:</div>
{pin.references.map(reference => {
......
export { BioEntitiesPinsListItem } from './BioEntitiesPinsListItem.component';
export { BioEntitiesPinsList } from './BioEntitiesPinsList.component';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { bioEnititiesResultListSelector } from '@/redux/drawer/drawer.selectors';
import { DrawerHeadingBackwardButton } from '@/shared/DrawerHeadingBackwardButton';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { displayGroupedSearchResults } from '@/redux/drawer/drawer.slice';
import { searchValueSelector } from '@/redux/search/search.selectors';
import { BioEntitiesPinsList } from './BioEntitiesPinsList';
export const BioEntitiesResultsList = (): JSX.Element => {
const dispatch = useAppDispatch();
const bioEntityData = useAppSelector(bioEnititiesResultListSelector);
const searchValue = useAppSelector(searchValueSelector);
const navigateToGroupedSearchResults = (): void => {
dispatch(displayGroupedSearchResults());
};
return (
<div>
<DrawerHeadingBackwardButton
title="BioEntity"
value={searchValue}
backwardFunction={navigateToGroupedSearchResults}
/>
<BioEntitiesPinsList bioEnititesPins={bioEntityData} />
</div>
);
};
export { BioEntitiesResultsList } from './BioEntitiesResultsList.component';
......@@ -9,14 +9,14 @@ import { BioEntitiesSubmapItem } from '@/components/Map/Drawer/SearchDrawerWrapp
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import {
loadingBioEntityStatusSelector,
numberOfBioEntitiesPerModelSelector,
bioEntitiesPerModelSelector,
numberOfBioEntitiesSelector,
} from '@/redux/bioEntity/bioEntity.selectors';
export const BioEntitiesAccordion = (): JSX.Element => {
const bioEntitiesNumber = useAppSelector(numberOfBioEntitiesSelector);
const bioEntitiesState = useAppSelector(loadingBioEntityStatusSelector);
const numberOfBioEntitiesPerModel = useAppSelector(numberOfBioEntitiesPerModelSelector);
const bioEntitiesPerModel = useAppSelector(bioEntitiesPerModelSelector);
return (
<AccordionItem>
......@@ -27,11 +27,12 @@ export const BioEntitiesAccordion = (): JSX.Element => {
</AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel>
{numberOfBioEntitiesPerModel.map(model => (
{bioEntitiesPerModel.map(model => (
<BioEntitiesSubmapItem
key={model.modelName}
mapName={model.modelName}
numberOfEntities={model.numberOfEntities}
bioEntities={model.bioEntities}
/>
))}
</AccordionItemPanel>
......
import { render, RenderResult, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { StoreType } from '@/redux/store';
import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture';
import {
BioEntitiesSubmapItem,
BioEntitiesSubmapItemProps,
} from './BioEntitiesSubmapItem.component';
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { BioEntitiesSubmapItem } from './BioEntitiesSubmapItem.component';
const renderComponent = ({ mapName, numberOfEntities }: BioEntitiesSubmapItemProps): RenderResult =>
render(<BioEntitiesSubmapItem mapName={mapName} numberOfEntities={numberOfEntities} />);
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<BioEntitiesSubmapItem
mapName="main map"
numberOfEntities={21}
bioEntities={bioEntitiesContentFixture}
/>
</Wrapper>,
),
{
store,
}
);
};
describe('BioEntitiesSubmapItem - component', () => {
it('should display map name,number of elements, icon', () => {
renderComponent({ mapName: 'main map', numberOfEntities: 21 });
renderComponent();
expect(screen.getByText('main map (21)')).toBeInTheDocument();
});
......
import { Icon } from '@/shared/Icon';
import { displayBioEntitiesList } from '@/redux/drawer/drawer.slice';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { BioEntityContent } from '@/types/models';
export interface BioEntitiesSubmapItemProps {
mapName: string;
numberOfEntities: string | number;
bioEntities: BioEntityContent[];
}
export const BioEntitiesSubmapItem = ({
mapName,
numberOfEntities,
}: BioEntitiesSubmapItemProps): JSX.Element => (
<div className="flex flex-row flex-nowrap justify-between pl-6 [&:not(:last-of-type)]:pb-4">
<p>
{mapName} ({numberOfEntities})
</p>
<Icon name="arrow" className="h-6 w-6 fill-font-500" />
</div>
);
bioEntities,
}: BioEntitiesSubmapItemProps): JSX.Element => {
const dispatch = useAppDispatch();
const onSubmapClick = (): void => {
dispatch(displayBioEntitiesList(bioEntities));
};
return (
<button
onClick={onSubmapClick}
type="button"
className="flex flex-row flex-nowrap items-center justify-between pl-6 [&:not(:last-of-type)]:pb-4"
>
<p className="text-sm font-normal">
{mapName} ({numberOfEntities})
</p>
<Icon name="arrow" className="h-6 w-6 fill-font-500" />
</button>
);
};
import { assertNever } from '@/utils/assertNever';
import { AccordionsDetails } from '../AccordionsDetails/AccordionsDetails.component';
import { PinItem, PinType } from './PinsList.types';
import { MirnaPinsListItem } from './PinsListItem';
import { PinsListItem } from './PinsListItem';
interface PinsListProps {
pinsList: PinItem[];
......@@ -17,7 +17,7 @@ export const PinsList = ({ pinsList, type }: PinsListProps): JSX.Element => {
<ul className="px-6 py-2">
{pinsList.map(result => {
return result.data.targets.map(pin => (
<MirnaPinsListItem key={pin.name} name={pin.name} type={type} pin={pin} />
<PinsListItem key={pin.name} name={pin.name} type={type} pin={pin} />
));
})}
</ul>
......@@ -32,7 +32,7 @@ export const PinsList = ({ pinsList, type }: PinsListProps): JSX.Element => {
<ul className="px-6 py-2">
{pinsList.map(result => {
return result.data.targets.map(pin => (
<MirnaPinsListItem key={pin.name} name={pin.name} type={type} pin={pin} />
<PinsListItem key={pin.name} name={pin.name} type={type} pin={pin} />
));
})}
</ul>
......@@ -43,7 +43,7 @@ export const PinsList = ({ pinsList, type }: PinsListProps): JSX.Element => {
<ul className="h-[calc(100vh-198px)] overflow-auto px-6 py-2">
{pinsList.map(result => {
return result.data.targets.map(pin => (
<MirnaPinsListItem key={pin.name} name={pin.name} type={type} pin={pin} />
<PinsListItem key={pin.name} name={pin.name} type={type} pin={pin} />
));
})}
</ul>
......
import { twMerge } from 'tailwind-merge';
import { Icon } from '@/shared/Icon';
import { PinDetailsItem } from '@/types/models';
import { getPinColor } from './PinsListItem.component.utils';
import { PinType } from '../PinsList.types';
interface PinsListItemProps {
interface MirnaPinsListItemProps {
name: string;
type: PinType;
onClick: () => void;
pin: PinDetailsItem;
}
export const PinsListItem = ({ name, type, onClick }: PinsListItemProps): JSX.Element => (
<button
className="flex w-full flex-row items-center justify-between pt-4"
onClick={onClick}
type="button"
>
<Icon name="pin" className={twMerge('mr-2 shrink-0', getPinColor(type))} />
<p className="w-full text-left">{name}</p>
<Icon name="chevron-right" className="h-6 w-6 shrink-0" />
</button>
);
export const PinsListItem = ({ name, type, pin }: MirnaPinsListItemProps): JSX.Element => {
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">
<Icon name="pin" className={twMerge('mr-2 shrink-0', getPinColor(type))} />
<p>
Full name: <span className="w-full font-bold">{name}</span>
</p>
</div>
<ul className="leading-6">
<div className="font-bold">Elements:</div>
{'targetParticipants' in pin &&
pin.targetParticipants.map(element => {
return (
<li key={element.id} className="my-2 px-2">
<a
href={element.link}
target="_blank"
className="cursor-pointer text-primary-500 underline"
>
{element.type} ({element.resource})
</a>
</li>
);
})}
</ul>
<ul className="leading-6">
<div className="font-bold">References:</div>
{pin.references.map(reference => {
return (
<li key={reference.id} className="my-2 px-2">
<a
href={reference.article?.link}
target="_blank"
className="cursor-pointer text-primary-500 underline"
>
{reference.type} ({reference.resource})
</a>
</li>
);
})}
</ul>
</div>
);
};
export { PinsListItem } from './PinsListItem.component';
export { MirnaPinsListItem } from './MirnaPinsListItem.component';
......@@ -20,6 +20,7 @@ const INITIAL_STATE: InitialStoreState = {
currentStep: 2,
stepType: 'drugs',
selectedValue: undefined,
listOfBioEnitites: [],
},
},
drugs: {
......
import { SearchDrawerWrapper } from '@/components/Map/Drawer/SearchDrawerWrapper';
import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture';
import { drugFixture } from '@/models/fixtures/drugFixtures';
import { drawerSearchStepOneFixture } from '@/redux/drawer/drawerFixture';
import { StoreType } from '@/redux/store';
import {
......@@ -42,6 +40,7 @@ describe('SearchDrawerWrapper - component', () => {
currentStep: 2,
stepType: 'bioEntity',
selectedValue: undefined,
listOfBioEnitites: [],
},
},
});
......@@ -58,42 +57,11 @@ describe('SearchDrawerWrapper - component', () => {
currentStep: 2,
stepType: 'drugs',
selectedValue: undefined,
listOfBioEnitites: [],
},
},
});
expect(screen.getByTestId('search-second-step')).toBeInTheDocument();
});
it('should display the third step for value type bioEntity', () => {
renderComponent({
drawer: {
isOpen: true,
drawerName: 'search',
searchDrawerState: {
currentStep: 3,
stepType: 'bioEntity',
selectedValue: bioEntityContentFixture,
},
},
});
expect(screen.getByTestId('search-third-step')).toBeInTheDocument();
});
it('should display the third step for value type drugs', () => {
renderComponent({
drawer: {
isOpen: true,
drawerName: 'search',
searchDrawerState: {
currentStep: 3,
stepType: 'drugs',
selectedValue: drugFixture,
},
},
});
expect(screen.getByTestId('search-third-step')).toBeInTheDocument();
});
});
......@@ -7,6 +7,7 @@ import {
import { useSelector } from 'react-redux';
import { ResultsList } from '@/components/Map/Drawer/SearchDrawerWrapper/ResultsList';
import { GroupedSearchResults } from '@/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults';
import { BioEntitiesResultsList } from './BioEntitiesResultsList';
export const SearchDrawerWrapper = (): JSX.Element => {
const currentStep = useSelector(currentStepDrawerStateSelector);
......@@ -21,7 +22,9 @@ export const SearchDrawerWrapper = (): JSX.Element => {
{currentStep === STEP.FIRST && <GroupedSearchResults />}
{/* 2nd step for bioEntities aka content */}
{currentStep === STEP.SECOND && isBioEntityType && (
<div data-testid="search-second-step">The second step</div>
<div data-testid="search-second-step">
<BioEntitiesResultsList />
</div>
)}
{/* 2nd step for drugs,chemicals,mirna */}
{currentStep === STEP.SECOND && isChemicalsDrugsOrMirnaType && (
......@@ -29,14 +32,6 @@ export const SearchDrawerWrapper = (): JSX.Element => {
<ResultsList />
</div>
)}
{/* last step for bioentity */}
{currentStep === STEP.THIRD && isBioEntityType && (
<div data-testid="search-third-step">The third step</div>
)}
{/* last step for drugs,chemicals,mirna */}
{currentStep === STEP.THIRD && isChemicalsDrugsOrMirnaType && (
<div data-testid="search-third-step">The third step</div>
)}
</div>
);
};
......@@ -22,21 +22,23 @@ export const numberOfBioEntitiesSelector = createSelector(bioEntitySelector, sta
state.data ? state.data.length : SIZE_OF_EMPTY_ARRAY,
);
export const numberOfBioEntitiesPerModelSelector = createSelector(rootSelector, state => {
export const bioEntitiesPerModelSelector = createSelector(rootSelector, state => {
const {
models,
bioEntity: { data: bioEntities },
} = state;
const numberOfBioEntitiesPerModel = (models.data || []).map(model => {
const bioEntitiesPerModel = (models.data || []).map(model => {
const bioEntitiesInGivenModel = (bioEntities || []).filter(
entity => model.idObject === entity.bioEntity.model,
);
return { modelName: model.name, numberOfEntities: bioEntitiesInGivenModel.length };
return {
modelName: model.name,
numberOfEntities: bioEntitiesInGivenModel.length,
bioEntities: bioEntitiesInGivenModel,
};
});
return numberOfBioEntitiesPerModel.filter(
model => model.numberOfEntities !== SIZE_OF_EMPTY_ARRAY,
);
return bioEntitiesPerModel.filter(model => model.numberOfEntities !== SIZE_OF_EMPTY_ARRAY);
});
......@@ -21,6 +21,7 @@ const INITIAL_STATE: DrawerState = {
currentStep: 0,
stepType: 'none',
selectedValue: undefined,
listOfBioEnitites: [],
},
};
......
......@@ -45,6 +45,16 @@ export const displayMirnaListReducer = (state: DrawerState): void => {
state.searchDrawerState.stepType = 'mirna';
};
export const displayBioEntitiesListReducer = (
state: DrawerState,
action: PayloadAction<DrawerState['searchDrawerState']['listOfBioEnitites']>,
): void => {
state.drawerName = 'search';
state.searchDrawerState.currentStep = STEP.SECOND;
state.searchDrawerState.listOfBioEnitites = action.payload;
state.searchDrawerState.stepType = 'bioEntity';
};
export const displayGroupedSearchResultsReducer = (state: DrawerState): void => {
state.searchDrawerState.currentStep = STEP.FIRST;
state.searchDrawerState.stepType = 'none';
......
......@@ -56,3 +56,7 @@ export const resultListSelector = createSelector(rootSelector, state => {
return assertNever(selectedType);
}
});
export const bioEnititiesResultListSelector = createSelector(rootSelector, state => {
return state.drawer.searchDrawerState.listOfBioEnitites;
});
......@@ -2,6 +2,7 @@ import { DrawerState } from '@/redux/drawer/drawer.types';
import { createSlice } from '@reduxjs/toolkit';
import {
closeDrawerReducer,
displayBioEntitiesListReducer,
displayChemicalsListReducer,
displayDrugsListReducer,
displayEntityDetailsReducer,
......@@ -19,6 +20,7 @@ const initialState: DrawerState = {
currentStep: 0,
stepType: 'none',
selectedValue: undefined,
listOfBioEnitites: [],
},
};
......@@ -33,6 +35,7 @@ const drawerSlice = createSlice({
displayDrugsList: displayDrugsListReducer,
displayChemicalsList: displayChemicalsListReducer,
displayMirnaList: displayMirnaListReducer,
displayBioEntitiesList: displayBioEntitiesListReducer,
displayGroupedSearchResults: displayGroupedSearchResultsReducer,
displayEntityDetails: displayEntityDetailsReducer,
},
......@@ -46,6 +49,7 @@ export const {
displayDrugsList,
displayChemicalsList,
displayMirnaList,
displayBioEntitiesList,
displayGroupedSearchResults,
displayEntityDetails,
} = drawerSlice.actions;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment