Skip to content
Snippets Groups Projects
Commit 79664147 authored by mateusz-winiarczyk's avatar mateusz-winiarczyk
Browse files

feat(plugins): legend tabs (MIN-231)

parent 119c70c2
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...,!153feat(plugins): legend tabs (MIN-231)
Showing
with 419 additions and 57 deletions
### 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();
```
......@@ -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',
},
]);
});
});
});
......@@ -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 });
};
......
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();
});
});
});
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>
);
......
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);
});
});
});
/* 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>
);
......
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]');
});
});
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>
);
};
export { LegendTab } from './LegendTab.component';
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>
);
};
export { LegendTabs } from './LegendTabs.component';
......@@ -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(() => {
......
......@@ -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', () => {
......
......@@ -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,
});
});
......
......@@ -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,
});
});
......
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',
......
......@@ -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,
});
});
......
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,
};
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,
};
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