Skip to content
Snippets Groups Projects
Commit 781de2b8 authored by Adrian Orłów's avatar Adrian Orłów
Browse files

Merge branch 'development' into feature/overview-image-interactive

parents 31ee8bff f51cab97
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...,!77feat: add overview image interactive layer
Pipeline #83467 passed
Showing
with 438 additions and 25 deletions
...@@ -41,4 +41,16 @@ describe('TopBar - component', () => { ...@@ -41,4 +41,16 @@ describe('TopBar - component', () => {
expect(isOpen).toBe(true); expect(isOpen).toBe(true);
expect(drawerName).toBe('submaps'); expect(drawerName).toBe('submaps');
}); });
it('should open overlays drawer on overlays button click', () => {
const { store } = renderComponent({ drawer: initialStateFixture });
const button = screen.getByRole('button', { name: 'Overlays' });
button.click();
const { isOpen, drawerName } = store.getState().drawer;
expect(isOpen).toBe(true);
expect(drawerName).toBe('overlays');
});
}); });
import { SearchBar } from '@/components/FunctionalArea/TopBar/SearchBar'; import { SearchBar } from '@/components/FunctionalArea/TopBar/SearchBar';
import { UserAvatar } from '@/components/FunctionalArea/TopBar/UserAvatar'; import { UserAvatar } from '@/components/FunctionalArea/TopBar/UserAvatar';
import { openSubmapsDrawer } from '@/redux/drawer/drawer.slice'; import { openOverlaysDrawer, openSubmapsDrawer } from '@/redux/drawer/drawer.slice';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { Button } from '@/shared/Button'; import { Button } from '@/shared/Button';
...@@ -11,6 +11,10 @@ export const TopBar = (): JSX.Element => { ...@@ -11,6 +11,10 @@ export const TopBar = (): JSX.Element => {
dispatch(openSubmapsDrawer()); dispatch(openSubmapsDrawer());
}; };
const onOverlaysClick = (): void => {
dispatch(openOverlaysDrawer());
};
return ( return (
<div className="flex h-16 w-full flex-row items-center justify-between border-b border-font-500 border-opacity-[0.12] bg-white py-4 pl-7 pr-6"> <div className="flex h-16 w-full flex-row items-center justify-between border-b border-font-500 border-opacity-[0.12] bg-white py-4 pl-7 pr-6">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
...@@ -19,7 +23,7 @@ export const TopBar = (): JSX.Element => { ...@@ -19,7 +23,7 @@ export const TopBar = (): JSX.Element => {
<Button icon="plus" isIcon isFrontIcon className="ml-8 mr-4" onClick={onSubmapsClick}> <Button icon="plus" isIcon isFrontIcon className="ml-8 mr-4" onClick={onSubmapsClick}>
Submaps Submaps
</Button> </Button>
<Button icon="plus" isIcon isFrontIcon> <Button icon="plus" isIcon isFrontIcon onClick={onOverlaysClick}>
Overlays Overlays
</Button> </Button>
</div> </div>
......
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 { ...@@ -11,6 +11,12 @@ import {
bioEntityContentFixture, bioEntityContentFixture,
} from '@/models/fixtures/bioEntityContentsFixture'; } from '@/models/fixtures/bioEntityContentsFixture';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; 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'; import { BioEntityDrawer } from './BioEntityDrawer.component';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
...@@ -177,5 +183,26 @@ describe('BioEntityDrawer - component', () => { ...@@ -177,5 +183,26 @@ describe('BioEntityDrawer - component', () => {
screen.getByText(bioEntity.references[0].resource, { exact: false }), screen.getByText(bioEntity.references[0].resource, { exact: false }),
).toBeInTheDocument(); ).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 { DrawerHeading } from '@/shared/DrawerHeading';
import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { searchedFromMapBioEntityElement } from '@/redux/bioEntity/bioEntity.selectors'; 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 => { export const BioEntityDrawer = (): React.ReactNode => {
const bioEntityData = useAppSelector(searchedFromMapBioEntityElement); const bioEntityData = useAppSelector(searchedFromMapBioEntityElement);
...@@ -10,6 +12,8 @@ export const BioEntityDrawer = (): React.ReactNode => { ...@@ -10,6 +12,8 @@ export const BioEntityDrawer = (): React.ReactNode => {
return null; return null;
} }
const isReferenceAvailable = bioEntityData.references.length > ZERO;
return ( return (
<div className="h-full max-h-full" data-testid="bioentity-drawer"> <div className="h-full max-h-full" data-testid="bioentity-drawer">
<DrawerHeading <DrawerHeading
...@@ -29,27 +33,20 @@ export const BioEntityDrawer = (): React.ReactNode => { ...@@ -29,27 +33,20 @@ export const BioEntityDrawer = (): React.ReactNode => {
Full name: <b className="font-semibold">{bioEntityData.fullName}</b> Full name: <b className="font-semibold">{bioEntityData.fullName}</b>
</div> </div>
)} )}
<h3 className="font-semibold">Annotations:</h3> <h3 className="font-semibold">
{bioEntityData.references.map(reference => { Annotations:{' '}
return ( {!isReferenceAvailable && <span className="font-normal">No annotations</span>}
<a </h3>
className="pl-3 text-sm font-normal" {isReferenceAvailable &&
href={reference.link?.toString()} bioEntityData.references.map(reference => (
<AnnotationItem
key={reference.id} key={reference.id}
target="_blank" type={reference.type}
> link={reference.link}
<div className="flex justify-between"> resource={reference.resource}
<span> />
Source:{' '} ))}
<b className="font-semibold"> <AssociatedSubmap />
{reference?.type} ({reference.resource})
</b>
</span>
<Icon name="arrow" className="h-6 w-6 fill-font-500" />
</div>
</a>
);
})}
</div> </div>
</div> </div>
); );
......
...@@ -5,6 +5,7 @@ import { twMerge } from 'tailwind-merge'; ...@@ -5,6 +5,7 @@ import { twMerge } from 'tailwind-merge';
import { ReactionDrawer } from './ReactionDrawer'; import { ReactionDrawer } from './ReactionDrawer';
import { SearchDrawerWrapper as SearchDrawerContent } from './SearchDrawerWrapper'; import { SearchDrawerWrapper as SearchDrawerContent } from './SearchDrawerWrapper';
import { SubmapsDrawer } from './SubmapsDrawer'; import { SubmapsDrawer } from './SubmapsDrawer';
import { OverlaysDrawer } from './OverlaysDrawer';
import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component'; import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component';
export const Drawer = (): JSX.Element => { export const Drawer = (): JSX.Element => {
...@@ -21,6 +22,7 @@ export const Drawer = (): JSX.Element => { ...@@ -21,6 +22,7 @@ export const Drawer = (): JSX.Element => {
{isOpen && drawerName === 'search' && <SearchDrawerContent />} {isOpen && drawerName === 'search' && <SearchDrawerContent />}
{isOpen && drawerName === 'submaps' && <SubmapsDrawer />} {isOpen && drawerName === 'submaps' && <SubmapsDrawer />}
{isOpen && drawerName === 'reaction' && <ReactionDrawer />} {isOpen && drawerName === 'reaction' && <ReactionDrawer />}
{isOpen && drawerName === 'overlays' && <OverlaysDrawer />}
{isOpen && drawerName === 'bio-entity' && <BioEntityDrawer />} {isOpen && drawerName === 'bio-entity' && <BioEntityDrawer />}
</div> </div>
); );
......
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { StoreType } from '@/redux/store';
import {
OVERLAYS_PUBLIC_FETCHED_STATE_MOCK,
PUBLIC_OVERLAYS_MOCK,
} from '@/redux/overlays/overlays.mock';
import { GeneralOverlays } from './GeneralOverlays.component';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<GeneralOverlays />
</Wrapper>,
),
{
store,
}
);
};
describe('GeneralOverlays - component', () => {
describe('render', () => {
const publicOverlaysNames = PUBLIC_OVERLAYS_MOCK.map(({ name }) => name);
it.each(publicOverlaysNames)('should display %s overlay item', source => {
renderComponent({ overlays: OVERLAYS_PUBLIC_FETCHED_STATE_MOCK });
expect(screen.getByText(source)).toBeInTheDocument();
});
});
describe('view overlays', () => {
// TODO implement when connecting logic to component
it.skip('should allow to turn on more then one overlay', () => {});
});
});
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { overlaysDataSelector } from '@/redux/overlays/overlays.selectors';
import { OverlayListItem } from './OverlayListItem';
export const GeneralOverlays = (): JSX.Element => {
const generalPublicOverlays = useAppSelector(overlaysDataSelector);
return (
<div className="border-b border-b-divide p-6">
<p className="mb-5 text-sm font-semibold">General Overlays:</p>
<ul>
{generalPublicOverlays.map(overlay => (
<OverlayListItem key={overlay.idObject} name={overlay.name} />
))}
</ul>
</div>
);
};
import { StoreType } from '@/redux/store';
import { render, screen } from '@testing-library/react';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { OverlayListItem } from './OverlayListItem.component';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<OverlayListItem name="Ageing brain" />
</Wrapper>,
),
{
store,
}
);
};
describe('OverlayListItem - component', () => {
it('should render component with correct properties', () => {
renderComponent();
expect(screen.getByText('Ageing brain')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'View' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument();
});
// TODO implement when connecting logic to component
it.skip('should trigger view overlays on view button click', () => {});
// TODO implement when connecting logic to component
it.skip('should trigger download overlay to PC on download button click', () => {});
});
import { Button } from '@/shared/Button';
interface OverlayListItemProps {
name: string;
}
export const OverlayListItem = ({ name }: OverlayListItemProps): JSX.Element => {
const onViewOverlay = (): void => {};
const onDownloadOverlay = (): void => {};
return (
<li className="flex flex-row flex-nowrap justify-between pl-5 [&:not(:last-of-type)]:mb-4">
<span>{name}</span>
<div className="flex flex-row flex-nowrap">
<Button variantStyles="ghost" className="mr-4 max-h-8" onClick={onViewOverlay}>
View
</Button>
<Button className="max-h-8" variantStyles="ghost" onClick={onDownloadOverlay}>
Download
</Button>
</div>
</li>
);
};
export { OverlayListItem } from './OverlayListItem.component';
export { GeneralOverlays } from './GeneralOverlays.component';
import { DrawerHeading } from '@/shared/DrawerHeading';
import { GeneralOverlays } from './GeneralOverlays';
export const OverlaysDrawer = (): JSX.Element => {
return (
<div data-testid="overlays-drawer">
<DrawerHeading title="Overlays" />
<GeneralOverlays />
</div>
);
};
export { OverlaysDrawer } from './OverlaysDrawer.component';
...@@ -73,7 +73,7 @@ describe('BioEntitiesAccordion - component', () => { ...@@ -73,7 +73,7 @@ describe('BioEntitiesAccordion - component', () => {
}); });
expect(screen.getByText('Content (10)')).toBeInTheDocument(); expect(screen.getByText('Content (10)')).toBeInTheDocument();
expect(screen.getByText('Core PD map (5)')).toBeInTheDocument(); expect(screen.getByText('Core PD map (4)')).toBeInTheDocument();
expect(screen.getByText('Histamine signaling (2)')).toBeInTheDocument(); expect(screen.getByText('Histamine signaling (1)')).toBeInTheDocument();
}); });
}); });
...@@ -8,3 +8,9 @@ export type PinItem = { ...@@ -8,3 +8,9 @@ export type PinItem = {
}; };
export type PinTypeWithNone = PinType | 'none'; export type PinTypeWithNone = PinType | 'none';
export type AvailableSubmaps = {
id: number;
modelId: number;
name: string;
};
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