From 1e9b0179e92c7c4f07d7aef17a46c5d9f2cd138e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com> Date: Mon, 29 Jan 2024 15:26:13 +0100 Subject: [PATCH] feat: add drawer available plugins --- .../NavBar/NavBar.component.tsx | 2 +- .../AvailablePluginsDrawer.component.test.tsx | 53 +++++++++++++++++++ .../AvailablePluginsDrawer.component.tsx | 21 ++++++++ .../LoadPlugin/LoadPlugin.component.test.tsx | 29 ++++++++++ .../LoadPlugin/LoadPlugin.component.tsx | 25 +++++++++ .../LoadPlugin/index.ts | 1 + .../LoadPluginFromUrl.component.test.tsx | 32 +++++++++++ .../LoadPluginFromUrl.component.tsx | 32 +++++++++++ .../LoadPluginFromUrl/index.ts | 1 + .../Drawer/AvailablePluginsDrawer/index.ts | 1 + .../Map/Drawer/Drawer.component.tsx | 10 ++-- src/models/mocks/pluginsMock.ts | 44 +++++++++++++++ src/models/pluginSchema.ts | 10 ++++ src/redux/apiPath.ts | 3 +- src/redux/plugins/plugins.constants.ts | 9 ++++ src/redux/plugins/plugins.mock.ts | 12 +++++ src/redux/plugins/plugins.reducers.ts | 17 ++++++ src/redux/plugins/plugins.selectors.ts | 19 +++++++ src/redux/plugins/plugins.slice.ts | 14 +++++ src/redux/plugins/plugins.thunks.ts | 18 +++++++ src/redux/plugins/plugins.types.ts | 8 +++ src/redux/root/init.thunks.ts | 24 +++++---- src/redux/root/root.fixtures.ts | 8 +-- src/redux/store.ts | 6 ++- src/types/drawerName.ts | 3 +- src/types/models.ts | 2 + 26 files changed, 382 insertions(+), 22 deletions(-) create mode 100644 src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.test.tsx create mode 100644 src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx create mode 100644 src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx create mode 100644 src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx create mode 100644 src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/index.ts create mode 100644 src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx create mode 100644 src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx create mode 100644 src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/index.ts create mode 100644 src/components/Map/Drawer/AvailablePluginsDrawer/index.ts create mode 100644 src/models/mocks/pluginsMock.ts create mode 100644 src/models/pluginSchema.ts create mode 100644 src/redux/plugins/plugins.constants.ts create mode 100644 src/redux/plugins/plugins.mock.ts create mode 100644 src/redux/plugins/plugins.reducers.ts create mode 100644 src/redux/plugins/plugins.selectors.ts create mode 100644 src/redux/plugins/plugins.slice.ts create mode 100644 src/redux/plugins/plugins.thunks.ts create mode 100644 src/redux/plugins/plugins.types.ts diff --git a/src/components/FunctionalArea/NavBar/NavBar.component.tsx b/src/components/FunctionalArea/NavBar/NavBar.component.tsx index fbc5c000..830131c1 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 00000000..06d70d35 --- /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 00000000..d1343d91 --- /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 00000000..92c659ac --- /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 00000000..fee403ca --- /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 00000000..e27e6ba3 --- /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 00000000..e1d83a8b --- /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 00000000..de486ad2 --- /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 00000000..cedb0f66 --- /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 00000000..85f2fe7f --- /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 098c4f98..de1aa94e 100644 --- a/src/components/Map/Drawer/Drawer.component.tsx +++ b/src/components/Map/Drawer/Drawer.component.tsx @@ -2,13 +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 { ReactionDrawer } from './ReactionDrawer'; +import { SearchDrawerWrapper as SearchDrawerContent } from './SearchDrawerWrapper'; +import { SubmapsDrawer } from './SubmapsDrawer'; export const Drawer = (): JSX.Element => { const { isOpen, drawerName } = useAppSelector(drawerSelector); @@ -28,6 +29,7 @@ export const Drawer = (): JSX.Element => { {isOpen && drawerName === 'bio-entity' && <BioEntityDrawer />} {isOpen && drawerName === 'project-info' && <ProjectInfoDrawer />} {isOpen && drawerName === 'export' && <ExportDrawer />} + {isOpen && drawerName === 'available-plugins' && <AvailablePluginsDrawer />} </div> ); }; diff --git a/src/models/mocks/pluginsMock.ts b/src/models/mocks/pluginsMock.ts new file mode 100644 index 00000000..f5c91eaf --- /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/pluginSchema.ts b/src/models/pluginSchema.ts new file mode 100644 index 00000000..0204b6f0 --- /dev/null +++ b/src/models/pluginSchema.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const pluginSchema = z.object({ + hash: z.string(), + name: z.string(), + version: z.string(), + isPublic: z.boolean(), + isDefault: z.boolean(), + urls: z.array(z.string()), +}); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 212660e5..0c592bc7 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: ({ @@ -63,4 +63,5 @@ export const apiPath = { getSourceFile: (): string => `/projects/${PROJECT_ID}:downloadSource`, getMesh: (meshId: string): string => `mesh/${meshId}`, getTaxonomy: (taxonomyId: string): string => `taxonomy/${taxonomyId}`, + getAllPlugins: (): string => `/plugins/`, }; diff --git a/src/redux/plugins/plugins.constants.ts b/src/redux/plugins/plugins.constants.ts new file mode 100644 index 00000000..0aa77a3a --- /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 new file mode 100644 index 00000000..2322bed6 --- /dev/null +++ b/src/redux/plugins/plugins.mock.ts @@ -0,0 +1,12 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { PluginsList, PluginsState } from './plugins.types'; + +export const PLUGINS_INITIAL_STATE_LIST_MOCK: PluginsList = { + data: undefined, + loading: 'idle', + error: DEFAULT_ERROR, +}; + +export const PLUGINS_INITIAL_STATE_MOCK: PluginsState = { + list: PLUGINS_INITIAL_STATE_LIST_MOCK, +}; diff --git a/src/redux/plugins/plugins.reducers.ts b/src/redux/plugins/plugins.reducers.ts new file mode 100644 index 00000000..dd5f9fc6 --- /dev/null +++ b/src/redux/plugins/plugins.reducers.ts @@ -0,0 +1,17 @@ +import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; +import { getAllPlugins } from './plugins.thunks'; +import { PluginsState } from './plugins.types'; + +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 00000000..5ece3dd2 --- /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 new file mode 100644 index 00000000..8ac4e011 --- /dev/null +++ b/src/redux/plugins/plugins.slice.ts @@ -0,0 +1,14 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { PLUGINS_INITIAL_STATE } from './plugins.constants'; +import { getAllPluginsReducer } from './plugins.reducers'; + +const pluginsState = createSlice({ + name: 'plugins', + initialState: PLUGINS_INITIAL_STATE, + reducers: {}, + extraReducers: builder => { + getAllPluginsReducer(builder); + }, +}); + +export default pluginsState.reducer; diff --git a/src/redux/plugins/plugins.thunks.ts b/src/redux/plugins/plugins.thunks.ts new file mode 100644 index 00000000..732cfdcb --- /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 new file mode 100644 index 00000000..c7d7ae4c --- /dev/null +++ b/src/redux/plugins/plugins.types.ts @@ -0,0 +1,8 @@ +import { FetchDataState } from '@/types/fetchDataState'; +import { MinervaPlugin } from '@/types/models'; + +export type PluginsList = FetchDataState<MinervaPlugin[]>; + +export type PluginsState = { + list: PluginsList; +}; diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts index c4ac1927..f6e14e6a 100644 --- a/src/redux/root/init.thunks.ts +++ b/src/redux/root/init.thunks.ts @@ -1,25 +1,26 @@ -import { openSearchDrawerWithSelectedTab } from '@/redux/drawer/drawer.slice'; -import { createAsyncThunk } from '@reduxjs/toolkit'; +import { getDefaultSearchTab } from '@/components/FunctionalArea/TopBar/SearchBar/SearchBar.utils'; 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 { 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 { getSessionValid } from '../user/user.thunks'; interface InitializeAppParams { queryData: QueryData; @@ -54,6 +55,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 c236df28..8bfe3f1b 100644 --- a/src/redux/root/root.fixtures.ts +++ b/src/redux/root/root.fixtures.ts @@ -1,25 +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'; export const INITIAL_STORE_STATE_MOCK: RootState = { search: SEARCH_STATE_INITIAL_MOCK, @@ -43,4 +44,5 @@ export const INITIAL_STORE_STATE_MOCK: RootState = { statistics: STATISTICS_STATE_INITIAL_MOCK, compartmentPathways: COMPARTMENT_PATHWAYS_INITIAL_STATE_MOCK, export: EXPORT_INITIAL_STATE_MOCK, + plugins: PLUGINS_INITIAL_STATE_MOCK, }; diff --git a/src/redux/store.ts b/src/redux/store.ts index a945a751..4e7447eb 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -22,11 +22,12 @@ import { TypedStartListening, configureStore, } from '@reduxjs/toolkit'; +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'; -import compartmentPathwaysReducer from './compartmentPathways/compartmentPathways.slice'; -import exportReducer from './export/export.slice'; export const reducers = { search: searchReducer, @@ -50,6 +51,7 @@ export const reducers = { statistics: statisticsReducer, compartmentPathways: compartmentPathwaysReducer, export: exportReducer, + plugins: pluginsReducer, }; export const middlewares = [mapListenerMiddleware.middleware]; diff --git a/src/types/drawerName.ts b/src/types/drawerName.ts index a5f3e3d2..3715c57d 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 437fa8d6..293fd8ff 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -37,6 +37,7 @@ import { overviewImageLinkModel, } from '@/models/overviewImageLink'; import { overviewImageView } from '@/models/overviewImageView'; +import { pluginSchema } from '@/models/pluginSchema'; import { projectSchema } from '@/models/projectSchema'; import { reactionSchema } from '@/models/reaction'; import { reactionLineSchema } from '@/models/reactionLineSchema'; @@ -88,3 +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 MinervaPlugin = z.infer<typeof pluginSchema>; // Plugin type interfers with global Plugin type -- GitLab