diff --git a/src/components/FunctionalArea/NavBar/NavBar.component.tsx b/src/components/FunctionalArea/NavBar/NavBar.component.tsx index fbc5c000ee41e1dd810219d5d9cf181263fc02a7..830131c1ac047fd046c01ee9e585b48d876cf8c5 100644 --- a/src/components/FunctionalArea/NavBar/NavBar.component.tsx +++ b/src/components/FunctionalArea/NavBar/NavBar.component.tsx @@ -15,7 +15,7 @@ export const NavBar = (): JSX.Element => { }; const openDrawerPlugins = (): void => { - dispatch(openDrawer('plugins')); + dispatch(openDrawer('available-plugins')); }; const openDrawerExport = (): void => { diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.test.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..06d70d35abe59da6cf05b3c3700d991746cc3db8 --- /dev/null +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.test.tsx @@ -0,0 +1,53 @@ +import { PLUGINS_MOCK } from '@/models/mocks/pluginsMock'; +import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; +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 { AvailablePluginsDrawer } from './AvailablePluginsDrawer.component'; + +const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStore); + return ( + render( + <Wrapper> + <AvailablePluginsDrawer /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('AvailablePluginsDrawer - component', () => { + describe('when always', () => { + it('should render drawer heading', () => { + renderComponent(INITIAL_STORE_STATE_MOCK); + const drawerTitle = screen.getByText('Available plugins'); + expect(drawerTitle).toBeInTheDocument(); + }); + + it('should render load plugin from url', () => { + renderComponent(INITIAL_STORE_STATE_MOCK); + const loadPluginFromUrlInput = screen.getByTestId('load-plugin-input-url'); + expect(loadPluginFromUrlInput).toBeInTheDocument(); + }); + + it.each(PLUGINS_MOCK)('should render render all public plugins', currentPlugin => { + renderComponent({ + ...INITIAL_STORE_STATE_MOCK, + plugins: { + ...INITIAL_STORE_STATE_MOCK.plugins, + list: { + ...INITIAL_STORE_STATE_MOCK.plugins.list, + data: PLUGINS_MOCK, + }, + }, + }); + + const pluginLabel = screen.getByText(currentPlugin.name); + expect(pluginLabel).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d1343d91bbf51f36f926c2af248d596790155f04 --- /dev/null +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx @@ -0,0 +1,21 @@ +import { publicPluginsListSelector } from '@/redux/plugins/plugins.selectors'; +import { DrawerHeading } from '@/shared/DrawerHeading'; +import { useSelector } from 'react-redux'; +import { LoadPlugin } from './LoadPlugin'; +import { LoadPluginFromUrl } from './LoadPluginFromUrl'; + +export const AvailablePluginsDrawer = (): JSX.Element => { + const publicPlugins = useSelector(publicPluginsListSelector); + + return ( + <div className="h-full max-h-full" data-testid="available-plugins-drawer"> + <DrawerHeading title="Available plugins" /> + <div className="flex flex-col gap-6 p-6"> + <LoadPluginFromUrl /> + {publicPlugins.map(plugin => ( + <LoadPlugin key={plugin.hash} plugin={plugin} /> + ))} + </div> + </div> + ); +}; diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..92c659ac76bde94184d4324be245989f7c8f4dec --- /dev/null +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx @@ -0,0 +1,29 @@ +import { FIRST_ARRAY_ELEMENT } from '@/constants/common'; +import { PLUGINS_MOCK } from '@/models/mocks/pluginsMock'; +import { render, screen } from '@testing-library/react'; +import { LoadPlugin, Props } from './LoadPlugin.component'; + +const renderComponent = ({ plugin }: Props): void => { + render(<LoadPlugin plugin={plugin} />); +}; + +describe('LoadPlugin - component', () => { + describe('when always', () => { + const plugin = PLUGINS_MOCK[FIRST_ARRAY_ELEMENT]; + + it('renders plugin name', () => { + renderComponent({ plugin }); + + const title = screen.getByText(plugin.name); + expect(title).toBeInTheDocument(); + }); + + it('renders plugin load button', () => { + renderComponent({ plugin }); + + const loadButton = screen.getByText('Load'); + expect(loadButton.tagName).toBe('BUTTON'); + expect(loadButton).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fee403ca18cb34195aaf42a559ba618eb68f821e --- /dev/null +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx @@ -0,0 +1,25 @@ +import { Button } from '@/shared/Button'; +import { MinervaPlugin } from '@/types/models'; + +export interface Props { + plugin: MinervaPlugin; +} + +export const LoadPlugin = ({ plugin }: Props): JSX.Element => { + const handleLoadPlugin = (): void => { + // TODO: handleLoadPlugin + }; + + return ( + <div className="flex w-full items-center justify-between"> + <span className="text-cetacean-blue">{plugin.name}</span> + <Button + variantStyles="secondary" + className="h-10 self-end rounded-e rounded-s" + onClick={handleLoadPlugin} + > + Load + </Button> + </div> + ); +}; diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/index.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e27e6ba35e4443188bf5a6e08a655ddb97b3262c --- /dev/null +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/index.ts @@ -0,0 +1 @@ +export { LoadPlugin } from './LoadPlugin.component'; diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e1d83a8ba9fde4258d36ad1d5df8629ac583ce57 --- /dev/null +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx @@ -0,0 +1,32 @@ +import { render, screen } from '@testing-library/react'; +import { LoadPluginFromUrl } from './LoadPluginFromUrl.component'; + +const renderComponent = (): void => { + render(<LoadPluginFromUrl />); +}; + +describe('LoadPluginFromUrl - component', () => { + describe('when always', () => { + it('renders plugin input label', () => { + renderComponent(); + + const pluginInputLabel = screen.getByLabelText('URL:'); + expect(pluginInputLabel).toBeInTheDocument(); + }); + + it('renders plugin input', () => { + renderComponent(); + + const pluginInput = screen.getByTestId('load-plugin-input-url'); + expect(pluginInput).toBeInTheDocument(); + }); + + it('renders plugin load button', () => { + renderComponent(); + + const loadButton = screen.getByText('Load'); + expect(loadButton.tagName).toBe('BUTTON'); + expect(loadButton).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..de486ad2bb19462bca3674a24f9a72a126d1f75c --- /dev/null +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx @@ -0,0 +1,32 @@ +import { Button } from '@/shared/Button'; +import { useState } from 'react'; + +export const LoadPluginFromUrl = (): JSX.Element => { + const [url, setUrl] = useState<string>(''); + + const handleLoadPlugin = (): void => { + // TODO: handleLoadPlugin + }; + + return ( + <div className="flex w-full"> + <label className="flex w-full flex-col gap-2 text-sm text-cetacean-blue"> + <span>URL:</span> + <input + className="h-10 w-full bg-cultured p-3" + type="url" + value={url} + onChange={(e): void => setUrl(e.target.value)} + data-testid="load-plugin-input-url" + /> + </label> + <Button + variantStyles="secondary" + className="h-10 self-end rounded-e rounded-s" + onClick={handleLoadPlugin} + > + Load + </Button> + </div> + ); +}; diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/index.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..cedb0f665553574022fb55f4f00ef9678b546f1d --- /dev/null +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/index.ts @@ -0,0 +1 @@ +export { LoadPluginFromUrl } from './LoadPluginFromUrl.component'; diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/index.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..85f2fe7f30853779b57a030199940cd3055f8e3a --- /dev/null +++ b/src/components/Map/Drawer/AvailablePluginsDrawer/index.ts @@ -0,0 +1 @@ +export { AvailablePluginsDrawer } from './AvailablePluginsDrawer.component'; diff --git a/src/components/Map/Drawer/Drawer.component.tsx b/src/components/Map/Drawer/Drawer.component.tsx index 4403fe9b6c719c971445feb36dafcd0d55f60329..de1aa94ea44f6f9e2203ea3a386e373cbce620d6 100644 --- a/src/components/Map/Drawer/Drawer.component.tsx +++ b/src/components/Map/Drawer/Drawer.component.tsx @@ -2,14 +2,14 @@ import { DRAWER_ROLE } from '@/components/Map/Drawer/Drawer.constants'; import { drawerSelector } from '@/redux/drawer/drawer.selectors'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { twMerge } from 'tailwind-merge'; -import { ReactionDrawer } from './ReactionDrawer'; -import { SearchDrawerWrapper as SearchDrawerContent } from './SearchDrawerWrapper'; -import { SubmapsDrawer } from './SubmapsDrawer'; -import { OverlaysDrawer } from './OverlaysDrawer'; +import { AvailablePluginsDrawer } from './AvailablePluginsDrawer'; import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component'; import { ExportDrawer } from './ExportDrawer'; +import { OverlaysDrawer } from './OverlaysDrawer'; import { ProjectInfoDrawer } from './ProjectInfoDrawer'; -import { PluginsDrawer } from './PluginDrawer'; +import { ReactionDrawer } from './ReactionDrawer'; +import { SearchDrawerWrapper as SearchDrawerContent } from './SearchDrawerWrapper'; +import { SubmapsDrawer } from './SubmapsDrawer'; export const Drawer = (): JSX.Element => { const { isOpen, drawerName } = useAppSelector(drawerSelector); @@ -29,7 +29,7 @@ export const Drawer = (): JSX.Element => { {isOpen && drawerName === 'bio-entity' && <BioEntityDrawer />} {isOpen && drawerName === 'project-info' && <ProjectInfoDrawer />} {isOpen && drawerName === 'export' && <ExportDrawer />} - {isOpen && drawerName === 'plugins' && <PluginsDrawer />} + {isOpen && drawerName === 'available-plugins' && <AvailablePluginsDrawer />} </div> ); }; diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayLineFeature.test.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayLineFeature.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..74f6839491421bf93129b446cc6d543636b7961f --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayLineFeature.test.ts @@ -0,0 +1,91 @@ +import { LINE_WIDTH } from '@/constants/canvas'; +import { initialMapStateFixture } from '@/redux/map/map.fixtures'; +import { LinePoint } from '@/types/reactions'; +import { usePointToProjection } from '@/utils/map/usePointToProjection'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { renderHook } from '@testing-library/react'; +import { Feature } from 'ol'; +import { Geometry } from 'ol/geom'; +import { createOverlayLineFeature } from './createOverlayLineFeature'; + +/* eslint-disable no-magic-numbers */ +const CASES: [LinePoint, number[]][] = [ + [ + [ + { x: 0, y: 0 }, + { x: 0, y: 0 }, + ], + [0, 0, 0, 0], + ], + [ + [ + { x: 0, y: 0 }, + { x: 100, y: 100 }, + ], + [0, -238107693, Infinity, 0], + ], + [ + [ + { x: 100, y: 100 }, + { x: 0, y: 0 }, + ], + [0, -238107693, Infinity, 0], + ], + [ + [ + { x: 100, y: 0 }, + { x: 0, y: 100 }, + ], + [0, 0, 0, 0], + ], + [ + [ + { x: -50, y: 0 }, + { x: 0, y: -50 }, + ], + [0, 0, 0, 0], + ], +]; + +const COLOR = '#FFB3B3cc'; + +const getFeature = (linePoint: LinePoint): Feature<Geometry> => { + const { Wrapper } = getReduxWrapperWithStore({ + map: initialMapStateFixture, + }); + const { + result: { current: pointToProjection }, + } = renderHook(() => usePointToProjection(), { + wrapper: Wrapper, + }); + + return createOverlayLineFeature(linePoint, { pointToProjection, color: COLOR }); +}; + +describe('createOverlayLineFeature - util', () => { + it.each(CASES)('should return Feature instance', linePoint => { + const feature = getFeature(linePoint); + + expect(feature).toBeInstanceOf(Feature); + }); + + it.each(CASES)('should return Feature instance with valid style and stroke', linePoint => { + const feature = getFeature(linePoint); + const style = feature.getStyle(); + + expect(style).toMatchObject({ + fill_: { color_: COLOR }, + stroke_: { + color_: COLOR, + width_: LINE_WIDTH, + }, + }); + }); + + it.each(CASES)('should return Feature instance with valid geometry', (linePoint, extent) => { + const feature = getFeature(linePoint); + const geometry = feature.getGeometry(); + + expect(geometry?.getExtent()).toEqual(extent); + }); +}); diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayLineFeature.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayLineFeature.ts new file mode 100644 index 0000000000000000000000000000000000000000..b38d603bbb793c6578722f2c8170cd8f5c895b51 --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/createOverlayLineFeature.ts @@ -0,0 +1,22 @@ +import { LinePoint } from '@/types/reactions'; +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import { Feature } from 'ol'; +import { SimpleGeometry } from 'ol/geom'; +import { getLineFeature } from '../reactionsLayer/getLineFeature'; +import { getOverlayLineFeatureStyle } from './getOverlayLineFeatureStyle'; + +interface Options { + color: string; + pointToProjection: UsePointToProjectionResult; +} + +export const createOverlayLineFeature = ( + points: LinePoint, + { color, pointToProjection }: Options, +): Feature<SimpleGeometry> => { + const feature = getLineFeature(points, pointToProjection); + + feature.setStyle(getOverlayLineFeatureStyle(color)); + + return feature; +}; diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/getOverlayLineFeatureStyle.test.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/getOverlayLineFeatureStyle.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..174c620d5c12b95a9033da7cf11a04a942302c91 --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/getOverlayLineFeatureStyle.test.ts @@ -0,0 +1,27 @@ +import { LINE_WIDTH } from '@/constants/canvas'; +import { Fill, Stroke, Style } from 'ol/style'; +import { getOverlayLineFeatureStyle } from './getOverlayLineFeatureStyle'; + +const COLORS = ['#000000', '#FFFFFF', '#F5F5F5', '#C0C0C0', '#C0C0C0aa', '#C0C0C0bb']; + +describe('getOverlayLineFeatureStyle - util', () => { + it.each(COLORS)('should return Style object', color => { + const result = getOverlayLineFeatureStyle(color); + expect(result).toBeInstanceOf(Style); + }); + + it.each(COLORS)('should set valid color values for Fill', color => { + const result = getOverlayLineFeatureStyle(color); + const fill = result.getFill(); + expect(fill).toBeInstanceOf(Fill); + expect(fill?.getColor()).toBe(color); + }); + + it.each(COLORS)('should set valid color values for Fill', color => { + const result = getOverlayLineFeatureStyle(color); + const stroke = result.getStroke(); + expect(stroke).toBeInstanceOf(Stroke); + expect(stroke?.getColor()).toBe(color); + expect(stroke?.getWidth()).toBe(LINE_WIDTH); + }); +}); diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/getOverlayLineFeatureStyle.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/getOverlayLineFeatureStyle.ts new file mode 100644 index 0000000000000000000000000000000000000000..407c2416bbcee994a3d954d866520e0f2d65064c --- /dev/null +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/getOverlayLineFeatureStyle.ts @@ -0,0 +1,5 @@ +import { LINE_WIDTH } from '@/constants/canvas'; +import { Fill, Stroke, Style } from 'ol/style'; + +export const getOverlayLineFeatureStyle = (color: string): Style => + new Style({ fill: new Fill({ color }), stroke: new Stroke({ color, width: LINE_WIDTH }) }); diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.test.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.test.ts index b580a998904b2975b82c0a527aa805c748c38288..61841611e83b4818cf7c1382cc0ff8795e85996e 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.test.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/useGetOverlayColor.test.ts @@ -1,7 +1,7 @@ -import { renderHook } from '@testing-library/react'; -import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; import { CONFIGURATION_INITIAL_STORE_MOCKS } from '@/redux/configuration/configuration.mock'; import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { renderHook } from '@testing-library/react'; import { useGetOverlayColor } from './useGetOverlayColor'; describe('useOverlayFeatures - hook', () => { @@ -20,6 +20,7 @@ describe('useOverlayFeatures - hook', () => { describe('getOverlayBioEntityColorByAvailableProperties - function', () => { const ENTITY: OverlayBioEntityRender = { + type: 'rectangle', id: 0, modelId: 0, x1: 0, diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.test.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.test.ts index 19c9b309415c6943c7c86e8b1fe477c606affe81..15c9cf3e5e9820d3d3f9d604bc19a7e5dad1c383 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.test.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.test.ts @@ -1,11 +1,11 @@ /* eslint-disable no-magic-numbers */ -import { renderHook } from '@testing-library/react'; -import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; import { CONFIGURATION_INITIAL_STORE_MOCKS } from '@/redux/configuration/configuration.mock'; -import { OVERLAYS_PUBLIC_FETCHED_STATE_MOCK } from '@/redux/overlays/overlays.mock'; import { mapStateWithCurrentlySelectedMainMapFixture } from '@/redux/map/map.fixtures'; import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock'; import { MOCKED_OVERLAY_BIO_ENTITY_RENDER } from '@/redux/overlayBioEntity/overlayBioEntity.mock'; +import { OVERLAYS_PUBLIC_FETCHED_STATE_MOCK } from '@/redux/overlays/overlays.mock'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { renderHook } from '@testing-library/react'; import { useOverlayFeatures } from './useOverlayFeatures'; /** @@ -52,7 +52,9 @@ describe('useOverlayFeatures', () => { wrapper: Wrapper, }); - expect(features).toHaveLength(6); + expect(features).toHaveLength(10); + + // type: rectangle expect(features[0].getGeometry()?.getCoordinates()).toEqual([ [ [-13149141, 18867005], @@ -65,5 +67,19 @@ describe('useOverlayFeatures', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore expect(features[0].getStyle().getFill().getColor()).toBe('#FFFFFFcc'); + + // type: line + expect(features[7].getGeometry()?.getCoordinates()).toEqual([ + [ + [-13149141, 18867005], + [-13149141, 18881970], + [-13141659, 18881970], + [-13141659, 18867005], + [-13149141, 18867005], + ], + ]); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(features[7].getStyle().getFill().getColor()).toBe('#ff0000cc'); }); }); diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts index 06985195eb9dafe82dbf23f616b3214945c9aff2..477a95dd324f06f8099d88db81055e7324b77d5f 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/useOverlayFeatures.ts @@ -1,18 +1,21 @@ -import { useMemo } from 'react'; -import { usePointToProjection } from '@/utils/map/usePointToProjection'; -import type Feature from 'ol/Feature'; -import type Polygon from 'ol/geom/Polygon'; import { ZERO } from '@/constants/common'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { getOverlayOrderSelector, overlayBioEntitiesForCurrentModelSelector, } from '@/redux/overlayBioEntity/overlayBioEntity.selector'; +import { LinePoint } from '@/types/reactions'; +import { usePointToProjection } from '@/utils/map/usePointToProjection'; +import type Feature from 'ol/Feature'; +import { SimpleGeometry } from 'ol/geom'; +import type Polygon from 'ol/geom/Polygon'; +import { useMemo } from 'react'; import { createOverlayGeometryFeature } from './createOverlayGeometryFeature'; +import { createOverlayLineFeature } from './createOverlayLineFeature'; import { getPolygonLatitudeCoordinates } from './getPolygonLatitudeCoordinates'; import { useGetOverlayColor } from './useGetOverlayColor'; -export const useOverlayFeatures = (): Feature<Polygon>[] => { +export const useOverlayFeatures = (): Feature<Polygon>[] | Feature<SimpleGeometry>[] => { const pointToProjection = usePointToProjection(); const { getOverlayBioEntityColorByAvailableProperties } = useGetOverlayColor(); const overlaysOrder = useAppSelector(getOverlayOrderSelector); @@ -34,12 +37,27 @@ export const useOverlayFeatures = (): Feature<Polygon>[] => { overlaysOrder.find(({ id }) => id === entity.overlayId)?.index || ZERO, }); - return createOverlayGeometryFeature( + const color = getOverlayBioEntityColorByAvailableProperties(entity); + + if (entity.type === 'rectangle') { + return createOverlayGeometryFeature( + [ + ...pointToProjection({ x: xMin, y: entity.y1 }), + ...pointToProjection({ x: xMax, y: entity.y2 }), + ], + color, + ); + } + + return createOverlayLineFeature( [ - ...pointToProjection({ x: xMin, y: entity.y1 }), - ...pointToProjection({ x: xMax, y: entity.y2 }), - ], - getOverlayBioEntityColorByAvailableProperties(entity), + { x: entity.x1, y: entity.y1 }, + { x: entity.x2, y: entity.y2 }, + ] as LinePoint, + { + color, + pointToProjection, + }, ); }), [overlaysOrder, bioEntities, pointToProjection, getOverlayBioEntityColorByAvailableProperties], diff --git a/src/components/Map/MapViewer/utils/config/reactionsLayer/getLineFeature.ts b/src/components/Map/MapViewer/utils/config/reactionsLayer/getLineFeature.ts index 40211171605c35ff87995e6ed9abf02bc300c76b..b7ce166ad5bf96b39976fc6d08fabb8fae5e6f1a 100644 --- a/src/components/Map/MapViewer/utils/config/reactionsLayer/getLineFeature.ts +++ b/src/components/Map/MapViewer/utils/config/reactionsLayer/getLineFeature.ts @@ -1,12 +1,12 @@ import { LinePoint } from '@/types/reactions'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; -import { LineString } from 'ol/geom'; +import { LineString, SimpleGeometry } from 'ol/geom'; export const getLineFeature = ( linePoints: LinePoint, pointToProjection: UsePointToProjectionResult, -): Feature => { +): Feature<SimpleGeometry> => { const points = linePoints.map(pointToProjection); return new Feature({ diff --git a/src/models/fixtures/overlayBioEntityFixture.ts b/src/models/fixtures/overlayBioEntityFixture.ts index da0c6da654ba996874863860034ebf6856762054..4cdeaebfde4d08c304d56de8d8db889a385b7c85 100644 --- a/src/models/fixtures/overlayBioEntityFixture.ts +++ b/src/models/fixtures/overlayBioEntityFixture.ts @@ -2,9 +2,21 @@ import { ZOD_SEED } from '@/constants'; import { z } from 'zod'; // eslint-disable-next-line import/no-extraneous-dependencies import { createFixture } from 'zod-fixture'; -import { overlayBioEntitySchema } from '../overlayBioEntitySchema'; +import { + overlayBioEntitySchema, + overlayElementWithBioEntitySchema, + overlayElementWithReactionSchema, +} from '../overlayBioEntitySchema'; export const overlayBioEntityFixture = createFixture(z.array(overlayBioEntitySchema), { seed: ZOD_SEED, array: { min: 3, max: 3 }, }); + +export const overlayElementWithReactionFixture = createFixture(overlayElementWithReactionSchema, { + seed: ZOD_SEED, +}); + +export const overlayElementWithBioEntityFixture = createFixture(overlayElementWithBioEntitySchema, { + seed: ZOD_SEED, +}); diff --git a/src/models/mocks/pluginsMock.ts b/src/models/mocks/pluginsMock.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5c91eafdb32833dff36caedddfc4b85d9bac2f6 --- /dev/null +++ b/src/models/mocks/pluginsMock.ts @@ -0,0 +1,44 @@ +import { MinervaPlugin } from '@/types/models'; + +export const PLUGINS_MOCK: MinervaPlugin[] = [ + { + hash: '5e3fcb59588cc311ef9839feea6382eb', + name: 'Disease-variant associations', + version: '1.0.0', + isPublic: true, + isDefault: false, + urls: ['https://minerva-service.lcsb.uni.lu/plugins/disease-associations/plugin.js'], + }, + { + hash: '20df86476c311824bbfe73d1034af89e', + name: 'GSEA', + version: '0.9.2', + isPublic: true, + isDefault: false, + urls: ['https://minerva-service.lcsb.uni.lu/plugins/gsea/plugin.js'], + }, + { + hash: '5314b9f996e56e67f0dad65e7df8b73b', + name: 'PD map guide', + version: '1.0.2', + isPublic: true, + isDefault: false, + urls: ['https://minerva-service.lcsb.uni.lu/plugins/guide/plugin.js'], + }, + { + hash: 'b85ae2f4cd67736489b5fd2b635b1013', + name: 'Map exploation', + version: '1.0.0', + isPublic: true, + isDefault: false, + urls: ['https://minerva-service.lcsb.uni.lu/plugins/exploration/plugin.js'], + }, + { + hash: '77c32edf387652dfaad8a20f2a0ce76b', + name: 'Drug reactions', + version: '1.0.0', + isPublic: true, + isDefault: false, + urls: ['https://minerva-service.lcsb.uni.lu/plugins/drug-reactions/plugin.js'], + }, +]; diff --git a/src/models/overlayBioEntitySchema.ts b/src/models/overlayBioEntitySchema.ts index d9dd58950b85a6d21bdde12e38e668af4d9b30e4..ffa8f5847d578da84236314ce036d5bc425d123c 100644 --- a/src/models/overlayBioEntitySchema.ts +++ b/src/models/overlayBioEntitySchema.ts @@ -1,8 +1,19 @@ import { z } from 'zod'; import { overlayLeftBioEntitySchema } from './overlayLeftBioEntitySchema'; +import { overlayLeftReactionSchema } from './overlayLeftReactionSchema'; import { overlayRightBioEntitySchema } from './overlayRightBioEntitySchema'; -export const overlayBioEntitySchema = z.object({ +export const overlayElementWithBioEntitySchema = z.object({ left: overlayLeftBioEntitySchema, right: overlayRightBioEntitySchema, }); + +export const overlayElementWithReactionSchema = z.object({ + left: overlayLeftReactionSchema, + right: overlayRightBioEntitySchema, +}); + +export const overlayBioEntitySchema = z.union([ + overlayElementWithBioEntitySchema, + overlayElementWithReactionSchema, +]); diff --git a/src/models/overlayLeftReactionSchema.ts b/src/models/overlayLeftReactionSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..c47febcff7136233ac38e7d4c41c6eb3d85c2144 --- /dev/null +++ b/src/models/overlayLeftReactionSchema.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; +import { lineSchema } from './lineSchema'; +import { reactionProduct } from './reactionProduct'; +import { referenceSchema } from './referenceSchema'; + +export const overlayLeftReactionSchema = z.object({ + id: z.number(), + notes: z.string(), + idReaction: z.string(), + name: z.string(), + reversible: z.boolean(), + symbol: z.null(), + abbreviation: z.null(), + formula: z.null(), + mechanicalConfidenceScore: z.null(), + lowerBound: z.null(), + upperBound: z.null(), + subsystem: z.null(), + geneProteinReaction: z.null(), + visibilityLevel: z.string(), + z: z.number(), + synonyms: z.array(z.unknown()), + model: z.number(), + kinetics: z.null(), + line: lineSchema, + processCoordinates: z.null(), + stringType: z.string(), + modifiers: z.array(reactionProduct), + reactants: z.array(reactionProduct), + products: z.array(reactionProduct), + elementId: z.string(), + operators: z.array(z.unknown()), + references: z.array(referenceSchema), +}); diff --git a/src/models/reactionProduct.ts b/src/models/reactionProduct.ts new file mode 100644 index 0000000000000000000000000000000000000000..96905877910e382f1736d595351286f5fa636807 --- /dev/null +++ b/src/models/reactionProduct.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; +import { lineSchema } from './lineSchema'; + +export const reactionProduct = z.object({ + id: z.number(), + line: lineSchema, + stoichiometry: z.null(), + element: z.number(), +}); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 1638e3328327a0791d63f174dea299d029ae0d3a..5193c5e4865e5ad7d21c71896c3e666f27392ed9 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -1,6 +1,6 @@ import { PROJECT_ID } from '@/constants'; -import { PerfectSearchParams } from '@/types/search'; import { Point } from '@/types/map'; +import { PerfectSearchParams } from '@/types/search'; export const apiPath = { getBioEntityContentsStringWithQuery: ({ @@ -65,4 +65,5 @@ export const apiPath = { getTaxonomy: (taxonomyId: string): string => `taxonomy/${taxonomyId}`, registerPluign: (): string => `plugins/`, getPlugin: (pluginId: string): string => `plugins/${pluginId}/`, + getAllPlugins: (): string => `/plugins/`, }; diff --git a/src/redux/overlayBioEntity/overlayBioEntity.mock.ts b/src/redux/overlayBioEntity/overlayBioEntity.mock.ts index 84fab91d714d1acdbb49aa3275b6b5fe13cc668c..9b093c3eeaecee77b8fc2923f37f8cd3da8c5eec 100644 --- a/src/redux/overlayBioEntity/overlayBioEntity.mock.ts +++ b/src/redux/overlayBioEntity/overlayBioEntity.mock.ts @@ -8,6 +8,7 @@ export const OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK: OverlaysBioEntityState = { export const MOCKED_OVERLAY_BIO_ENTITY_RENDER: OverlayBioEntityRender[] = [ { + type: 'rectangle', id: 1, modelId: 52, width: 30, @@ -21,6 +22,7 @@ export const MOCKED_OVERLAY_BIO_ENTITY_RENDER: OverlayBioEntityRender[] = [ color: null, }, { + type: 'rectangle', id: 2, modelId: 52, width: 30, @@ -34,6 +36,7 @@ export const MOCKED_OVERLAY_BIO_ENTITY_RENDER: OverlayBioEntityRender[] = [ color: null, }, { + type: 'rectangle', id: 3, modelId: 52, width: 40, @@ -46,4 +49,32 @@ export const MOCKED_OVERLAY_BIO_ENTITY_RENDER: OverlayBioEntityRender[] = [ value: null, color: { rgb: -65536, alpha: 0 }, }, + { + type: 'line', + id: 66143, + modelId: 52, + x1: 4462.61826820353, + x2: 4571.99387254902, + y1: 7105.89040426431, + y2: 6979.823529411765, + width: 109.3756043454905, + height: 126.06687485254497, + value: null, + overlayId: 20, + color: null, + }, + { + type: 'line', + id: 66144, + modelId: 52, + x1: 4454.850442288663, + x2: 4463.773636826477, + y1: 7068.434324866321, + y2: 7112.188429617157, + width: 8.923194537814197, + height: 43.75410475083663, + value: null, + overlayId: 20, + color: null, + }, ]; diff --git a/src/redux/overlayBioEntity/overlayBioEntity.utils.ts b/src/redux/overlayBioEntity/overlayBioEntity.utils.ts index 7c92f7a20080e78660bcf391194990c13940451b..6656d725badcafef167e19d63226804077ca4a21 100644 --- a/src/redux/overlayBioEntity/overlayBioEntity.utils.ts +++ b/src/redux/overlayBioEntity/overlayBioEntity.utils.ts @@ -2,6 +2,8 @@ import { ONE } from '@/constants/common'; import { overlayBioEntitySchema } from '@/models/overlayBioEntitySchema'; import { OverlayBioEntityRender } from '@/types/OLrendering'; import { OverlayBioEntity } from '@/types/models'; +import { getOverlayReactionCoordsFromLine } from '@/utils/overlays/getOverlayReactionCoords'; +import { isBioEntity, isReaction } from '@/utils/overlays/overlaysElementsTypeGuards'; import { z } from 'zod'; export const parseOverlayBioEntityToOlRenderingFormat = ( @@ -9,8 +11,16 @@ export const parseOverlayBioEntityToOlRenderingFormat = ( overlayId: number, ): OverlayBioEntityRender[] => data.reduce((acc: OverlayBioEntityRender[], entity: OverlayBioEntity) => { - if (entity.left.x && entity.left.y) { + /** + * The're two types of entities - bioentity and reaction + * Bioentity comes with the single only element + * And reaction comes with many different lines that needs to be merged together + * Every reaction line is a different entity after reduce + */ + + if (isBioEntity(entity)) { acc.push({ + type: 'rectangle', id: entity.left.id, modelId: entity.left.model, x1: entity.left.x, @@ -24,6 +34,31 @@ export const parseOverlayBioEntityToOlRenderingFormat = ( color: entity.right.color, }); } + + if (isReaction(entity)) { + const { products, reactants, modifiers } = entity.left; + const lines = [products, reactants, modifiers].flat().map(element => element.line); + const coords = lines.map(getOverlayReactionCoordsFromLine).flat(); + const elements = coords.map( + ({ x1, x2, y1, y2, id, width, height }): OverlayBioEntityRender => ({ + type: 'line', + id, + modelId: entity.left.model, + x1, + x2, + y1, + y2, + width, + height: Math.abs(height), + value: entity.right.value, + overlayId, + color: entity.right.color, + }), + ); + + acc.push(...elements); + } + return acc; }, []); diff --git a/src/redux/plugins/plugins.constants.ts b/src/redux/plugins/plugins.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..0aa77a3a906e4ecbfaf61cd980008a2a1ed7a397 --- /dev/null +++ b/src/redux/plugins/plugins.constants.ts @@ -0,0 +1,9 @@ +import { PluginsState } from './plugins.types'; + +export const PLUGINS_INITIAL_STATE: PluginsState = { + list: { + data: [], + loading: 'idle', + error: { name: '', message: '' }, + }, +}; diff --git a/src/redux/plugins/plugins.mock.ts b/src/redux/plugins/plugins.mock.ts index 63cae1392973ceefc6234e54248670450dbb1137..6cbfcf5594d0f65536392a3db11e3135224fdf25 100644 --- a/src/redux/plugins/plugins.mock.ts +++ b/src/redux/plugins/plugins.mock.ts @@ -1,6 +1,18 @@ -import { PluginsState } from './plugins.types'; +import { DEFAULT_ERROR } from '@/constants/errors'; +import { ActivePlugins, PluginsList, PluginsState } from './plugins.types'; -export const PLUGINS_INITIAL_STATE_MOCK: PluginsState = { +export const PLUGINS_INITIAL_STATE__ACTIVE_PLUGINS_MOCK: ActivePlugins = { data: {}, pluginsId: [], }; + +export const PLUGINS_INITIAL_STATE_LIST_MOCK: PluginsList = { + data: [], + loading: 'idle', + error: DEFAULT_ERROR, +}; + +export const PLUGINS_INITIAL_STATE_MOCK: PluginsState = { + list: PLUGINS_INITIAL_STATE_LIST_MOCK, + activePlugins: PLUGINS_INITIAL_STATE__ACTIVE_PLUGINS_MOCK, +}; diff --git a/src/redux/plugins/plugins.reducers.ts b/src/redux/plugins/plugins.reducers.ts index af01fcfcd7d2ba2ea63be763e828ea1cea6cc4ba..31c0b6f96c04f683f32bbb6f90400c7e40c03b25 100644 --- a/src/redux/plugins/plugins.reducers.ts +++ b/src/redux/plugins/plugins.reducers.ts @@ -1,26 +1,40 @@ import type { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import type { PluginsState, RemovePluginAction } from './plugins.types'; -import { registerPlugin } from './plugins.thunk'; +import { registerPlugin, getAllPlugins } from './plugins.thunk'; export const removePluginReducer = (state: PluginsState, action: RemovePluginAction): void => { const { pluginId } = action.payload; - state.pluginsId = state.pluginsId.filter(id => id !== pluginId); - delete state.data[pluginId]; + state.activePlugins.pluginsId = state.activePlugins.pluginsId.filter(id => id !== pluginId); + delete state.activePlugins.data[pluginId]; }; export const registerPluginReducer = (builder: ActionReducerMapBuilder<PluginsState>): void => { builder.addCase(registerPlugin.pending, (state, action) => { const { hash } = action.meta.arg; - state.pluginsId.push(hash); + state.activePlugins.pluginsId.push(hash); }); builder.addCase(registerPlugin.fulfilled, (state, action) => { if (action.payload) { const { hash } = action.meta.arg; - state.data[hash] = action.payload; + state.activePlugins.data[hash] = action.payload; } }); builder.addCase(registerPlugin.rejected, state => { - state.pluginsId = []; + state.activePlugins.pluginsId = []; + }); +}; + +export const getAllPluginsReducer = (builder: ActionReducerMapBuilder<PluginsState>): void => { + builder.addCase(getAllPlugins.pending, state => { + state.list.loading = 'pending'; + }); + builder.addCase(getAllPlugins.fulfilled, (state, action) => { + state.list.data = action.payload || []; + state.list.loading = 'succeeded'; + }); + builder.addCase(getAllPlugins.rejected, state => { + state.list.loading = 'failed'; + // TODO to discuss manage state of failure }); }; diff --git a/src/redux/plugins/plugins.selectors.ts b/src/redux/plugins/plugins.selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..5ece3dd2e529230dd8f85c413703675ebd7de90d --- /dev/null +++ b/src/redux/plugins/plugins.selectors.ts @@ -0,0 +1,19 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { rootSelector } from '../root/root.selectors'; + +export const pluginsSelector = createSelector(rootSelector, state => state.plugins); + +export const pluginsListSelector = createSelector(pluginsSelector, plugins => { + return plugins.list; +}); + +export const pluginsListDataSelector = createSelector(pluginsListSelector, pluginsList => { + return pluginsList.data; +}); + +export const publicPluginsListSelector = createSelector( + pluginsListDataSelector, + pluginsListData => { + return (pluginsListData || []).filter(plugin => plugin.isPublic); + }, +); diff --git a/src/redux/plugins/plugins.slice.ts b/src/redux/plugins/plugins.slice.ts index be87d49638c587e2fafd19bc709daad157c72476..aeb408420001e991c41e1a060c280880450af4af 100644 --- a/src/redux/plugins/plugins.slice.ts +++ b/src/redux/plugins/plugins.slice.ts @@ -1,23 +1,23 @@ import { createSlice } from '@reduxjs/toolkit'; -import type { PluginsState } from './plugins.types'; -import { registerPluginReducer, removePluginReducer } from './plugins.reducers'; +import { + registerPluginReducer, + removePluginReducer, + getAllPluginsReducer, +} from './plugins.reducers'; -const initialState: PluginsState = { - pluginsId: [], - data: {}, -}; +import { PLUGINS_INITIAL_STATE } from './plugins.constants'; -export const pluginsSlice = createSlice({ +const pluginsSlice = createSlice({ name: 'plugins', - initialState, + initialState: PLUGINS_INITIAL_STATE, reducers: { removePlugin: removePluginReducer, }, extraReducers: builder => { registerPluginReducer(builder); + getAllPluginsReducer(builder); }, }); export const { removePlugin } = pluginsSlice.actions; - export default pluginsSlice.reducer; diff --git a/src/redux/plugins/plugins.thunks.ts b/src/redux/plugins/plugins.thunks.ts new file mode 100644 index 0000000000000000000000000000000000000000..732cfdcb4fcadb1b83a804bb51c8f581d2269f50 --- /dev/null +++ b/src/redux/plugins/plugins.thunks.ts @@ -0,0 +1,18 @@ +import { pluginSchema } from '@/models/pluginSchema'; +import { axiosInstance } from '@/services/api/utils/axiosInstance'; +import { MinervaPlugin } from '@/types/models'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { z } from 'zod'; +import { apiPath } from '../apiPath'; + +export const getAllPlugins = createAsyncThunk( + 'plugins/getAllPlugins', + async (): Promise<MinervaPlugin[]> => { + const response = await axiosInstance.get<MinervaPlugin[]>(apiPath.getAllPlugins()); + + const isDataValid = validateDataUsingZodSchema(response.data, z.array(pluginSchema)); + + return isDataValid ? response.data : []; + }, +); diff --git a/src/redux/plugins/plugins.types.ts b/src/redux/plugins/plugins.types.ts index 903fe01c45c8d564cf1ebaf3c286c8b159db3e9b..2569f12845cc3394a7b8897e9133745a52cb9fa6 100644 --- a/src/redux/plugins/plugins.types.ts +++ b/src/redux/plugins/plugins.types.ts @@ -1,12 +1,20 @@ import { PayloadAction } from '@reduxjs/toolkit'; -import { Plugin } from '@/types/models'; -export type PluginsState = { +import { FetchDataState } from '@/types/fetchDataState'; +import { MinervaPlugin } from '@/types/models'; + +export type RemovePluginPayload = { pluginId: string }; +export type RemovePluginAction = PayloadAction<RemovePluginPayload>; + +export type PluginsList = FetchDataState<MinervaPlugin[]>; +export type ActivePlugins = { pluginsId: string[]; data: { - [pluginId: string]: Plugin | undefined; + [pluginId: string]: MinervaPlugin; }; }; -export type RemovePluginPayload = { pluginId: string }; -export type RemovePluginAction = PayloadAction<RemovePluginPayload>; +export type PluginsState = { + list: PluginsList; + activePlugins: ActivePlugins; +}; diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts index a04a5a92496b0b54075b5a852e147636d7eea1cc..e193bf73c656e1ad8dbb25097dc217a6e4ea3060 100644 --- a/src/redux/root/init.thunks.ts +++ b/src/redux/root/init.thunks.ts @@ -1,27 +1,28 @@ -import { openSearchDrawerWithSelectedTab } from '@/redux/drawer/drawer.slice'; -import { createAsyncThunk } from '@reduxjs/toolkit'; import { PROJECT_ID } from '@/constants'; +import { openSearchDrawerWithSelectedTab } from '@/redux/drawer/drawer.slice'; import { AppDispatch } from '@/redux/store'; import { QueryData } from '@/types/query'; import { getDefaultSearchTab } from '@/components/FunctionalArea/TopBar/SearchBar/SearchBar.utils'; import { PluginsManager } from '@/services/pluginsManager'; +import { createAsyncThunk } from '@reduxjs/toolkit'; import { getAllBackgroundsByProjectId } from '../backgrounds/backgrounds.thunks'; -import { getAllPublicOverlaysByProjectId } from '../overlays/overlays.thunks'; -import { getModels } from '../models/models.thunks'; -import { getProjectById } from '../project/project.thunks'; +import { getConfiguration, getConfigurationOptions } from '../configuration/configuration.thunks'; import { initMapBackground, initMapPosition, initMapSizeAndModelId, initOpenedMaps, } from '../map/map.thunks'; -import { getSearchData } from '../search/search.thunks'; -import { setPerfectMatch } from '../search/search.slice'; -import { getSessionValid } from '../user/user.thunks'; +import { getModels } from '../models/models.thunks'; import { getInitOverlays } from '../overlayBioEntity/overlayBioEntity.thunk'; -import { getConfiguration, getConfigurationOptions } from '../configuration/configuration.thunks'; +import { getAllPublicOverlaysByProjectId } from '../overlays/overlays.thunks'; +import { getAllPlugins } from '../plugins/plugins.thunks'; +import { getProjectById } from '../project/project.thunks'; +import { setPerfectMatch } from '../search/search.slice'; +import { getSearchData } from '../search/search.thunks'; import { getStatisticsById } from '../statistics/statistics.thunks'; import { getInitPlugins } from '../plugins/plugins.thunk'; +import { getSessionValid } from '../user/user.thunks'; interface InitializeAppParams { queryData: QueryData; @@ -57,6 +58,9 @@ export const fetchInitialAppData = createAsyncThunk< dispatch(getStatisticsById(PROJECT_ID)); dispatch(getConfiguration()); + // Fetch plugins list + dispatch(getAllPlugins()); + /** Trigger search */ if (queryData.searchValue) { dispatch(setPerfectMatch(queryData.perfectMatch)); diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts index bfb6570be66aa04a3c47be5f9c9a26a46451b25f..8bfe3f1bcafc071a633a9da208708244fb101015 100644 --- a/src/redux/root/root.fixtures.ts +++ b/src/redux/root/root.fixtures.ts @@ -1,26 +1,26 @@ 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 { COMPARTMENT_PATHWAYS_INITIAL_STATE_MOCK } from '../compartmentPathways/compartmentPathways.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 { initialStateFixture as drawerInitialStateMock } from '../drawer/drawerFixture'; import { DRUGS_INITIAL_STATE_MOCK } from '../drugs/drugs.mock'; +import { EXPORT_INITIAL_STATE_MOCK } from '../export/export.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'; import { OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK } from '../overlayBioEntity/overlayBioEntity.mock'; import { OVERLAYS_INITIAL_STATE_MOCK } from '../overlays/overlays.mock'; +import { PLUGINS_INITIAL_STATE_MOCK } from '../plugins/plugins.mock'; import { PROJECT_STATE_INITIAL_MOCK } from '../project/project.mock'; import { REACTIONS_STATE_INITIAL_MOCK } from '../reactions/reactions.mock'; import { SEARCH_STATE_INITIAL_MOCK } from '../search/search.mock'; +import { STATISTICS_STATE_INITIAL_MOCK } from '../statistics/statistics.mock'; import { RootState } from '../store'; import { USER_INITIAL_STATE_MOCK } from '../user/user.mock'; -import { STATISTICS_STATE_INITIAL_MOCK } from '../statistics/statistics.mock'; -import { COMPARTMENT_PATHWAYS_INITIAL_STATE_MOCK } from '../compartmentPathways/compartmentPathways.mock'; -import { EXPORT_INITIAL_STATE_MOCK } from '../export/export.mock'; -import { PLUGINS_INITIAL_STATE_MOCK } from '../plugins/plugins.mock'; export const INITIAL_STORE_STATE_MOCK: RootState = { search: SEARCH_STATE_INITIAL_MOCK, diff --git a/src/redux/store.ts b/src/redux/store.ts index a515461172f3468e39668a4b5606a4df24528938..4e7447eba76d7e6d33cb5f70ffc8fc9b096ade8a 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -22,12 +22,12 @@ import { TypedStartListening, configureStore, } from '@reduxjs/toolkit'; -import legendReducer from './legend/legend.slice'; -import { mapListenerMiddleware } from './map/middleware/map.middleware'; -import statisticsReducer from './statistics/statistics.slice'; import compartmentPathwaysReducer from './compartmentPathways/compartmentPathways.slice'; import exportReducer from './export/export.slice'; +import legendReducer from './legend/legend.slice'; +import { mapListenerMiddleware } from './map/middleware/map.middleware'; import pluginsReducer from './plugins/plugins.slice'; +import statisticsReducer from './statistics/statistics.slice'; export const reducers = { search: searchReducer, diff --git a/src/types/OLrendering.ts b/src/types/OLrendering.ts index 11ab030c2ca0dd67874f94da6b29309e81f52e1c..6aee24d703ddc7ab5c39bf21644ad91895886891 100644 --- a/src/types/OLrendering.ts +++ b/src/types/OLrendering.ts @@ -1,5 +1,7 @@ import { Color } from './models'; +export type OverlayBioEntityRenderType = 'line' | 'rectangle'; + export type OverlayBioEntityRender = { id: number; modelId: number; @@ -16,4 +18,15 @@ export type OverlayBioEntityRender = { value: number | null; overlayId: number; color: Color | null; + type: OverlayBioEntityRenderType; }; + +export interface OverlayReactionCoords { + x1: number; + x2: number; + y1: number; + y2: number; + id: number; + height: number; + width: number; +} diff --git a/src/types/drawerName.ts b/src/types/drawerName.ts index a5f3e3d2ff2f0155c67bdb1a458fc2ce64c5764f..3715c57ddd45995b4c8194a917bf0ad8a129b5f4 100644 --- a/src/types/drawerName.ts +++ b/src/types/drawerName.ts @@ -8,4 +8,5 @@ export type DrawerName = | 'submaps' | 'reaction' | 'overlays' - | 'bio-entity'; + | 'bio-entity' + | 'available-plugins'; diff --git a/src/types/models.ts b/src/types/models.ts index d46b347f7d6e3fa6c0f12b87a9949d3f94d82271..293fd8ffe08e9930c8d2b88385a697b16834106e 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -12,7 +12,8 @@ import { configurationSchema, formatSchema, miriamTypesSchema } from '@/models/c import { disease } from '@/models/disease'; import { drugSchema } from '@/models/drugSchema'; import { elementSearchResult, elementSearchResultType } from '@/models/elementSearchResult'; -import { exportNetworkchema, exportElementsSchema } from '@/models/exportSchema'; +import { exportElementsSchema, exportNetworkchema } from '@/models/exportSchema'; +import { lineSchema } from '@/models/lineSchema'; import { loginSchema } from '@/models/loginSchema'; import { mapBackground } from '@/models/mapBackground'; import { @@ -23,7 +24,13 @@ import { } from '@/models/mapOverlay'; import { mapModelSchema } from '@/models/modelSchema'; import { organism } from '@/models/organism'; -import { overlayBioEntitySchema } from '@/models/overlayBioEntitySchema'; +import { + overlayBioEntitySchema, + overlayElementWithBioEntitySchema, + overlayElementWithReactionSchema, +} from '@/models/overlayBioEntitySchema'; +import { overlayLeftBioEntitySchema } from '@/models/overlayLeftBioEntitySchema'; +import { overlayLeftReactionSchema } from '@/models/overlayLeftReactionSchema'; import { overviewImageLink, overviewImageLinkImage, @@ -68,6 +75,11 @@ export type Configuration = z.infer<typeof configurationSchema>; export type ConfigurationFormatSchema = z.infer<typeof formatSchema>; export type ConfigurationMiramiTypes = z.infer<typeof miriamTypesSchema>; export type OverlayBioEntity = z.infer<typeof overlayBioEntitySchema>; +export type OverlayElementWithReaction = z.infer<typeof overlayElementWithReactionSchema>; +export type OverlayElementWithBioEntity = z.infer<typeof overlayElementWithBioEntitySchema>; +export type OverlayLeftBioEntity = z.infer<typeof overlayLeftBioEntitySchema>; +export type OverlayLeftReaction = z.infer<typeof overlayLeftReactionSchema>; +export type Line = z.infer<typeof lineSchema>; export type CreatedOverlayFile = z.infer<typeof createdOverlayFileSchema>; export type UploadedOverlayFileContent = z.infer<typeof uploadedOverlayFileContentSchema>; export type CreatedOverlay = z.infer<typeof createdOverlaySchema>; @@ -77,4 +89,4 @@ export type CompartmentPathway = z.infer<typeof compartmentPathwaySchema>; export type CompartmentPathwayDetails = z.infer<typeof compartmentPathwayDetailsSchema>; export type ExportNetwork = z.infer<typeof exportNetworkchema>; export type ExportElements = z.infer<typeof exportElementsSchema>; -export type Plugin = z.infer<typeof pluginSchema>; +export type MinervaPlugin = z.infer<typeof pluginSchema>; // Plugin type interfers with global Plugin type diff --git a/src/utils/overlays/getOverlayReactionCoords.test.ts b/src/utils/overlays/getOverlayReactionCoords.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d5d2ea1b023364c6ad7d5e06b0a5f062d5cec1c --- /dev/null +++ b/src/utils/overlays/getOverlayReactionCoords.test.ts @@ -0,0 +1,85 @@ +import { OverlayReactionCoords } from '@/types/OLrendering'; +import { Line } from '@/types/models'; +import { getOverlayReactionCoordsFromLine } from './getOverlayReactionCoords'; + +const LINE_DATA_BASE: Line = { + id: 66141, + width: 1, + color: { + alpha: 255, + rgb: -16777216, + }, + z: 0, + segments: [ + { + x1: 4457.375604345491, + y1: 7111.933125147456, + x2: 4462.61826820353, + y2: 7105.89040426431, + }, + ], + startArrow: { + arrowType: 'NONE', + angle: 2.748893571891069, + lineType: 'SOLID', + length: 15, + }, + endArrow: { + arrowType: 'NONE', + angle: 2.748893571891069, + lineType: 'SOLID', + length: 15, + }, + lineType: 'SOLID', +}; + +describe('getOverlayReactionCoords - util', () => { + const cases: [Line, OverlayReactionCoords[]][] = [ + [ + { + ...LINE_DATA_BASE, + segments: [ + { + x1: 10, + y1: 10, + x2: 100, + y2: 100, + }, + ], + }, + [{ height: -90, id: 66141, width: 90, x1: 10, x2: 100, y1: 10, y2: 100 }], + ], + [ + { + ...LINE_DATA_BASE, + segments: [ + { + x1: 10, + y1: 10, + x2: 2000, + y2: 0, + }, + ], + }, + [{ height: 10, id: 66141, width: 1990, x1: 10, x2: 2000, y1: 10, y2: 0 }], + ], + [ + { + ...LINE_DATA_BASE, + segments: [ + { + x1: 0, + y1: 0, + x2: 0, + y2: 0, + }, + ], + }, + [{ height: 0, id: 66141, width: 0, x1: 0, x2: 0, y1: 0, y2: 0 }], + ], + ]; + + it.each(cases)('should return valid result', (line, result) => { + expect(getOverlayReactionCoordsFromLine(line)).toStrictEqual(result); + }); +}); diff --git a/src/utils/overlays/getOverlayReactionCoords.ts b/src/utils/overlays/getOverlayReactionCoords.ts new file mode 100644 index 0000000000000000000000000000000000000000..c607fcde6e01f08334f0a38122646ec145fe17cc --- /dev/null +++ b/src/utils/overlays/getOverlayReactionCoords.ts @@ -0,0 +1,14 @@ +import { OverlayReactionCoords } from '@/types/OLrendering'; +import { Line } from '@/types/models'; + +export const getOverlayReactionCoordsFromLine = (line: Line): OverlayReactionCoords[] => + line.segments.map(segment => { + const { x1, y1, x2, y2 } = segment; + + return { + ...segment, + id: line.id, + width: x2 - x1, + height: y1 - y2, + }; + }); diff --git a/src/utils/overlays/overlaysElementsTypeGuards.test.ts b/src/utils/overlays/overlaysElementsTypeGuards.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..19028e2c88aa16431ee269d267a531998004d189 --- /dev/null +++ b/src/utils/overlays/overlaysElementsTypeGuards.test.ts @@ -0,0 +1,35 @@ +import { + overlayElementWithBioEntityFixture, + overlayElementWithReactionFixture, +} from '@/models/fixtures/overlayBioEntityFixture'; +import { isBioEntity, isReaction } from './overlaysElementsTypeGuards'; + +describe('overlaysElementsTypeGruards - utils', () => { + describe('isReaction', () => { + describe('when is reaction', () => { + it('should return true', () => { + expect(isReaction(overlayElementWithReactionFixture)).toBe(true); + }); + }); + + describe('when is bioentity', () => { + it('should return false', () => { + expect(isReaction(overlayElementWithBioEntityFixture)).toBe(false); + }); + }); + }); + + describe('isBioEntity', () => { + describe('when is reaction', () => { + it('should return false', () => { + expect(isBioEntity(overlayElementWithReactionFixture)).toBe(false); + }); + }); + + describe('when is bioentity', () => { + it('should return true', () => { + expect(isBioEntity(overlayElementWithBioEntityFixture)).toBe(true); + }); + }); + }); +}); diff --git a/src/utils/overlays/overlaysElementsTypeGuards.ts b/src/utils/overlays/overlaysElementsTypeGuards.ts new file mode 100644 index 0000000000000000000000000000000000000000..6997b141f5d627b5d4641ee23c2f7b902f17a1af --- /dev/null +++ b/src/utils/overlays/overlaysElementsTypeGuards.ts @@ -0,0 +1,14 @@ +import { + OverlayBioEntity, + OverlayElementWithBioEntity, + OverlayElementWithReaction, + OverlayLeftBioEntity, + OverlayLeftReaction, +} from '@/types/models'; + +export const isReaction = (e: OverlayBioEntity): e is OverlayElementWithReaction => + (e.left as OverlayLeftReaction).line !== undefined; + +export const isBioEntity = (e: OverlayBioEntity): e is OverlayElementWithBioEntity => + (e.left as OverlayLeftBioEntity).x !== undefined && + (e.left as OverlayLeftBioEntity).y !== undefined;