Skip to content
Snippets Groups Projects
Commit 0b50954b authored by Adrian Orłów's avatar Adrian Orłów :fire:
Browse files

Merge branch 'MIN-169-display-legend' into 'development'

feat: add display legend

Closes MIN-169

See merge request !93
parents 0fde397d b6bcdf26
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...,!93feat: add display legend
Pipeline #84059 passed
Showing
with 360 additions and 11 deletions
......@@ -2,6 +2,7 @@ import logoImg from '@/assets/images/logo.png';
import luxembourgLogoImg from '@/assets/images/luxembourg-logo.png';
import { openDrawer } from '@/redux/drawer/drawer.slice';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { openLegend } from '@/redux/legend/legend.slice';
import { IconButton } from '@/shared/IconButton';
import Image from 'next/image';
......@@ -21,7 +22,7 @@ export const NavBar = (): JSX.Element => {
};
const openDrawerLegend = (): void => {
dispatch(openDrawer('legend'));
dispatch(openLegend());
};
return (
......
import { currentLegendImagesSelector, legendSelector } from '@/redux/legend/legend.selectors';
import { StoreType } from '@/redux/store';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen, within } from '@testing-library/react';
import { Legend } from './Legend.component';
import { LEGEND_ROLE } from './Legend.constants';
jest.mock('../../../redux/legend/legend.selectors', () => ({
legendSelector: jest.fn(),
currentLegendImagesSelector: jest.fn(),
}));
const legendSelectorMock = legendSelector as unknown as jest.Mock;
const currentLegendImagesSelectorMock = currentLegendImagesSelector as unknown as jest.Mock;
const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
return (
render(
<Wrapper>
<Legend />
</Wrapper>,
),
{
store,
}
);
};
describe('Legend - component', () => {
beforeAll(() => {
currentLegendImagesSelectorMock.mockImplementation(() => []);
});
describe('when is closed', () => {
beforeEach(() => {
legendSelectorMock.mockImplementation(() => ({
isOpen: false,
}));
renderComponent();
});
it('should render the component without translation', () => {
expect(screen.getByRole(LEGEND_ROLE)).not.toHaveClass('translate-y-0');
});
});
describe('when is open', () => {
beforeEach(() => {
legendSelectorMock.mockImplementation(() => ({
isOpen: true,
}));
renderComponent();
});
it('should render the component with translation', () => {
expect(screen.getByRole(LEGEND_ROLE)).toHaveClass('translate-y-0');
});
it('should render legend header', async () => {
const legendContainer = screen.getByRole(LEGEND_ROLE);
const legendHeader = await within(legendContainer).getByTestId('legend-header');
expect(legendHeader).toBeInTheDocument();
});
it('should render legend images', async () => {
const legendContainer = screen.getByRole(LEGEND_ROLE);
const legendImages = await within(legendContainer).getByTestId('legend-images');
expect(legendImages).toBeInTheDocument();
});
});
});
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { legendSelector } from '@/redux/legend/legend.selectors';
import * as React from 'react';
import { twMerge } from 'tailwind-merge';
import { LEGEND_ROLE } from './Legend.constants';
import { LegendHeader } from './LegendHeader';
import { LegendImages } from './LegendImages';
export const Legend: React.FC = () => {
const { isOpen } = useAppSelector(legendSelector);
return (
<div
className={twMerge(
'absolute bottom-0 left-[88px] z-10 w-[calc(100%-88px)] -translate-y-[-100%] transform border border-divide bg-white-pearl text-font-500 transition-all duration-500',
isOpen && 'translate-y-0',
)}
role={LEGEND_ROLE}
>
<LegendHeader />
<LegendImages />
</div>
);
};
export const LEGEND_ROLE = 'legend';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { AppDispatch, RootState } from '@/redux/store';
import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener';
import { InitialStoreState } from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { MockStoreEnhanced } from 'redux-mock-store';
import { CLOSE_BUTTON_ROLE, LegendHeader } from './LegendHeader.component';
const renderComponent = (
initialStore?: InitialStoreState,
): { store: MockStoreEnhanced<Partial<RootState>, AppDispatch> } => {
const { Wrapper, store } = getReduxStoreWithActionsListener(initialStore);
return (
render(
<Wrapper>
<LegendHeader />
</Wrapper>,
),
{
store,
}
);
};
describe('LegendHeader - component', () => {
it('should render legend title', () => {
renderComponent();
const legendTitle = screen.getByText('Legend');
expect(legendTitle).toBeInTheDocument();
});
it('should render legend close button', () => {
renderComponent();
const closeButton = screen.getByRole(CLOSE_BUTTON_ROLE);
expect(closeButton).toBeInTheDocument();
});
it('should close legend on close button click', async () => {
const { store } = renderComponent();
const closeButton = screen.getByRole(CLOSE_BUTTON_ROLE);
closeButton.click();
const actions = store.getActions();
expect(actions[FIRST_ARRAY_ELEMENT].type).toBe('legend/closeLegend');
});
});
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { closeLegend } from '@/redux/legend/legend.slice';
import { IconButton } from '@/shared/IconButton';
export const CLOSE_BUTTON_ROLE = 'close-legend-button';
export const LegendHeader: React.FC = () => {
const dispatch = useAppDispatch();
const handleCloseLegend = (): void => {
dispatch(closeLegend());
};
return (
<div
data-testid="legend-header"
className="flex items-center justify-between border-b border-b-divide px-6"
>
<div className="py-8 text-xl">
<span className="font-semibold">Legend</span>
</div>
<IconButton
className="bg-white-pearl"
classNameIcon="fill-font-500"
icon="close"
role={CLOSE_BUTTON_ROLE}
onClick={handleCloseLegend}
/>
</div>
);
};
export { LegendHeader } from './LegendHeader.component';
import { BASE_MAP_IMAGES_URL } from '@/constants';
import { currentLegendImagesSelector, legendSelector } from '@/redux/legend/legend.selectors';
import { StoreType } from '@/redux/store';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { LegendImages } from './LegendImages.component';
jest.mock('../../../../redux/legend/legend.selectors', () => ({
legendSelector: jest.fn(),
currentLegendImagesSelector: jest.fn(),
}));
const legendSelectorMock = legendSelector as unknown as jest.Mock;
const currentLegendImagesSelectorMock = currentLegendImagesSelector as unknown as jest.Mock;
const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
return (
render(
<Wrapper>
<LegendImages />
</Wrapper>,
),
{
store,
}
);
};
describe('LegendImages - component', () => {
beforeAll(() => {
legendSelectorMock.mockImplementation(() => ({
isOpen: true,
}));
});
describe('when current images are empty', () => {
beforeEach(() => {
currentLegendImagesSelectorMock.mockImplementation(() => []);
renderComponent();
});
it('should render empty container', () => {
expect(screen.getByTestId('legend-images')).toBeEmptyDOMElement();
});
});
describe('when current images are present', () => {
const imagesPartialUrls = ['url1/image.png', 'url2/image.png', 'url3/image.png'];
beforeEach(() => {
currentLegendImagesSelectorMock.mockImplementation(() => imagesPartialUrls);
renderComponent();
});
it.each(imagesPartialUrls)('should render img element, partialUrl=%s', partialUrl => {
const imgElement = screen.getByAltText(partialUrl);
expect(imgElement).toBeInTheDocument();
expect(imgElement.getAttribute('src')).toBe(`${BASE_MAP_IMAGES_URL}/minerva/${partialUrl}`);
});
});
});
/* eslint-disable @next/next/no-img-element */
import { BASE_MAP_IMAGES_URL } from '@/constants';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { currentLegendImagesSelector } from '@/redux/legend/legend.selectors';
export const LegendImages: React.FC = () => {
const imageUrls = useAppSelector(currentLegendImagesSelector);
return (
<div
data-testid="legend-images"
className="flex items-center justify-between overflow-x-auto border-b border-b-divide px-6 py-8"
>
{imageUrls.map(imageUrl => (
<img
key={imageUrl}
src={`${BASE_MAP_IMAGES_URL}/minerva/${imageUrl}`}
alt={imageUrl}
className="h-[400px]"
/>
))}
</div>
);
};
export { LegendImages } from './LegendImages.component';
export { Legend } from './Legend.component';
import { Drawer } from '@/components/Map/Drawer';
import { Legend } from '@/components/Map/Legend';
import { MapAdditionalOptions } from './MapAdditionalOptions';
import { MapViewer } from './MapViewer/MapViewer.component';
export const Map = (): JSX.Element => (
<div className="relative z-0 h-screen w-full bg-black" data-testid="map-container">
<div
className="relative z-0 h-screen w-full overflow-hidden bg-black"
data-testid="map-container"
>
<MapAdditionalOptions />
<Drawer />
<MapViewer />
<Legend />
</div>
);
import Geometry from 'ol/geom/Geometry';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { Feature } from 'ol';
import { useMemo } from 'react';
import { usePointToProjection } from '@/utils/map/usePointToProjection';
import { useTriColorLerp } from '@/hooks/useTriColorLerp';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import {
getOverlayOrderSelector,
overlayBioEntitiesForCurrentModelSelector,
} from '@/redux/overlayBioEntity/overlayBioEntity.selector';
import { usePointToProjection } from '@/utils/map/usePointToProjection';
import { Feature } from 'ol';
import { Geometry } from 'ol/geom';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { useMemo } from 'react';
import { getOverlayFeatures } from './getOverlayFeatures';
/**
......
......@@ -4,7 +4,8 @@ import { allReactionsSelectorOfCurrentMap } from '@/redux/reactions/reactions.se
import { Reaction } from '@/types/models';
import { LinePoint } from '@/types/reactions';
import { usePointToProjection } from '@/utils/map/usePointToProjection';
import Geometry from 'ol/geom/Geometry';
import { Feature } from 'ol';
import { Geometry } from 'ol/geom';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import Fill from 'ol/style/Fill';
......@@ -12,7 +13,6 @@ import Stroke from 'ol/style/Stroke';
import Style from 'ol/style/Style';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Feature } from 'ol';
import { getLineFeature } from './getLineFeature';
const getReactionsLines = (reactions: Reaction[]): LinePoint[] =>
......
......@@ -3,3 +3,10 @@ export const MAX_COLOR_VAL_NAME_ID = 'MAX_COLOR_VAL';
export const SIMPLE_COLOR_VAL_NAME_ID = 'SIMPLE_COLOR_VAL';
export const NEUTRAL_COLOR_VAL_NAME_ID = 'NEUTRAL_COLOR_VAL';
export const OVERLAY_OPACITY_NAME_ID = 'OVERLAY_OPACITY';
export const LEGEND_FILE_NAMES_IDS = [
'LEGEND_FILE_1',
'LEGEND_FILE_2',
'LEGEND_FILE_3',
'LEGEND_FILE_4',
];
import { createSelector } from '@reduxjs/toolkit';
import { configurationAdapter } from './configuration.adapter';
import { rootSelector } from '../root/root.selectors';
import { configurationAdapter } from './configuration.adapter';
import {
LEGEND_FILE_NAMES_IDS,
MAX_COLOR_VAL_NAME_ID,
MIN_COLOR_VAL_NAME_ID,
NEUTRAL_COLOR_VAL_NAME_ID,
......@@ -39,6 +40,12 @@ export const simpleColorValSelector = createSelector(
state => configurationAdapterSelectors.selectById(state, SIMPLE_COLOR_VAL_NAME_ID)?.value,
);
export const defaultLegendImagesSelector = createSelector(configurationOptionsSelector, state =>
LEGEND_FILE_NAMES_IDS.map(
legendNameId => configurationAdapterSelectors.selectById(state, legendNameId)?.value,
).filter(legendImage => Boolean(legendImage)),
);
export const configurationMainSelector = createSelector(
configurationSelector,
state => state.main.data,
......
import { LegendState } from './legend.types';
export const LEGEND_INITIAL_STATE: LegendState = {
isOpen: false,
pluginLegend: {},
selectedPluginId: undefined,
};
import { LegendState } from './legend.types';
export const LEGEND_INITIAL_STATE_MOCK: LegendState = {
isOpen: false,
pluginLegend: {},
selectedPluginId: undefined,
};
import { PayloadAction } from '@reduxjs/toolkit';
import { LegendState, PluginId } from './legend.types';
export const openLegendReducer = (state: LegendState): void => {
state.isOpen = true;
};
export const closeLegendReducer = (state: LegendState): void => {
state.isOpen = false;
};
export const selectLegendPluginIdReducer = (
state: LegendState,
action: PayloadAction<PluginId>,
): void => {
state.selectedPluginId = action.payload;
};
export const selectDefaultLegendReducer = (state: LegendState): void => {
state.selectedPluginId = undefined;
};
import { createSelector } from '@reduxjs/toolkit';
import { defaultLegendImagesSelector } from '../configuration/configuration.selectors';
import { rootSelector } from '../root/root.selectors';
export const legendSelector = createSelector(rootSelector, state => state.legend);
export const isLegendOpenSelector = createSelector(legendSelector, state => state.isOpen);
// TODO: add filter for active plugins
export const currentLegendImagesSelector = createSelector(
legendSelector,
defaultLegendImagesSelector,
({ selectedPluginId, pluginLegend }, defaultImages) => {
if (selectedPluginId) {
return pluginLegend?.[selectedPluginId] || [];
}
return defaultImages;
},
);
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