From 79664147976e785d9c20cf1e6de2bccc1d7ce1c8 Mon Sep 17 00:00:00 2001 From: mateusz-winiarczyk <mateusz.winiarczyk@appunite.com> Date: Wed, 20 Mar 2024 15:13:28 +0100 Subject: [PATCH] feat(plugins): legend tabs (MIN-231) --- docs/plugins/legend.md | 45 +++++ .../LoadPlugin/hooks/useLoadPlugin.test.ts | 161 +++++++++++++----- .../LoadPlugin/hooks/useLoadPlugin.ts | 14 ++ .../Map/Legend/Legend.component.test.tsx | 100 ++++++++++- .../Map/Legend/Legend.component.tsx | 10 +- .../LegendImages.component.test.tsx | 3 +- .../LegendImages/LegendImages.component.tsx | 8 +- .../LegendTab/LegendTab.component.test.tsx | 45 +++++ .../LegendTab/LegendTab.component.tsx | 28 +++ .../Map/Legend/LegendTabs/LegendTab/index.ts | 1 + .../LegendTabs/LegendTabs.component.tsx | 16 ++ src/components/Map/Legend/LegendTabs/index.ts | 1 + .../PluginHeaderInfo.component.test.tsx | 6 + .../LoadPluginElement.component.test.tsx | 2 + .../PluginOpenButton.component.test.tsx | 2 + .../PluginsHeader.component.test.tsx | 3 + .../PluginSingleTab.component.test.tsx | 17 +- .../PluginsTabs.component.test.tsx | 2 + src/redux/legend/legend.constants.ts | 9 +- src/redux/legend/legend.mock.ts | 3 +- src/redux/legend/legend.reducers.ts | 35 +++- src/redux/legend/legend.selectors.ts | 41 ++++- src/redux/legend/legend.slice.ts | 22 ++- src/redux/legend/legend.types.ts | 4 +- .../pluginsManager/legend/removeLegend.ts | 14 ++ .../pluginsManager/legend/setLegend.ts | 13 ++ src/services/pluginsManager/pluginsManager.ts | 6 + 27 files changed, 540 insertions(+), 71 deletions(-) create mode 100644 docs/plugins/legend.md create mode 100644 src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.test.tsx create mode 100644 src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.tsx create mode 100644 src/components/Map/Legend/LegendTabs/LegendTab/index.ts create mode 100644 src/components/Map/Legend/LegendTabs/LegendTabs.component.tsx create mode 100644 src/components/Map/Legend/LegendTabs/index.ts create mode 100644 src/services/pluginsManager/legend/removeLegend.ts create mode 100644 src/services/pluginsManager/legend/setLegend.ts diff --git a/docs/plugins/legend.md b/docs/plugins/legend.md new file mode 100644 index 00000000..dcc3e588 --- /dev/null +++ b/docs/plugins/legend.md @@ -0,0 +1,45 @@ +### Legend + +#### Set Legend + +To set a legend for a specific plugin, plugins can use the `setLegend` method in `legend` object returned by `window.minerva.plugins.registerPlugin`. This method takes one argument: + +- legend: an array containing image urls + +##### Example of setLegend method usage: + +```javascript +const { + element, + legend: { setLegend }, +} = minerva.plugins.registerPlugin({ + pluginName, + pluginVersion, + pluginUrl, +}); + +setLegend([ + 'https://lux1.atcomp.pl//minerva/resources/images/legend_d.png', + 'https://lux1.atcomp.pl//minerva/resources/images/legend_a.png', + 'https://lux1.atcomp.pl//minerva/resources/images/legend_b.png', +]); +``` + +#### Remove Legend + +To remove a legend associated with a specific plugin, plugins can use the `removeLegend` method in the `legend` object returned by `window.minerva.plugins.registerPlugin`. + +##### Example of removeLegend method usage: + +```javascript +const { + element, + legend: { removeLegend }, +} = minerva.plugins.registerPlugin({ + pluginName, + pluginVersion, + pluginUrl, +}); + +removeLegend(); +``` diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts index f54c8f5b..3b826b1a 100644 --- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts @@ -8,55 +8,38 @@ import axios, { HttpStatusCode } from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; import { showToast } from '@/utils/showToast'; +import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock'; import { useLoadPlugin } from './useLoadPlugin'; const mockedAxiosClient = new MockAdapter(axios); jest.mock('../../../../../../services/pluginsManager/pluginsManager'); jest.mock('../../../../../../utils/showToast'); +const STATE_MOCK = { + ...INITIAL_STORE_STATE_MOCK, + plugins: { + ...INITIAL_STORE_STATE_MOCK.plugins, + activePlugins: { + pluginsId: [pluginFixture.hash], + data: { + [pluginFixture.hash]: pluginFixture, + }, + }, + list: INITIAL_STORE_STATE_MOCK.plugins.list, + }, + legend: { + ...LEGEND_INITIAL_STATE_MOCK, + pluginLegend: { + [pluginFixture.hash]: ['url1', 'url2'], + }, + }, +}; + describe('useLoadPlugin', () => { afterEach(() => { jest.restoreAllMocks(); }); - it('should unload plugin successfully', async () => { - const { Wrapper, store } = getReduxStoreWithActionsListener({ - ...INITIAL_STORE_STATE_MOCK, - plugins: { - ...INITIAL_STORE_STATE_MOCK.plugins, - activePlugins: { - pluginsId: [pluginFixture.hash], - data: { - [pluginFixture.hash]: pluginFixture, - }, - }, - list: INITIAL_STORE_STATE_MOCK.plugins.list, - }, - }); - - const { - result: { - current: { isPluginActive, isPluginLoading, togglePlugin }, - }, - } = renderHook( - () => useLoadPlugin({ hash: pluginFixture.hash, pluginUrl: pluginFixture.urls[0] }), - { - wrapper: Wrapper, - }, - ); - - expect(isPluginActive).toBe(true); - expect(isPluginLoading).toBe(false); - act(() => { - togglePlugin(); - }); - - const actions = store.getActions(); - expect(actions[0]).toEqual({ - payload: { pluginId: pluginFixture.hash }, - type: 'plugins/removePlugin', - }); - }); it('should load plugin successfully', async () => { const hash = 'pluginHash'; const pluginUrl = 'http://example.com/plugin.js'; @@ -115,4 +98,106 @@ describe('useLoadPlugin', () => { }); }); }); + describe('when unload plugin', () => { + it('should unload plugin successfully', async () => { + const { Wrapper, store } = getReduxStoreWithActionsListener(STATE_MOCK); + + const { + result: { + current: { isPluginActive, isPluginLoading, togglePlugin }, + }, + } = renderHook( + () => useLoadPlugin({ hash: pluginFixture.hash, pluginUrl: pluginFixture.urls[0] }), + { + wrapper: Wrapper, + }, + ); + + expect(isPluginActive).toBe(true); + expect(isPluginLoading).toBe(false); + + act(() => { + togglePlugin(); + }); + + const actions = store.getActions(); + expect(actions[0]).toEqual({ + payload: { pluginId: pluginFixture.hash }, + type: 'plugins/removePlugin', + }); + }); + it('should remove plugin legend', () => { + const { Wrapper, store } = getReduxStoreWithActionsListener(STATE_MOCK); + + const { + result: { + current: { togglePlugin }, + }, + } = renderHook( + () => useLoadPlugin({ hash: pluginFixture.hash, pluginUrl: pluginFixture.urls[0] }), + { + wrapper: Wrapper, + }, + ); + + act(() => { + togglePlugin(); + }); + + const actions = store.getActions(); + + expect(actions).toEqual([ + { + payload: { pluginId: pluginFixture.hash }, + type: 'plugins/removePlugin', + }, + { + payload: pluginFixture.hash, + type: 'legend/removePluginLegend', + }, + ]); + }); + + it('should set active legend to main legend if plugin legend was active one', () => { + const { Wrapper, store } = getReduxStoreWithActionsListener({ + ...STATE_MOCK, + legend: { + ...STATE_MOCK.legend, + activeLegendId: pluginFixture.hash, + }, + }); + + const { + result: { + current: { togglePlugin }, + }, + } = renderHook( + () => useLoadPlugin({ hash: pluginFixture.hash, pluginUrl: pluginFixture.urls[0] }), + { + wrapper: Wrapper, + }, + ); + + act(() => { + togglePlugin(); + }); + + const actions = store.getActions(); + + expect(actions).toEqual([ + { + payload: { pluginId: pluginFixture.hash }, + type: 'plugins/removePlugin', + }, + { + payload: undefined, + type: 'legend/setDefaultLegendId', + }, + { + payload: pluginFixture.hash, + type: 'legend/removePluginLegend', + }, + ]); + }); + }); }); diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts index ebce11ec..a7208789 100644 --- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts @@ -10,6 +10,8 @@ import { PluginsManager } from '@/services/pluginsManager'; import { showToast } from '@/utils/showToast'; import axios from 'axios'; import { getErrorMessage } from '@/utils/getErrorMessage'; +import { removePluginLegend, setDefaultLegendId } from '@/redux/legend/legend.slice'; +import { isActiveLegendSelector } from '@/redux/legend/legend.selectors'; import { PLUGIN_LOADING_ERROR_PREFIX } from '../../AvailablePluginsDrawer.constants'; type UseLoadPluginReturnType = { @@ -36,6 +38,7 @@ export const useLoadPlugin = ({ const isPluginActive = useAppSelector(state => isPluginActiveSelector(state, hash)); const isPluginLoading = useAppSelector(state => isPluginLoadingSelector(state, hash)); const isPluginSelected = useAppSelector(state => isPluginSelectedSelector(state, hash)); + const isActivePluginLegend = useAppSelector(state => isActiveLegendSelector(state, hash)); const dispatch = useAppDispatch(); @@ -66,8 +69,19 @@ export const useLoadPlugin = ({ } }; + const handleRemoveLegend = (): void => { + if (isActivePluginLegend) { + dispatch(setDefaultLegendId()); + } + + dispatch(removePluginLegend(hash)); + }; + const handleUnloadPlugin = (): void => { dispatch(removePlugin({ pluginId: hash })); + + handleRemoveLegend(); + PluginsManager.removePluginContent({ hash }); }; diff --git a/src/components/Map/Legend/Legend.component.test.tsx b/src/components/Map/Legend/Legend.component.test.tsx index 4ad65a5a..266c109c 100644 --- a/src/components/Map/Legend/Legend.component.test.tsx +++ b/src/components/Map/Legend/Legend.component.test.tsx @@ -1,20 +1,41 @@ -import { currentLegendImagesSelector, legendSelector } from '@/redux/legend/legend.selectors'; +import { + allLegendsNamesAndIdsSelector, + currentLegendImagesSelector, + legendSelector, + pluginLegendsSelector, +} 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 { PLUGINS_INITIAL_STATE_MOCK } from '@/redux/plugins/plugins.mock'; +import { pluginFixture } from '@/models/fixtures/pluginFixture'; +import { DEFAULT_LEGEND_TAB } from '@/redux/legend/legend.constants'; import { LEGEND_ROLE } from './Legend.constants'; +import { Legend } from './Legend.component'; + +const PLUGIN_ID = '1'; +const PLUGIN_NAME = 'Plugin Custom Name'; +const LEGEND_IMAGES = [ + 'https://lux1.atcomp.pl//minerva/resources/images/legend_d.png', + 'https://lux1.atcomp.pl//minerva/resources/images/legend_a.png', + 'https://lux1.atcomp.pl//minerva/resources/images/legend_b.png', +]; jest.mock('../../../redux/legend/legend.selectors', () => ({ legendSelector: jest.fn(), currentLegendImagesSelector: jest.fn(), + allLegendsNamesAndIdsSelector: jest.fn(), + pluginLegendsSelector: jest.fn(), + isActiveLegendSelector: jest.fn(), })); const legendSelectorMock = legendSelector as unknown as jest.Mock; const currentLegendImagesSelectorMock = currentLegendImagesSelector as unknown as jest.Mock; +const pluginLegendsSelectorMock = pluginLegendsSelector as unknown as jest.Mock; +const allLegendsNamesAndIdsSelectorMock = allLegendsNamesAndIdsSelector as unknown as jest.Mock; const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => { const { Wrapper, store } = getReduxWrapperWithStore(initialStore); @@ -32,7 +53,19 @@ const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } describe('Legend - component', () => { beforeAll(() => { - currentLegendImagesSelectorMock.mockImplementation(() => []); + allLegendsNamesAndIdsSelectorMock.mockImplementation(() => [ + DEFAULT_LEGEND_TAB, + { + id: PLUGIN_ID, + name: PLUGIN_NAME, + }, + ]); + + pluginLegendsSelectorMock.mockImplementation(() => ({ + PLUGIN_ID: LEGEND_IMAGES, + })); + + currentLegendImagesSelectorMock.mockImplementation(() => LEGEND_IMAGES); }); describe('when is closed', () => { @@ -72,4 +105,65 @@ describe('Legend - component', () => { expect(legendImages).toBeInTheDocument(); }); }); + describe('when loaded plugin has own legend', () => { + it('should display legend tabs', () => { + legendSelectorMock.mockImplementation(() => ({ + isOpen: false, + pluginLegend: { + PLUGIN_ID: LEGEND_IMAGES, + }, + activeLegendId: 'MAIN', + })); + renderComponent({ + plugins: { + ...PLUGINS_INITIAL_STATE_MOCK, + activePlugins: { + data: { + PLUGIN_ID: { + ...pluginFixture, + hash: PLUGIN_ID, + name: PLUGIN_NAME, + }, + }, + pluginsId: [PLUGIN_ID], + }, + }, + }); + + expect(screen.getByText('Plugin Custom Name')).toBeVisible(); + expect(screen.getByText('Main Legend')).toBeVisible(); + }); + }); + describe('when loaded plugin does not have own legend', () => { + it('should not display legend tabs and display only main legend', () => { + allLegendsNamesAndIdsSelectorMock.mockImplementation(() => [DEFAULT_LEGEND_TAB]); + + pluginLegendsSelectorMock.mockImplementation(() => ({})); + + currentLegendImagesSelectorMock.mockImplementation(() => LEGEND_IMAGES); + legendSelectorMock.mockImplementation(() => ({ + isOpen: false, + pluginLegend: {}, + activeLegendId: 'MAIN', + })); + renderComponent({ + plugins: { + ...PLUGINS_INITIAL_STATE_MOCK, + activePlugins: { + data: { + PLUGIN_ID: { + ...pluginFixture, + hash: PLUGIN_ID, + name: PLUGIN_NAME, + }, + }, + pluginsId: [PLUGIN_ID], + }, + }, + }); + + expect(screen.queryByText('Plugin Custom Name')).not.toBeInTheDocument(); + expect(screen.queryByText('Main Legend')).not.toBeInTheDocument(); + }); + }); }); diff --git a/src/components/Map/Legend/Legend.component.tsx b/src/components/Map/Legend/Legend.component.tsx index 7414765e..e515483a 100644 --- a/src/components/Map/Legend/Legend.component.tsx +++ b/src/components/Map/Legend/Legend.component.tsx @@ -1,13 +1,20 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { legendSelector } from '@/redux/legend/legend.selectors'; +import { legendSelector, pluginLegendsSelector } from '@/redux/legend/legend.selectors'; import * as React from 'react'; import { twMerge } from 'tailwind-merge'; +import { ZERO } from '@/constants/common'; import { LEGEND_ROLE } from './Legend.constants'; import { LegendHeader } from './LegendHeader'; import { LegendImages } from './LegendImages'; +import { LegendTabs } from './LegendTabs'; export const Legend: React.FC = () => { const { isOpen } = useAppSelector(legendSelector); + const allPluginLegends = useAppSelector(pluginLegendsSelector); + const isAnyPluginLegendExists = React.useMemo( + () => Object.values(allPluginLegends).length > ZERO, + [allPluginLegends], + ); return ( <div @@ -18,6 +25,7 @@ export const Legend: React.FC = () => { role={LEGEND_ROLE} > <LegendHeader /> + {isAnyPluginLegendExists ? <LegendTabs /> : null} <LegendImages /> </div> ); diff --git a/src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx b/src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx index e6d2681f..87fb0757 100644 --- a/src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx +++ b/src/components/Map/Legend/LegendImages/LegendImages.component.test.tsx @@ -1,4 +1,3 @@ -import { BASE_MAP_IMAGES_URL } from '@/constants'; import { currentLegendImagesSelector, legendSelector } from '@/redux/legend/legend.selectors'; import { StoreType } from '@/redux/store'; import { @@ -60,7 +59,7 @@ describe('LegendImages - component', () => { const imgElement = screen.getByAltText(partialUrl); expect(imgElement).toBeInTheDocument(); - expect(imgElement.getAttribute('src')).toBe(`${BASE_MAP_IMAGES_URL}/minerva/${partialUrl}`); + expect(imgElement.getAttribute('src')).toBe(partialUrl); }); }); }); diff --git a/src/components/Map/Legend/LegendImages/LegendImages.component.tsx b/src/components/Map/Legend/LegendImages/LegendImages.component.tsx index dfc49d8f..8a56a050 100644 --- a/src/components/Map/Legend/LegendImages/LegendImages.component.tsx +++ b/src/components/Map/Legend/LegendImages/LegendImages.component.tsx @@ -1,5 +1,4 @@ /* 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'; @@ -12,12 +11,7 @@ export const LegendImages: React.FC = () => { 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] max-h-[50vh]" - /> + <img key={imageUrl} src={imageUrl} alt={imageUrl} className="h-[400px] max-h-[50vh]" /> ))} </div> ); diff --git a/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.test.tsx b/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.test.tsx new file mode 100644 index 00000000..2667820c --- /dev/null +++ b/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.test.tsx @@ -0,0 +1,45 @@ +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 { act } from 'react-dom/test-utils'; +import { LegendTab } from './LegendTab.component'; + +const LEGEND_ID = '2'; + +const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStore); + return ( + render( + <Wrapper> + <LegendTab id={LEGEND_ID} name="Legend Tab Name" /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('LegendTab - component', () => { + it('should display tab with provided name', () => { + renderComponent(); + + expect(screen.queryByText('Legend Tab Name')).toBeVisible(); + }); + it('should set tab as active on tab click', () => { + const { store } = renderComponent(); + const tab = screen.queryByText('Legend Tab Name'); + expect(tab).toHaveClass('font-normal'); + + act(() => { + tab?.click(); + }); + + const { activeLegendId } = store.getState().legend; + expect(activeLegendId).toBe(LEGEND_ID); + + expect(tab).not.toHaveClass('font-normal'); + expect(tab).toHaveClass('bg-[#EBF4FF]'); + }); +}); diff --git a/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.tsx b/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.tsx new file mode 100644 index 00000000..094e8ab9 --- /dev/null +++ b/src/components/Map/Legend/LegendTabs/LegendTab/LegendTab.component.tsx @@ -0,0 +1,28 @@ +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { isActiveLegendSelector } from '@/redux/legend/legend.selectors'; +import { setActiveLegendId } from '@/redux/legend/legend.slice'; +import { Button } from '@/shared/Button'; +import React from 'react'; +import { twMerge } from 'tailwind-merge'; + +type LegendTypeProps = { id: string; name: string }; + +export const LegendTab = ({ id, name }: LegendTypeProps): React.ReactNode => { + const dispatch = useAppDispatch(); + const isActiveLegend = useAppSelector(state => isActiveLegendSelector(state, id)); + + const handleLegendTabClick = (): void => { + dispatch(setActiveLegendId(id)); + }; + + return ( + <Button + className={twMerge('h-10 whitespace-nowrap', isActiveLegend ? 'bg-[#EBF4FF]' : 'font-normal')} + variantStyles={isActiveLegend ? 'secondary' : 'ghost'} + onClick={handleLegendTabClick} + > + {name} + </Button> + ); +}; diff --git a/src/components/Map/Legend/LegendTabs/LegendTab/index.ts b/src/components/Map/Legend/LegendTabs/LegendTab/index.ts new file mode 100644 index 00000000..96003da9 --- /dev/null +++ b/src/components/Map/Legend/LegendTabs/LegendTab/index.ts @@ -0,0 +1 @@ +export { LegendTab } from './LegendTab.component'; diff --git a/src/components/Map/Legend/LegendTabs/LegendTabs.component.tsx b/src/components/Map/Legend/LegendTabs/LegendTabs.component.tsx new file mode 100644 index 00000000..216e6ac1 --- /dev/null +++ b/src/components/Map/Legend/LegendTabs/LegendTabs.component.tsx @@ -0,0 +1,16 @@ +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { allLegendsNamesAndIdsSelector } from '@/redux/legend/legend.selectors'; +import React from 'react'; +import { LegendTab } from './LegendTab/LegendTab.component'; + +export const LegendTabs = (): React.ReactNode => { + const allLegendsNamesAndIds = useAppSelector(allLegendsNamesAndIdsSelector); + + return ( + <div className="flex h-10 w-full flex-row flex-nowrap justify-start border-b border-b-divide bg-white-pearl text-xs"> + {allLegendsNamesAndIds.map(({ id, name }) => ( + <LegendTab name={name} id={id} key={id} /> + ))} + </div> + ); +}; diff --git a/src/components/Map/Legend/LegendTabs/index.ts b/src/components/Map/Legend/LegendTabs/index.ts new file mode 100644 index 00000000..bea41d1d --- /dev/null +++ b/src/components/Map/Legend/LegendTabs/index.ts @@ -0,0 +1 @@ +export { LegendTabs } from './LegendTabs.component'; diff --git a/src/components/Map/PluginsDrawer/PluginsHeader/PluginHeaderInfo/PluginHeaderInfo.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsHeader/PluginHeaderInfo/PluginHeaderInfo.component.test.tsx index fa013b07..7729070d 100644 --- a/src/components/Map/PluginsDrawer/PluginsHeader/PluginHeaderInfo/PluginHeaderInfo.component.test.tsx +++ b/src/components/Map/PluginsDrawer/PluginsHeader/PluginHeaderInfo/PluginHeaderInfo.component.test.tsx @@ -15,6 +15,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import axios, { HttpStatusCode } from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { MockStoreEnhanced } from 'redux-mock-store'; +import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock'; import { PluginHeaderInfo } from './PluginHeaderInfo.component'; import { RELOAD_PLUGIN_DRAWER_BUTTON_ROLE } from './PluginHeaderInfo.constants'; @@ -57,6 +58,7 @@ const STATE = { }, list: PLUGINS_INITIAL_STATE_LIST_MOCK, }, + legend: LEGEND_INITIAL_STATE_MOCK, }; describe('PluginHeaderInfo - component', () => { @@ -92,6 +94,10 @@ describe('PluginHeaderInfo - component', () => { expect(actions).toEqual([ { payload: { pluginId: '5e3fcb59588cc311ef9839feea6382eb' }, type: 'plugins/removePlugin' }, + { + payload: '5e3fcb59588cc311ef9839feea6382eb', + type: 'legend/removePluginLegend', + }, ]); await waitFor(() => { diff --git a/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/LoadPluginElement/LoadPluginElement.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/LoadPluginElement/LoadPluginElement.component.test.tsx index 9f6d9e24..f6a4184c 100644 --- a/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/LoadPluginElement/LoadPluginElement.component.test.tsx +++ b/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/LoadPluginElement/LoadPluginElement.component.test.tsx @@ -15,6 +15,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import axios, { HttpStatusCode } from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { MockStoreEnhanced } from 'redux-mock-store'; +import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock'; import { LoadPluginElement } from './LoadPluginElement.component'; const mockedAxiosClient = new MockAdapter(axios); @@ -55,6 +56,7 @@ const STATE = { }, list: PLUGINS_INITIAL_STATE_LIST_MOCK, }, + legend: LEGEND_INITIAL_STATE_MOCK, }; describe('LoadPluginButton - component', () => { diff --git a/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/PluginOpenButton.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/PluginOpenButton.component.test.tsx index 01f6e7fc..43345cb8 100644 --- a/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/PluginOpenButton.component.test.tsx +++ b/src/components/Map/PluginsDrawer/PluginsHeader/PluginOpenButton/PluginOpenButton.component.test.tsx @@ -8,6 +8,7 @@ import { } from '@/utils/testing/getReduxStoreActionsListener'; import { render, screen } from '@testing-library/react'; import { MockStoreEnhanced } from 'redux-mock-store'; +import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock'; import { PluginOpenButton } from './PluginOpenButton.component'; const renderComponent = ( @@ -60,6 +61,7 @@ describe('PluginOpenButton - component', () => { data: PLUGINS_MOCK, }, }, + legend: LEGEND_INITIAL_STATE_MOCK, }); }); diff --git a/src/components/Map/PluginsDrawer/PluginsHeader/PluginsHeader.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsHeader/PluginsHeader.component.test.tsx index 864f1d96..a4fab9d6 100644 --- a/src/components/Map/PluginsDrawer/PluginsHeader/PluginsHeader.component.test.tsx +++ b/src/components/Map/PluginsDrawer/PluginsHeader/PluginsHeader.component.test.tsx @@ -11,6 +11,7 @@ import { } from '@/utils/testing/getReduxStoreActionsListener'; import { render, screen } from '@testing-library/react'; import { MockStoreEnhanced } from 'redux-mock-store'; +import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock'; import { CLOSE_PLUGINS_DRAWER_BUTTON_ROLE } from '../PluginsDrawer.constants'; import { PluginsHeader } from './PluginsHeader.component'; @@ -41,6 +42,7 @@ describe('PluginsHeader - component', () => { currentPluginHash: undefined, }, }, + legend: LEGEND_INITIAL_STATE_MOCK, }); }); @@ -69,6 +71,7 @@ describe('PluginsHeader - component', () => { }, list: PLUGINS_INITIAL_STATE_LIST_MOCK, }, + legend: LEGEND_INITIAL_STATE_MOCK, }); }); diff --git a/src/components/Map/PluginsDrawer/PluginsTabs/PluginSingleTab/PluginSingleTab.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsTabs/PluginSingleTab/PluginSingleTab.component.test.tsx index 157d4220..f9ff782a 100644 --- a/src/components/Map/PluginsDrawer/PluginsTabs/PluginSingleTab/PluginSingleTab.component.test.tsx +++ b/src/components/Map/PluginsDrawer/PluginsTabs/PluginSingleTab/PluginSingleTab.component.test.tsx @@ -1,4 +1,9 @@ -import { FIRST_ARRAY_ELEMENT, SECOND_ARRAY_ELEMENT, THIRD_ARRAY_ELEMENT } from '@/constants/common'; +import { + FIRST_ARRAY_ELEMENT, + SECOND_ARRAY_ELEMENT, + THIRD_ARRAY_ELEMENT, + ZERO, +} from '@/constants/common'; import { PLUGINS_MOCK } from '@/models/mocks/pluginsMock'; import { PLUGINS_INITIAL_STATE_LIST_MOCK, @@ -13,6 +18,7 @@ import { } from '@/utils/testing/getReduxStoreActionsListener'; import { render, screen } from '@testing-library/react'; import { MockStoreEnhanced } from 'redux-mock-store'; +import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock'; import { PluginSingleTab } from './PluginSingleTab.component'; const renderComponent = ( @@ -53,6 +59,7 @@ const STATE = { }, list: PLUGINS_INITIAL_STATE_LIST_MOCK, }, + legend: LEGEND_INITIAL_STATE_MOCK, }; describe('PluginSingleTab - component', () => { @@ -82,9 +89,10 @@ describe('PluginSingleTab - component', () => { hash: PLUGIN.hash, }); - expect(actions).toEqual([ - { payload: { pluginId: '5e3fcb59588cc311ef9839feea6382eb' }, type: 'plugins/removePlugin' }, - ]); + expect(actions[ZERO]).toEqual({ + payload: { pluginId: '5e3fcb59588cc311ef9839feea6382eb' }, + type: 'plugins/removePlugin', + }); }); it('should dispatch close action on close btn click and new current drawer on last active plugin', () => { @@ -116,6 +124,7 @@ describe('PluginSingleTab - component', () => { expect(actions).toEqual([ { payload: { pluginId: '5e3fcb59588cc311ef9839feea6382eb' }, type: 'plugins/removePlugin' }, + { payload: '5e3fcb59588cc311ef9839feea6382eb', type: 'legend/removePluginLegend' }, { payload: '5314b9f996e56e67f0dad65e7df8b73b', type: 'plugins/setCurrentDrawerPluginHash', diff --git a/src/components/Map/PluginsDrawer/PluginsTabs/PluginsTabs.component.test.tsx b/src/components/Map/PluginsDrawer/PluginsTabs/PluginsTabs.component.test.tsx index 64bc4670..ba00c577 100644 --- a/src/components/Map/PluginsDrawer/PluginsTabs/PluginsTabs.component.test.tsx +++ b/src/components/Map/PluginsDrawer/PluginsTabs/PluginsTabs.component.test.tsx @@ -11,6 +11,7 @@ import { } from '@/utils/testing/getReduxStoreActionsListener'; import { render, screen } from '@testing-library/react'; import { MockStoreEnhanced } from 'redux-mock-store'; +import { LEGEND_INITIAL_STATE_MOCK } from '@/redux/legend/legend.mock'; import { PluginsTabs } from './PluginsTabs.component'; const renderComponent = ( @@ -75,6 +76,7 @@ describe('PluginsTabs - component', () => { }, list: PLUGINS_INITIAL_STATE_LIST_MOCK, }, + legend: LEGEND_INITIAL_STATE_MOCK, }); }); diff --git a/src/redux/legend/legend.constants.ts b/src/redux/legend/legend.constants.ts index 5719c74e..ffb97578 100644 --- a/src/redux/legend/legend.constants.ts +++ b/src/redux/legend/legend.constants.ts @@ -1,7 +1,14 @@ import { LegendState } from './legend.types'; +export const DEFAULT_LEGEND_ID = 'MAIN'; + export const LEGEND_INITIAL_STATE: LegendState = { isOpen: false, pluginLegend: {}, - selectedPluginId: undefined, + activeLegendId: DEFAULT_LEGEND_ID, +}; + +export const DEFAULT_LEGEND_TAB = { + name: 'Main Legend', + id: DEFAULT_LEGEND_ID, }; diff --git a/src/redux/legend/legend.mock.ts b/src/redux/legend/legend.mock.ts index 6874d3a6..fe0791d6 100644 --- a/src/redux/legend/legend.mock.ts +++ b/src/redux/legend/legend.mock.ts @@ -1,7 +1,8 @@ +import { DEFAULT_LEGEND_ID } from './legend.constants'; import { LegendState } from './legend.types'; export const LEGEND_INITIAL_STATE_MOCK: LegendState = { isOpen: false, pluginLegend: {}, - selectedPluginId: undefined, + activeLegendId: DEFAULT_LEGEND_ID, }; diff --git a/src/redux/legend/legend.reducers.ts b/src/redux/legend/legend.reducers.ts index be1f0572..8fe3cba2 100644 --- a/src/redux/legend/legend.reducers.ts +++ b/src/redux/legend/legend.reducers.ts @@ -1,5 +1,6 @@ import { PayloadAction } from '@reduxjs/toolkit'; import { LegendState, PluginId } from './legend.types'; +import { DEFAULT_LEGEND_ID } from './legend.constants'; export const openLegendReducer = (state: LegendState): void => { state.isOpen = true; @@ -9,13 +10,37 @@ export const closeLegendReducer = (state: LegendState): void => { state.isOpen = false; }; -export const selectLegendPluginIdReducer = ( +export const setActiveLegendIdReducer = ( state: LegendState, - action: PayloadAction<PluginId>, + action: PayloadAction<string>, ): void => { - state.selectedPluginId = action.payload; + state.activeLegendId = action.payload; }; -export const selectDefaultLegendReducer = (state: LegendState): void => { - state.selectedPluginId = undefined; +export const setDefaultLegendIdReducer = (state: LegendState): void => { + state.activeLegendId = DEFAULT_LEGEND_ID; +}; + +export const setPluginLegendReducer = ( + state: LegendState, + { + payload, + }: PayloadAction<{ + pluginId: PluginId; + legend: string[]; + }>, +): void => { + state.pluginLegend = { + ...state.pluginLegend, + [payload.pluginId]: payload.legend, + }; +}; + +export const removePluginLegendReducer = ( + state: LegendState, + { payload }: PayloadAction<string>, +): void => { + if (state.pluginLegend[payload]) { + delete state.pluginLegend[payload]; + } }; diff --git a/src/redux/legend/legend.selectors.ts b/src/redux/legend/legend.selectors.ts index fdf927f1..91d7277b 100644 --- a/src/redux/legend/legend.selectors.ts +++ b/src/redux/legend/legend.selectors.ts @@ -1,20 +1,51 @@ import { createSelector } from '@reduxjs/toolkit'; +import { BASE_MAP_IMAGES_URL } from '@/constants'; import { defaultLegendImagesSelector } from '../configuration/configuration.selectors'; import { rootSelector } from '../root/root.selectors'; +import { activePluginsDataSelector } from '../plugins/plugins.selectors'; +import { DEFAULT_LEGEND_ID, DEFAULT_LEGEND_TAB } from './legend.constants'; export const legendSelector = createSelector(rootSelector, state => state.legend); export const isLegendOpenSelector = createSelector(legendSelector, state => state.isOpen); -// TODO: add filter for active plugins +export const pluginLegendsSelector = createSelector(legendSelector, state => state.pluginLegend); + export const currentLegendImagesSelector = createSelector( legendSelector, defaultLegendImagesSelector, - ({ selectedPluginId, pluginLegend }, defaultImages) => { - if (selectedPluginId) { - return pluginLegend?.[selectedPluginId] || []; + ({ activeLegendId, pluginLegend }, defaultImages) => { + if (activeLegendId === DEFAULT_LEGEND_ID) + return defaultImages.map(image => `${BASE_MAP_IMAGES_URL}/minerva/${image}`); + + if (activeLegendId) { + return pluginLegend?.[activeLegendId] || []; } - return defaultImages; + return []; + }, +); + +export const allLegendsNamesAndIdsSelector = createSelector( + activePluginsDataSelector, + pluginLegendsSelector, + (activePlugins, pluginLegends) => { + const allPluginLegendsNamesAndIds = Object.keys(pluginLegends).map(hash => { + const plugin = Object.values(activePlugins).find(activePlugin => activePlugin.hash === hash); + + return { + name: plugin?.name || '', + id: plugin?.hash || '', + }; + }); + + return [DEFAULT_LEGEND_TAB, ...allPluginLegendsNamesAndIds]; }, ); + +export const activeLegendIdSelector = createSelector(legendSelector, state => state.activeLegendId); + +export const isActiveLegendSelector = createSelector( + [activeLegendIdSelector, (_, legendId: string): string => legendId], + (activeLegendId, legendId) => activeLegendId === legendId, +); diff --git a/src/redux/legend/legend.slice.ts b/src/redux/legend/legend.slice.ts index fbb62f00..2dccdb5a 100644 --- a/src/redux/legend/legend.slice.ts +++ b/src/redux/legend/legend.slice.ts @@ -1,6 +1,13 @@ import { createSlice } from '@reduxjs/toolkit'; import { LEGEND_INITIAL_STATE } from './legend.constants'; -import { closeLegendReducer, openLegendReducer } from './legend.reducers'; +import { + closeLegendReducer, + openLegendReducer, + removePluginLegendReducer, + setActiveLegendIdReducer, + setDefaultLegendIdReducer, + setPluginLegendReducer, +} from './legend.reducers'; const legendSlice = createSlice({ name: 'legend', @@ -8,9 +15,20 @@ const legendSlice = createSlice({ reducers: { openLegend: openLegendReducer, closeLegend: closeLegendReducer, + setPluginLegend: setPluginLegendReducer, + setActiveLegendId: setActiveLegendIdReducer, + setDefaultLegendId: setDefaultLegendIdReducer, + removePluginLegend: removePluginLegendReducer, }, }); -export const { openLegend, closeLegend } = legendSlice.actions; +export const { + openLegend, + closeLegend, + setPluginLegend, + setActiveLegendId, + setDefaultLegendId, + removePluginLegend, +} = legendSlice.actions; export default legendSlice.reducer; diff --git a/src/redux/legend/legend.types.ts b/src/redux/legend/legend.types.ts index 80314aec..ad797feb 100644 --- a/src/redux/legend/legend.types.ts +++ b/src/redux/legend/legend.types.ts @@ -1,8 +1,8 @@ -export type PluginId = number; +export type PluginId = string; export type ImageUrl = string; export type LegendState = { isOpen: boolean; pluginLegend: Record<PluginId, ImageUrl[]>; - selectedPluginId: PluginId | undefined; + activeLegendId: string; }; diff --git a/src/services/pluginsManager/legend/removeLegend.ts b/src/services/pluginsManager/legend/removeLegend.ts new file mode 100644 index 00000000..70fb75ed --- /dev/null +++ b/src/services/pluginsManager/legend/removeLegend.ts @@ -0,0 +1,14 @@ +import { isActiveLegendSelector } from '@/redux/legend/legend.selectors'; +import { removePluginLegend, setDefaultLegendId } from '@/redux/legend/legend.slice'; +import { store } from '@/redux/store'; + +export const removeLegend = (pluginId: string): void => { + const isActivePluginLegend = isActiveLegendSelector(store.getState(), pluginId); + const { dispatch } = store; + + if (isActivePluginLegend) { + dispatch(setDefaultLegendId()); + } + + dispatch(removePluginLegend(pluginId)); +}; diff --git a/src/services/pluginsManager/legend/setLegend.ts b/src/services/pluginsManager/legend/setLegend.ts new file mode 100644 index 00000000..4d50c9f6 --- /dev/null +++ b/src/services/pluginsManager/legend/setLegend.ts @@ -0,0 +1,13 @@ +import { setPluginLegend } from '@/redux/legend/legend.slice'; +import { store } from '@/redux/store'; + +export const setLegend = (pluginId: string, legend: string[]): void => { + const { dispatch } = store; + + dispatch( + setPluginLegend({ + pluginId, + legend, + }), + ); +}; diff --git a/src/services/pluginsManager/pluginsManager.ts b/src/services/pluginsManager/pluginsManager.ts index 0b083ef9..e8e4b924 100644 --- a/src/services/pluginsManager/pluginsManager.ts +++ b/src/services/pluginsManager/pluginsManager.ts @@ -33,6 +33,8 @@ import { getVersion } from './project/data/getVersion'; import { getBounds } from './map/data/getBounds'; import { fitBounds } from './map/fitBounds'; import { getOpenMapId } from './map/getOpenMapId'; +import { setLegend } from './legend/setLegend'; +import { removeLegend } from './legend/removeLegend'; export const PluginsManager: PluginsManagerType = { hashedPlugins: {}, @@ -132,6 +134,10 @@ export const PluginsManager: PluginsManagerType = { removeListener: PluginsEventBus.removeListener.bind(this, hash), removeAllListeners: PluginsEventBus.removeAllListeners.bind(this, hash), }, + legend: { + setLegend: setLegend.bind(this, hash), + removeLegend: removeLegend.bind(this, hash), + }, }; }, createAndGetPluginContent({ hash }) { -- GitLab