diff --git a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx index 03c3f49587c5fb3ea0f8a6fb4bfd58549f77ee26..3e9efc13be0c4229d62b0a7a33056d1b9b413bc5 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/BioEntityDrawer.component.tsx @@ -82,7 +82,10 @@ export const BioEntityDrawer = (): React.ReactNode => { </CollapsibleSection> </> )} - <OverlayData /> + <OverlayData + isShowGroupedOverlays={Boolean(relatedSubmap)} + isShowOverlayBioEntityName={Boolean(relatedSubmap)} + /> </div> </div> ); diff --git a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/GroupedOverlayAxes/GroupedOverlayAxes.components.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/GroupedOverlayAxes/GroupedOverlayAxes.components.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8f073a57cf1e74197ae74f84d25ff286b994f12d --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/GroupedOverlayAxes/GroupedOverlayAxes.components.test.tsx @@ -0,0 +1,55 @@ +import { ZERO } from '@/constants/common'; +import { overlayFixture } from '@/models/fixtures/overlaysFixture'; +import { MapOverlay } from '@/types/models'; +import { render, screen } from '@testing-library/react'; +import { OverlayDataAxis } from '../OverlayData.types'; +import { GroupedOverlayAxes } from './GroupedOverlayAxes.components'; + +const BASE_AXIS: OverlayDataAxis = { + id: 123, + title: 'axis title', + value: 2137, + color: '#FFFFFF', + geneVariants: undefined, + overlayId: overlayFixture.idObject, +}; + +const renderComponent = (axes: OverlayDataAxis[], overlay: MapOverlay): void => { + render(<GroupedOverlayAxes axes={axes} overlay={overlay} />); +}; + +describe('GroupedOverlayAxes', () => { + describe('when axes array is empty', () => { + beforeEach(() => { + renderComponent([], overlayFixture); + }); + + it('should not render title', () => { + expect(screen.queryAllByText(overlayFixture.name).length).toEqual(ZERO); + }); + }); + + describe('when axes array is present', () => { + const AXES = [ + { + ...BASE_AXIS, + title: 'axis 1', + }, + { + ...BASE_AXIS, + title: 'axis 2', + }, + ]; + + beforeEach(() => { + renderComponent(AXES, overlayFixture); + }); + it('should render title', () => { + expect(screen.getByText(overlayFixture.name)).toBeInTheDocument(); + }); + + it.each(AXES)('should render overlay axis', ({ title }) => { + expect(screen.getByText(title)).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/GroupedOverlayAxes/GroupedOverlayAxes.components.tsx b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/GroupedOverlayAxes/GroupedOverlayAxes.components.tsx new file mode 100644 index 0000000000000000000000000000000000000000..de4e92086ea87acc0646584c1b22424d447693cc --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/GroupedOverlayAxes/GroupedOverlayAxes.components.tsx @@ -0,0 +1,33 @@ +import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; +import { MapOverlay } from '@/types/models'; +import { useMemo } from 'react'; +import { OverlayAxis } from '../OverlayAxis'; +import { OverlayDataAxis } from '../OverlayData.types'; +import { getAxesSortedByValue } from '../utils/getAxesSortedByValue'; + +interface Props { + overlay: MapOverlay; + axes: OverlayDataAxis[]; +} + +export const GroupedOverlayAxes = ({ overlay, axes }: Props): JSX.Element | null => { + const { idObject, name } = overlay; + const overlayAxes = axes.filter(axis => axis.overlayId === idObject); + const sortedAxes = useMemo(() => getAxesSortedByValue(overlayAxes), [overlayAxes]); + + if (overlayAxes.length === SIZE_OF_EMPTY_ARRAY) { + return null; + } + + return ( + <div className="flex flex-col gap-2 rounded-lg border border-divide p-4"> + <div className="font-bold" data-testid="grouped-overlay-title"> + {name} + </div> + <div className="my-1 h-[1px] w-full bg-divide" /> + {sortedAxes.map(axis => ( + <OverlayAxis key={axis.title} axis={axis} /> + ))} + </div> + ); +}; diff --git a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/GroupedOverlayAxes/index.ts b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/GroupedOverlayAxes/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0edd1357aaf33eca0b397afbdee20152f84439b0 --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/GroupedOverlayAxes/index.ts @@ -0,0 +1 @@ +export { GroupedOverlayAxes } from './GroupedOverlayAxes.components'; diff --git a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayAxis/OverlayAxis.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayAxis/OverlayAxis.component.test.tsx index 0bfa8529c292b9f36d8753f760631701f752328d..c60c1095dc4660b26b24beca46868917fef08cf5 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayAxis/OverlayAxis.component.test.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayAxis/OverlayAxis.component.test.tsx @@ -15,6 +15,7 @@ const BASE_AXIS: OverlayDataAxis = { value: 2137, color: '#FFFFFF', geneVariants: undefined, + overlayId: 1, }; describe('OverlayAxis - component', () => { diff --git a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.test.tsx b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.test.tsx index ac239c2fc2e98eeb77c5ff51d07d4b95c07725d9..3704c38f3ff595b95f78a5aa0760005bb08b95bf 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.test.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.test.tsx @@ -15,15 +15,18 @@ 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 { OverlayData } from './OverlayData.component'; +import { OverlayData, Props } from './OverlayData.component'; -const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { +const renderComponent = ( + initialStoreState: InitialStoreState = {}, + props: Props = {}, +): { store: StoreType } => { const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); return ( render( <Wrapper> - <OverlayData /> + <OverlayData {...props} /> </Wrapper>, ), { @@ -113,4 +116,157 @@ describe('OverlayData - component', () => { expect(screen.getByText('axis name')).toBeInTheDocument(); }); }); + + describe('when axes list is present and isShowGroupedOverlays=true', () => { + beforeEach(() => { + const OVERLAY_ID = overlayFixture.idObject; + const BIO_ENTITY = MOCKED_OVERLAY_BIO_ENTITY_RENDER[0]; + + renderComponent( + { + ...INITIAL_STORE_STATE_MOCK, + map: { + ...initialMapStateFixture, + data: { ...initialMapStateFixture.data, modelId: CORE_PD_MODEL_MOCK.idObject }, + }, + overlays: { + ...INITIAL_STORE_STATE_MOCK.overlays, + data: [{ ...overlayFixture, name: 'overlay name' }], + }, + bioEntity: { + data: [ + { + searchQueryElement: '', + loading: 'pending', + error: { name: '', message: '' }, + data: [ + { + ...bioEntitiesContentFixture[0], + bioEntity: { + ...bioEntitiesContentFixture[0].bioEntity, + id: BIO_ENTITY.id, + }, + }, + ], + }, + ], + loading: 'pending', + error: { name: '', message: '' }, + }, + overlayBioEntity: { + ...OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, + overlaysId: [OVERLAY_ID], + data: { + [OVERLAY_ID]: { + [CORE_PD_MODEL_MOCK.idObject]: [ + { + ...BIO_ENTITY, + geneVariants: GENE_VARIANTS_MOCK, + modelId: CORE_PD_MODEL_MOCK.idObject, + overlayId: OVERLAY_ID, + }, + ], + }, + }, + }, + models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] }, + drawer: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioEntityDrawerState: { + ...INITIAL_STORE_STATE_MOCK.drawer.bioEntityDrawerState, + bioentityId: BIO_ENTITY.id, + }, + }, + }, + { + isShowGroupedOverlays: true, + }, + ); + }); + + it('should render title', () => { + expect(screen.getByText('Overlay data:')).toBeInTheDocument(); + }); + + it('should render names of two overlays', () => { + expect(screen.queryAllByText('overlay name').length).toEqual(2); + }); + }); + + describe('when axes list is present and isShowOverlayBioEntityName=true', () => { + beforeEach(() => { + const OVERLAY_ID = overlayFixture.idObject; + const BIO_ENTITY = MOCKED_OVERLAY_BIO_ENTITY_RENDER[0]; + + renderComponent( + { + ...INITIAL_STORE_STATE_MOCK, + map: { + ...initialMapStateFixture, + data: { ...initialMapStateFixture.data, modelId: CORE_PD_MODEL_MOCK.idObject }, + }, + overlays: { + ...INITIAL_STORE_STATE_MOCK.overlays, + data: [{ ...overlayFixture, name: 'overlay name' }], + }, + bioEntity: { + data: [ + { + searchQueryElement: '', + loading: 'pending', + error: { name: '', message: '' }, + data: [ + { + ...bioEntitiesContentFixture[0], + bioEntity: { + ...bioEntitiesContentFixture[0].bioEntity, + id: BIO_ENTITY.id, + }, + }, + ], + }, + ], + loading: 'pending', + error: { name: '', message: '' }, + }, + overlayBioEntity: { + ...OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, + overlaysId: [OVERLAY_ID], + data: { + [OVERLAY_ID]: { + [CORE_PD_MODEL_MOCK.idObject]: [ + { + ...BIO_ENTITY, + geneVariants: GENE_VARIANTS_MOCK, + modelId: CORE_PD_MODEL_MOCK.idObject, + overlayId: OVERLAY_ID, + name: 'element name', + }, + ], + }, + }, + }, + models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] }, + drawer: { + ...INITIAL_STORE_STATE_MOCK.drawer, + bioEntityDrawerState: { + ...INITIAL_STORE_STATE_MOCK.drawer.bioEntityDrawerState, + bioentityId: BIO_ENTITY.id, + }, + }, + }, + { + isShowOverlayBioEntityName: true, + }, + ); + }); + + it('should render title', () => { + expect(screen.getByText('Overlay data:')).toBeInTheDocument(); + }); + + it('should render element name', () => { + expect(screen.getByText('element name')).toBeInTheDocument(); + }); + }); }); diff --git a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.tsx index b4c193f2b23e57a1087b983d53ff00cc99736b4f..bc52903a5a672ad55202dedd64bc42a8a3b56206 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.component.tsx @@ -1,22 +1,51 @@ import { ZERO } from '@/constants/common'; +import { overlaysOpenedSelector } from '@/redux/overlayBioEntity/overlayBioEntity.selector'; +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { GroupedOverlayAxes } from './GroupedOverlayAxes'; import { OverlayAxis } from './OverlayAxis'; +import { getAxesSortedByValue } from './utils/getAxesSortedByValue'; +import { getUniqueAxes } from './utils/getUniqueAxes'; import { useOverlaysAxes } from './utils/useOverlaysAxes'; -export const OverlayData = (): JSX.Element | null => { - const axes = useOverlaysAxes(); +export interface Props { + isShowGroupedOverlays?: boolean; + isShowOverlayBioEntityName?: boolean; +} - if (axes.length === ZERO) { +export const OverlayData = ({ + isShowGroupedOverlays = false, + isShowOverlayBioEntityName = false, +}: Props = {}): JSX.Element | null => { + const axes = useOverlaysAxes({ isShowOverlayBioEntityName }); + const uniqueAxes = getUniqueAxes(axes); + const openedOverlays = useSelector(overlaysOpenedSelector); + const sortedAxes = useMemo(() => getAxesSortedByValue(uniqueAxes), [uniqueAxes]); + + if (uniqueAxes.length === ZERO) { return null; } + const overlaysAxesContent = ( + <div className="flex flex-col gap-2 rounded-lg border border-divide p-4"> + {sortedAxes.map(axis => ( + <OverlayAxis key={axis.title} axis={axis} /> + ))} + </div> + ); + + const groupedOverlayAxesContent = ( + <> + {openedOverlays.map(overlay => ( + <GroupedOverlayAxes key={overlay.idObject} overlay={overlay} axes={uniqueAxes} /> + ))} + </> + ); + return ( - <div> + <div className="flex flex-col gap-2"> <h3 className="mb-2 font-semibold">Overlay data:</h3> - <div className="flex flex-col gap-2 rounded-lg border border-divide p-4"> - {axes.map(axis => ( - <OverlayAxis key={axis.title} axis={axis} /> - ))} - </div> + {isShowGroupedOverlays ? groupedOverlayAxesContent : overlaysAxesContent} </div> ); }; diff --git a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.types.ts b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.types.ts index 3d43db8f8fdb810df0e8288f59d218ac669e2d73..5dfb216221758588545553f4303ab3072092c9cd 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.types.ts +++ b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/OverlayData.types.ts @@ -6,4 +6,5 @@ export interface OverlayDataAxis { value?: number; color: string; geneVariants?: GeneVariant[] | null; + overlayId: number; } diff --git a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/utils/getAxesSortedByValue.ts b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/utils/getAxesSortedByValue.ts new file mode 100644 index 0000000000000000000000000000000000000000..448a1f20af06a7669edf133dab2de375b7a8b1a7 --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/utils/getAxesSortedByValue.ts @@ -0,0 +1,5 @@ +import { ZERO } from '@/constants/common'; +import { OverlayDataAxis } from '../OverlayData.types'; + +export const getAxesSortedByValue = (axes: OverlayDataAxis[]): OverlayDataAxis[] => + axes.sort((a, b) => (b?.value || ZERO) - (a?.value || ZERO)); diff --git a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/utils/getUniqueAxes.ts b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/utils/getUniqueAxes.ts new file mode 100644 index 0000000000000000000000000000000000000000..a3c116abf7fd440a25490c376cbdfa3d09e0daf7 --- /dev/null +++ b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/utils/getUniqueAxes.ts @@ -0,0 +1,20 @@ +import { OverlayDataAxis } from '../OverlayData.types'; + +const UNIQUE_KEYS: (keyof Pick<OverlayDataAxis, 'title' | 'value'>)[] = ['title', 'value']; + +const SEPARATOR = ';'; + +export const getUniqueAxes = (nonUniqueAxes: OverlayDataAxis[]): OverlayDataAxis[] => { + const getUniqueAxesKey = (axis: OverlayDataAxis): string => + UNIQUE_KEYS.map(key => axis[key]).join(SEPARATOR); + + const uniqueAxesObj = nonUniqueAxes.reduce( + (obj, axis) => ({ + ...obj, + [getUniqueAxesKey(axis)]: axis, + }), + {} as Record<string, OverlayDataAxis>, + ); + + return Object.values(uniqueAxesObj); +}; diff --git a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/utils/useOverlaysAxes.ts b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/utils/useOverlaysAxes.ts index d7735979501adc9e868b6195bf97a2b856cd2b34..69bbcbe9ad14d3717252092caacf9311ca2116e4 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/OverlayData/utils/useOverlaysAxes.ts +++ b/src/components/Map/Drawer/BioEntityDrawer/OverlayData/utils/useOverlaysAxes.ts @@ -8,7 +8,13 @@ import { import { useSelector } from 'react-redux'; import { OverlayDataAxis } from '../OverlayData.types'; -export const useOverlaysAxes = (): OverlayDataAxis[] => { +interface Options { + isShowOverlayBioEntityName?: boolean; +} + +export const useOverlaysAxes = ({ + isShowOverlayBioEntityName = false, +}: Options = {}): OverlayDataAxis[] => { const openedOverlays = useSelector(overlaysOpenedSelector); const currentBioEntityOverlaysForCurrentBioEntity = useAppSelector( overlaysBioEntityForCurrentBioEntityAndCurrentModelSelector, @@ -23,10 +29,11 @@ export const useOverlaysAxes = (): OverlayDataAxis[] => { return { id: overlayBioEntity.id, - title: overlay?.name || '', + title: (isShowOverlayBioEntityName ? overlayBioEntity?.name : '') || overlay?.name || '', value: overlayBioEntity.value || undefined, color: getOverlayBioEntityColorByAvailableProperties(overlayBioEntity), geneVariants: overlayBioEntity?.geneVariants, + overlayId: overlayBioEntity.overlayId, }; }); }; diff --git a/src/redux/overlayBioEntity/overlayBioEntity.utils.ts b/src/redux/overlayBioEntity/overlayBioEntity.utils.ts index 420ec1d08e39f19f7bc6c2acf79753aaa5cd0d6e..38cd53086119920b85968bf9df99775729817c34 100644 --- a/src/redux/overlayBioEntity/overlayBioEntity.utils.ts +++ b/src/redux/overlayBioEntity/overlayBioEntity.utils.ts @@ -34,6 +34,7 @@ export const parseOverlayBioEntityToOlRenderingFormat = ( value: entity.right.value, overlayId, color: entity.right.color, + name: entity.right.name, }); return acc; } @@ -53,6 +54,7 @@ export const parseOverlayBioEntityToOlRenderingFormat = ( overlayId, color: entity.right.color, geneVariants: entity.right.geneVariants, + name: entity.right.name, }); } diff --git a/src/types/OLrendering.ts b/src/types/OLrendering.ts index 54f0c7ab06ea5521f4206a0599e0aba5b7f954f4..099a8266240819e2d64d54a185255633c64cd3ac 100644 --- a/src/types/OLrendering.ts +++ b/src/types/OLrendering.ts @@ -21,6 +21,7 @@ export type OverlayBioEntityRender = { hexColor?: string; type: OverlayBioEntityRenderType; geneVariants?: GeneVariant[] | null; + name?: string; }; export interface OverlayReactionCoords {