Skip to content
Snippets Groups Projects
Commit fbb8ffcd authored by Tadeusz Miesiąc's avatar Tadeusz Miesiąc
Browse files

feat(bioentity:submaplink): allow user to open submap by clicking on submaplink on map

parent e82dbe4c
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...,!73feat(bioentity:submaplink): allow user to open submap by clicking on submaplink on map
Pipeline #82853 passed
Showing
with 351 additions and 28 deletions
import { Icon } from '@/shared/Icon';
import { Reference } from '@/types/models';
type AnnotationItemProps = Pick<Reference, 'link' | 'type' | 'resource'>;
export const AnnotationItem = ({ link, type, resource }: AnnotationItemProps): JSX.Element => (
<a className="pl-3 text-sm font-normal" href={link?.toString()} target="_blank">
<div className="flex justify-between">
<span>
Source:{' '}
<b className="font-semibold">
{type} ({resource})
</b>
</span>
<Icon name="arrow" className="h-6 w-6 fill-font-500" />
</div>
</a>
);
export { AnnotationItem } from './AnnotationItem.component';
import { StoreType } from '@/redux/store';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { act, render, screen } from '@testing-library/react';
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 { AssociatedSubmap } from './AssociatedSubmap.component';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<AssociatedSubmap />
</Wrapper>,
),
{
store,
}
);
};
const MAIN_MAP_ID = 5053;
const HISTAMINE_MAP_ID = 5052;
describe('AssociatedSubmap - component', () => {
it('should not display component when can not find asociated map model', () => {
renderComponent({
bioEntity: {
...BIOENTITY_INITIAL_STATE_MOCK,
data: BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK,
},
drawer: {
...DRAWER_INITIAL_STATE,
bioEntityDrawerState: {
bioentityId: bioEntityContentFixture.bioEntity.id,
},
},
models: {
...MODELS_INITIAL_STATE_MOCK,
},
});
expect(screen.queryByTestId('associated-submap')).not.toBeInTheDocument();
});
it('should render component when associated map model is found', () => {
renderComponent({
bioEntity: {
...BIOENTITY_INITIAL_STATE_MOCK,
data: BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK,
},
drawer: {
...DRAWER_INITIAL_STATE,
bioEntityDrawerState: {
bioentityId: bioEntityContentFixture.bioEntity.id,
},
},
models: {
...MODELS_INITIAL_STATE_MOCK,
data: MODELS_MOCK_SHORT,
},
});
expect(screen.getByTestId('associated-submap')).toBeInTheDocument();
});
describe('when map is already opened', () => {
it('should open submap and set it to active on open submap button click', async () => {
const { store } = renderComponent({
map: {
data: initialMapDataFixture,
loading: 'succeeded',
error: { name: '', message: '' },
openedMaps: openedMapsInitialValueFixture,
},
bioEntity: {
...BIOENTITY_INITIAL_STATE_MOCK,
data: BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK,
},
drawer: {
...DRAWER_INITIAL_STATE,
bioEntityDrawerState: {
bioentityId: bioEntityContentFixture.bioEntity.id,
},
},
models: {
...MODELS_INITIAL_STATE_MOCK,
data: MODELS_MOCK_SHORT,
},
});
const {
data: { modelId },
openedMaps,
} = store.getState().map;
expect(modelId).toBe(ZERO);
expect(openedMaps).not.toContainEqual({
modelId: HISTAMINE_MAP_ID,
modelName: 'Histamine signaling',
lastPosition: { x: 0, y: 0, z: 0 },
});
const openSubmapButton = screen.getByRole('button', { name: 'Open submap' });
await act(() => {
openSubmapButton.click();
});
const {
data: { modelId: newModelId },
openedMaps: newOpenedMaps,
} = store.getState().map;
expect(newOpenedMaps).toContainEqual({
modelId: HISTAMINE_MAP_ID,
modelName: 'Histamine signaling',
lastPosition: { x: 0, y: 0, z: 0 },
});
expect(newModelId).toBe(HISTAMINE_MAP_ID);
});
it('should set map active on open submap button click', async () => {
const { store } = renderComponent({
map: {
data: {
...initialMapDataFixture,
modelId: MAIN_MAP_ID,
},
loading: 'succeeded',
error: { name: '', message: '' },
openedMaps: openedMapsThreeSubmapsFixture,
},
bioEntity: {
...BIOENTITY_INITIAL_STATE_MOCK,
data: BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK,
},
drawer: {
...DRAWER_INITIAL_STATE,
bioEntityDrawerState: {
bioentityId: bioEntityContentFixture.bioEntity.id,
},
},
models: {
...MODELS_INITIAL_STATE_MOCK,
data: MODELS_MOCK_SHORT,
},
});
const openSubmapButton = screen.getByRole('button', { name: 'Open submap' });
await act(() => {
openSubmapButton.click();
});
const {
map: {
data: { modelId },
openedMaps,
},
} = store.getState();
const histamineMap = openedMaps.filter(map => map.modelName === 'Histamine signaling');
expect(histamineMap.length).toBe(SIZE_OF_ARRAY_WITH_ONE_ELEMENT);
expect(modelId).toBe(HISTAMINE_MAP_ID);
});
});
});
import { useOpenSubmap } from '@/hooks/useOpenSubmaps';
import { searchedFromMapBioEntityElementRelatedSubmapSelector } from '@/redux/bioEntity/bioEntity.selectors';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { Button } from '@/shared/Button';
export const AssociatedSubmap = (): React.ReactNode => {
const relatedSubmap = useAppSelector(searchedFromMapBioEntityElementRelatedSubmapSelector);
const { openSubmap } = useOpenSubmap({
modelId: relatedSubmap?.idObject,
modelName: relatedSubmap?.name,
});
if (!relatedSubmap) {
return null;
}
return (
<div
data-testid="associated-submap"
className="flex flex-row flex-nowrap items-center justify-between"
>
<p>Associated Submap: </p>
<Button className="max-h-8" variantStyles="ghost" onClick={openSubmap}>
Open submap
</Button>
</div>
);
};
export { AssociatedSubmap } from './AssociatedSubmap.component';
......@@ -11,6 +11,12 @@ import {
bioEntityContentFixture,
} from '@/models/fixtures/bioEntityContentsFixture';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
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 { MODELS_INITIAL_STATE_MOCK } from '@/redux/models/models.mock';
import { BioEntityDrawer } from './BioEntityDrawer.component';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
......@@ -177,5 +183,26 @@ describe('BioEntityDrawer - component', () => {
screen.getByText(bioEntity.references[0].resource, { exact: false }),
).toBeInTheDocument();
});
it('should display associated submaps if bio entity links to submap', () => {
renderComponent({
bioEntity: {
...BIOENTITY_INITIAL_STATE_MOCK,
data: BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK,
},
drawer: {
...DRAWER_INITIAL_STATE,
bioEntityDrawerState: {
bioentityId: bioEntityContentFixture.bioEntity.id,
},
},
models: {
...MODELS_INITIAL_STATE_MOCK,
data: MODELS_MOCK_SHORT,
},
});
expect(screen.getByTestId('associated-submap')).toBeInTheDocument();
});
});
});
import { DrawerHeading } from '@/shared/DrawerHeading';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { searchedFromMapBioEntityElement } from '@/redux/bioEntity/bioEntity.selectors';
import { Icon } from '@/shared/Icon';
import { ZERO } from '@/constants/common';
import { AnnotationItem } from './AnnotationItem';
import { AssociatedSubmap } from './AssociatedSubmap';
export const BioEntityDrawer = (): React.ReactNode => {
const bioEntityData = useAppSelector(searchedFromMapBioEntityElement);
......@@ -10,6 +12,8 @@ export const BioEntityDrawer = (): React.ReactNode => {
return null;
}
const isReferenceAvailable = bioEntityData.references.length > ZERO;
return (
<div className="h-full max-h-full" data-testid="bioentity-drawer">
<DrawerHeading
......@@ -29,27 +33,20 @@ export const BioEntityDrawer = (): React.ReactNode => {
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()}
<h3 className="font-semibold">
Annotations:{' '}
{!isReferenceAvailable && <span className="font-normal">No annotations</span>}
</h3>
{isReferenceAvailable &&
bioEntityData.references.map(reference => (
<AnnotationItem
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>
);
})}
type={reference.type}
link={reference.link}
resource={reference.resource}
/>
))}
<AssociatedSubmap />
</div>
</div>
);
......
export const SIZE_OF_EMPTY_ARRAY = 0;
export const SIZE_OF_ARRAY_WITH_ONE_ELEMENT = 1;
export const ZERO = 0;
export const FIRST_ARRAY_ELEMENT = 0;
......
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { mapOpenedMapsSelector } from '@/redux/map/map.selectors';
import { openMapAndSetActive, setActiveMap } from '@/redux/map/map.slice';
import { modelsDataSelector } from '@/redux/models/models.selectors';
import { useCallback } from 'react';
type UseOpenSubmapProps = {
modelId: number | undefined;
modelName: string | undefined;
};
type UseOpenSubmapReturnType = {
openSubmap: () => void;
isItPossibleToOpenMap: boolean;
};
export const useOpenSubmap = ({
modelId,
modelName,
}: UseOpenSubmapProps): UseOpenSubmapReturnType => {
const openedMaps = useAppSelector(mapOpenedMapsSelector);
const models = useAppSelector(modelsDataSelector);
const dispatch = useAppDispatch();
const isMapAlreadyOpened = openedMaps.some(map => map.modelId === modelId);
const isMapExist = models.some(model => model.idObject === modelId);
const isItPossibleToOpenMap = modelId && modelName && isMapExist;
const openSubmap = useCallback(() => {
if (!isItPossibleToOpenMap) {
return;
}
if (isMapAlreadyOpened) {
dispatch(setActiveMap({ modelId }));
} else {
dispatch(openMapAndSetActive({ modelId, modelName }));
}
}, [dispatch, isItPossibleToOpenMap, isMapAlreadyOpened, modelId, modelName]);
return { openSubmap, isItPossibleToOpenMap: Boolean(isItPossibleToOpenMap) };
};
import { DEFAULT_ERROR } from '@/constants/errors';
import { BioEntity, BioEntityContent } from '@/types/models';
import { bioEntityContentFixture } from '@/models/fixtures/bioEntityContentsFixture';
import { MultiSearchData } from '@/types/fetchDataState';
import { BioEntityContentsState } from './bioEntity.types';
export const BIOENTITY_INITIAL_STATE_MOCK: BioEntityContentsState = {
......@@ -6,3 +9,20 @@ export const BIOENTITY_INITIAL_STATE_MOCK: BioEntityContentsState = {
loading: 'idle',
error: DEFAULT_ERROR,
};
export const BIO_ENTITY_LINKING_TO_SUBMAP: BioEntity = {
...bioEntityContentFixture.bioEntity,
submodel: {
mapId: 5052,
type: 'DONWSTREAM_TARGETS',
},
};
export const BIO_ENTITY_LINKING_TO_SUBMAP_DATA_MOCK: MultiSearchData<BioEntityContent[]>[] = [
{
data: [{ bioEntity: BIO_ENTITY_LINKING_TO_SUBMAP, perfect: false }],
searchQueryElement: '',
loading: 'succeeded',
error: DEFAULT_ERROR,
},
];
import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { rootSelector } from '@/redux/root/root.selectors';
import { MultiSearchData } from '@/types/fetchDataState';
import { BioEntity, BioEntityContent } from '@/types/models';
import { BioEntity, BioEntityContent, MapModel } from '@/types/models';
import { createSelector } from '@reduxjs/toolkit';
import {
currentSearchedBioEntityId,
......@@ -23,12 +23,16 @@ export const bioEntitiesForSelectedSearchElement = createSelector(
export const searchedFromMapBioEntityElement = createSelector(
bioEntitiesForSelectedSearchElement,
currentSearchedBioEntityId,
(bioEntitiesState, currentBioEntityId): BioEntity | undefined => {
return (
bioEntitiesState &&
bioEntitiesState.data?.find(({ bioEntity }) => bioEntity.id === currentBioEntityId)?.bioEntity
);
},
(bioEntitiesState, currentBioEntityId): BioEntity | undefined =>
bioEntitiesState &&
bioEntitiesState.data?.find(({ bioEntity }) => bioEntity.id === currentBioEntityId)?.bioEntity,
);
export const searchedFromMapBioEntityElementRelatedSubmapSelector = createSelector(
searchedFromMapBioEntityElement,
modelsDataSelector,
(bioEntity, models): MapModel | undefined =>
models.find(({ idObject }) => idObject === bioEntity?.submodel?.mapId),
);
export const loadingBioEntityStatusSelector = createSelector(
......
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