From f303aaf4bf889090807518c1d1fd5cef5dbb9c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com> Date: Thu, 4 Jan 2024 16:24:23 +0100 Subject: [PATCH] feat: add display legend --- .../NavBar/NavBar.component.tsx | 3 +- .../Map/Legend/Legend.component.test.tsx | 75 +++++++++++++++++++ .../Map/Legend/Legend.component.tsx | 24 ++++++ src/components/Map/Legend/Legend.constants.ts | 1 + .../LegendHeader.component.test.tsx | 49 ++++++++++++ .../LegendHeader/LegendHeader.component.tsx | 31 ++++++++ .../Map/Legend/LegendHeader/index.ts | 1 + .../LegendImages.component.test.tsx | 66 ++++++++++++++++ .../LegendImages/LegendImages.component.tsx | 24 ++++++ .../Map/Legend/LegendImages/index.ts | 1 + src/components/Map/Legend/index.ts | 1 + src/components/Map/Map.component.tsx | 7 +- .../overlaysLayer/useOlMapOverlaysLayer.ts | 13 ++-- .../reactionsLayer/useOlMapReactionsLayer.ts | 5 +- .../configuration/configuration.constants.ts | 7 ++ .../configuration/configuration.selectors.ts | 9 ++- src/redux/legend/legend.constants.ts | 7 ++ src/redux/legend/legend.mock.ts | 7 ++ src/redux/legend/legend.reducers.ts | 21 ++++++ src/redux/legend/legend.selectors.ts | 20 +++++ src/redux/legend/legend.slice.ts | 16 ++++ src/redux/legend/legend.types.ts | 8 ++ src/redux/root/root.fixtures.ts | 4 +- src/redux/store.ts | 10 ++- 24 files changed, 392 insertions(+), 18 deletions(-) create mode 100644 src/components/Map/Legend/Legend.component.test.tsx create mode 100644 src/components/Map/Legend/Legend.component.tsx create mode 100644 src/components/Map/Legend/Legend.constants.ts create mode 100644 src/components/Map/Legend/LegendHeader/LegendHeader.component.test.tsx create mode 100644 src/components/Map/Legend/LegendHeader/LegendHeader.component.tsx create mode 100644 src/components/Map/Legend/LegendHeader/index.ts create mode 100644 src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx create mode 100644 src/components/Map/Legend/LegendImages/LegendImages.component.tsx create mode 100644 src/components/Map/Legend/LegendImages/index.ts create mode 100644 src/components/Map/Legend/index.ts create mode 100644 src/redux/legend/legend.constants.ts create mode 100644 src/redux/legend/legend.mock.ts create mode 100644 src/redux/legend/legend.reducers.ts create mode 100644 src/redux/legend/legend.selectors.ts create mode 100644 src/redux/legend/legend.slice.ts create mode 100644 src/redux/legend/legend.types.ts diff --git a/src/components/FunctionalArea/NavBar/NavBar.component.tsx b/src/components/FunctionalArea/NavBar/NavBar.component.tsx index 6783d04b..29ce0fb0 100644 --- a/src/components/FunctionalArea/NavBar/NavBar.component.tsx +++ b/src/components/FunctionalArea/NavBar/NavBar.component.tsx @@ -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 ( diff --git a/src/components/Map/Legend/Legend.component.test.tsx b/src/components/Map/Legend/Legend.component.test.tsx new file mode 100644 index 00000000..4ad65a5a --- /dev/null +++ b/src/components/Map/Legend/Legend.component.test.tsx @@ -0,0 +1,75 @@ +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(); + }); + }); +}); diff --git a/src/components/Map/Legend/Legend.component.tsx b/src/components/Map/Legend/Legend.component.tsx new file mode 100644 index 00000000..7414765e --- /dev/null +++ b/src/components/Map/Legend/Legend.component.tsx @@ -0,0 +1,24 @@ +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> + ); +}; diff --git a/src/components/Map/Legend/Legend.constants.ts b/src/components/Map/Legend/Legend.constants.ts new file mode 100644 index 00000000..e0b04e86 --- /dev/null +++ b/src/components/Map/Legend/Legend.constants.ts @@ -0,0 +1 @@ +export const LEGEND_ROLE = 'legend'; diff --git a/src/components/Map/Legend/LegendHeader/LegendHeader.component.test.tsx b/src/components/Map/Legend/LegendHeader/LegendHeader.component.test.tsx new file mode 100644 index 00000000..1cf02917 --- /dev/null +++ b/src/components/Map/Legend/LegendHeader/LegendHeader.component.test.tsx @@ -0,0 +1,49 @@ +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'); + }); +}); diff --git a/src/components/Map/Legend/LegendHeader/LegendHeader.component.tsx b/src/components/Map/Legend/LegendHeader/LegendHeader.component.tsx new file mode 100644 index 00000000..19e52d7a --- /dev/null +++ b/src/components/Map/Legend/LegendHeader/LegendHeader.component.tsx @@ -0,0 +1,31 @@ +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> + ); +}; diff --git a/src/components/Map/Legend/LegendHeader/index.ts b/src/components/Map/Legend/LegendHeader/index.ts new file mode 100644 index 00000000..8cd37107 --- /dev/null +++ b/src/components/Map/Legend/LegendHeader/index.ts @@ -0,0 +1 @@ +export { LegendHeader } from './LegendHeader.component'; diff --git a/src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx b/src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx new file mode 100644 index 00000000..e6d2681f --- /dev/null +++ b/src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx @@ -0,0 +1,66 @@ +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}`); + }); + }); +}); diff --git a/src/components/Map/Legend/LegendImages/LegendImages.component.tsx b/src/components/Map/Legend/LegendImages/LegendImages.component.tsx new file mode 100644 index 00000000..04516e44 --- /dev/null +++ b/src/components/Map/Legend/LegendImages/LegendImages.component.tsx @@ -0,0 +1,24 @@ +/* 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> + ); +}; diff --git a/src/components/Map/Legend/LegendImages/index.ts b/src/components/Map/Legend/LegendImages/index.ts new file mode 100644 index 00000000..e038a280 --- /dev/null +++ b/src/components/Map/Legend/LegendImages/index.ts @@ -0,0 +1 @@ +export { LegendImages } from './LegendImages.component'; diff --git a/src/components/Map/Legend/index.ts b/src/components/Map/Legend/index.ts new file mode 100644 index 00000000..2237b30d --- /dev/null +++ b/src/components/Map/Legend/index.ts @@ -0,0 +1 @@ +export { Legend } from './Legend.component'; diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx index 90492655..ea5a18e2 100644 --- a/src/components/Map/Map.component.tsx +++ b/src/components/Map/Map.component.tsx @@ -1,11 +1,16 @@ 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> ); diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer.ts index 830dc498..048fecd7 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOlMapOverlaysLayer.ts @@ -1,15 +1,14 @@ -import Geometry from 'ol/geom/Geometry'; -import VectorLayer from 'ol/layer/Vector'; -import VectorSource from 'ol/source/Vector'; -import { useMemo } from 'react'; -import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { useTriColorLerp } from '@/hooks/useTriColorLerp'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { overlayBioEntitiesForCurrentModelSelector } from '@/redux/overlayBioEntity/overlayBioEntity.selector'; -import { Feature } from 'ol'; +import { usePointToProjection } from '@/utils/map/usePointToProjection'; +import { Polygon } from 'ol/geom'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; +import { useMemo } from 'react'; import { getOverlayFeatures } from './getOverlayFeatures'; -export const useOlMapOverlaysLayer = (): VectorLayer<VectorSource<Feature<Geometry>>> => { +export const useOlMapOverlaysLayer = (): VectorLayer<VectorSource<Polygon>> => { const pointToProjection = usePointToProjection(); const { getHex3ColorGradientColorWithAlpha, defaultColorHex } = useTriColorLerp(); const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); diff --git a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index 8b784426..3722f302 100644 --- a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -4,7 +4,7 @@ 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 { Geometry } from 'ol/geom'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import Fill from 'ol/style/Fill'; @@ -12,13 +12,12 @@ 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[] => reactions.map(({ lines }) => lines.map(({ start, end }): LinePoint => [start, end])).flat(); -export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<Geometry>>> => { +export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Geometry>> => { const pointToProjection = usePointToProjection(); const reactions = useSelector(allReactionsSelectorOfCurrentMap); const reactionsLines = getReactionsLines(reactions); diff --git a/src/redux/configuration/configuration.constants.ts b/src/redux/configuration/configuration.constants.ts index 765ad32a..ebf80efa 100644 --- a/src/redux/configuration/configuration.constants.ts +++ b/src/redux/configuration/configuration.constants.ts @@ -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', +]; diff --git a/src/redux/configuration/configuration.selectors.ts b/src/redux/configuration/configuration.selectors.ts index 7a694a44..2831a55f 100644 --- a/src/redux/configuration/configuration.selectors.ts +++ b/src/redux/configuration/configuration.selectors.ts @@ -1,7 +1,8 @@ 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, @@ -37,3 +38,9 @@ export const simpleColorValSelector = createSelector( configurationSelector, state => configurationAdapterSelectors.selectById(state, SIMPLE_COLOR_VAL_NAME_ID)?.value, ); + +export const defaultLegendImagesSelector = createSelector(configurationSelector, state => + LEGEND_FILE_NAMES_IDS.map( + legendNameId => configurationAdapterSelectors.selectById(state, legendNameId)?.value, + ).filter(legendImage => Boolean(legendImage)), +); diff --git a/src/redux/legend/legend.constants.ts b/src/redux/legend/legend.constants.ts new file mode 100644 index 00000000..5719c74e --- /dev/null +++ b/src/redux/legend/legend.constants.ts @@ -0,0 +1,7 @@ +import { LegendState } from './legend.types'; + +export const LEGEND_INITIAL_STATE: LegendState = { + isOpen: false, + pluginLegend: {}, + selectedPluginId: undefined, +}; diff --git a/src/redux/legend/legend.mock.ts b/src/redux/legend/legend.mock.ts new file mode 100644 index 00000000..6874d3a6 --- /dev/null +++ b/src/redux/legend/legend.mock.ts @@ -0,0 +1,7 @@ +import { LegendState } from './legend.types'; + +export const LEGEND_INITIAL_STATE_MOCK: LegendState = { + isOpen: false, + pluginLegend: {}, + selectedPluginId: undefined, +}; diff --git a/src/redux/legend/legend.reducers.ts b/src/redux/legend/legend.reducers.ts new file mode 100644 index 00000000..be1f0572 --- /dev/null +++ b/src/redux/legend/legend.reducers.ts @@ -0,0 +1,21 @@ +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; +}; diff --git a/src/redux/legend/legend.selectors.ts b/src/redux/legend/legend.selectors.ts new file mode 100644 index 00000000..fdf927f1 --- /dev/null +++ b/src/redux/legend/legend.selectors.ts @@ -0,0 +1,20 @@ +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; + }, +); diff --git a/src/redux/legend/legend.slice.ts b/src/redux/legend/legend.slice.ts new file mode 100644 index 00000000..fbb62f00 --- /dev/null +++ b/src/redux/legend/legend.slice.ts @@ -0,0 +1,16 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { LEGEND_INITIAL_STATE } from './legend.constants'; +import { closeLegendReducer, openLegendReducer } from './legend.reducers'; + +const legendSlice = createSlice({ + name: 'legend', + initialState: LEGEND_INITIAL_STATE, + reducers: { + openLegend: openLegendReducer, + closeLegend: closeLegendReducer, + }, +}); + +export const { openLegend, closeLegend } = legendSlice.actions; + +export default legendSlice.reducer; diff --git a/src/redux/legend/legend.types.ts b/src/redux/legend/legend.types.ts new file mode 100644 index 00000000..80314aec --- /dev/null +++ b/src/redux/legend/legend.types.ts @@ -0,0 +1,8 @@ +export type PluginId = number; +export type ImageUrl = string; + +export type LegendState = { + isOpen: boolean; + pluginLegend: Record<PluginId, ImageUrl[]>; + selectedPluginId: PluginId | undefined; +}; diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts index e2c17d64..966bb1d5 100644 --- a/src/redux/root/root.fixtures.ts +++ b/src/redux/root/root.fixtures.ts @@ -1,11 +1,12 @@ import { BACKGROUND_INITIAL_STATE_MOCK } from '../backgrounds/background.mock'; import { BIOENTITY_INITIAL_STATE_MOCK } from '../bioEntity/bioEntity.mock'; import { CHEMICALS_INITIAL_STATE_MOCK } from '../chemicals/chemicals.mock'; +import { CONFIGURATION_INITIAL_STATE } from '../configuration/configuration.adapter'; import { CONTEXT_MENU_INITIAL_STATE } from '../contextMenu/contextMenu.constants'; import { COOKIE_BANNER_INITIAL_STATE_MOCK } from '../cookieBanner/cookieBanner.mock'; -import { CONFIGURATION_INITIAL_STATE } from '../configuration/configuration.adapter'; import { initialStateFixture as drawerInitialStateMock } from '../drawer/drawerFixture'; import { DRUGS_INITIAL_STATE_MOCK } from '../drugs/drugs.mock'; +import { LEGEND_INITIAL_STATE_MOCK } from '../legend/legend.mock'; import { initialMapStateFixture } from '../map/map.fixtures'; import { MODAL_INITIAL_STATE_MOCK } from '../modal/modal.mock'; import { MODELS_INITIAL_STATE_MOCK } from '../models/models.mock'; @@ -35,4 +36,5 @@ export const INITIAL_STORE_STATE_MOCK: RootState = { contextMenu: CONTEXT_MENU_INITIAL_STATE, cookieBanner: COOKIE_BANNER_INITIAL_STATE_MOCK, user: USER_INITIAL_STATE_MOCK, + legend: LEGEND_INITIAL_STATE_MOCK, }; diff --git a/src/redux/store.ts b/src/redux/store.ts index c0016526..663bc180 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,20 +1,20 @@ import backgroundsReducer from '@/redux/backgrounds/backgrounds.slice'; import bioEntityReducer from '@/redux/bioEntity/bioEntity.slice'; import chemicalsReducer from '@/redux/chemicals/chemicals.slice'; +import configurationReducer from '@/redux/configuration/configuration.slice'; +import contextMenuReducer from '@/redux/contextMenu/contextMenu.slice'; +import cookieBannerReducer from '@/redux/cookieBanner/cookieBanner.slice'; import drawerReducer from '@/redux/drawer/drawer.slice'; import drugsReducer from '@/redux/drugs/drugs.slice'; import mapReducer from '@/redux/map/map.slice'; import modalReducer from '@/redux/modal/modal.slice'; import modelsReducer from '@/redux/models/models.slice'; +import overlayBioEntityReducer from '@/redux/overlayBioEntity/overlayBioEntity.slice'; import overlaysReducer from '@/redux/overlays/overlays.slice'; import projectReducer from '@/redux/project/project.slice'; import reactionsReducer from '@/redux/reactions/reactions.slice'; -import contextMenuReducer from '@/redux/contextMenu/contextMenu.slice'; import searchReducer from '@/redux/search/search.slice'; -import cookieBannerReducer from '@/redux/cookieBanner/cookieBanner.slice'; import userReducer from '@/redux/user/user.slice'; -import configurationReducer from '@/redux/configuration/configuration.slice'; -import overlayBioEntityReducer from '@/redux/overlayBioEntity/overlayBioEntity.slice'; import { AnyAction, ListenerEffectAPI, @@ -22,6 +22,7 @@ import { TypedStartListening, configureStore, } from '@reduxjs/toolkit'; +import legendReducer from './legend/legend.slice'; import { mapListenerMiddleware } from './map/middleware/map.middleware'; export const reducers = { @@ -42,6 +43,7 @@ export const reducers = { user: userReducer, configuration: configurationReducer, overlayBioEntity: overlayBioEntityReducer, + legend: legendReducer, }; export const middlewares = [mapListenerMiddleware.middleware]; -- GitLab