diff --git a/src/components/Map/Drawer/Drawer.component.tsx b/src/components/Map/Drawer/Drawer.component.tsx index b18e949fed1a9db8afe490c00ce6f3feb175f098..de0f9d35b499fc150ac224591fe5504ec897ec40 100644 --- a/src/components/Map/Drawer/Drawer.component.tsx +++ b/src/components/Map/Drawer/Drawer.component.tsx @@ -4,6 +4,7 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { twMerge } from 'tailwind-merge'; import { CommentDrawer } from '@/components/Map/Drawer/CommentDrawer'; import { LayersDrawer } from '@/components/Map/Drawer/LayersDrawer/LayersDrawer.component'; +import { OverlayGroupDrawer } from '@/components/Map/Drawer/OverlayGroupDrawer'; import { AvailablePluginsDrawer } from './AvailablePluginsDrawer'; import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component'; import { ExportDrawer } from './ExportDrawer'; @@ -29,6 +30,7 @@ export const Drawer = (): JSX.Element => { {isOpen && drawerName === 'reaction' && <ReactionDrawer />} {isOpen && drawerName === 'overlays' && <OverlaysDrawer />} {isOpen && drawerName === 'bio-entity' && <BioEntityDrawer />} + {isOpen && drawerName === 'overlay-group' && <OverlayGroupDrawer />} {isOpen && drawerName === 'project-info' && <ProjectInfoDrawer />} {isOpen && drawerName === 'export' && <ExportDrawer />} {isOpen && drawerName === 'available-plugins' && <AvailablePluginsDrawer />} diff --git a/src/components/Map/Drawer/OverlayGroupDrawer/OverlaysGroupDrawer.component.tsx b/src/components/Map/Drawer/OverlayGroupDrawer/OverlaysGroupDrawer.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..43188a929c10015b44c317e7a0e4962f8545cc51 --- /dev/null +++ b/src/components/Map/Drawer/OverlayGroupDrawer/OverlaysGroupDrawer.component.tsx @@ -0,0 +1,47 @@ +import { DrawerHeadingBackwardButton } from '@/shared/DrawerHeadingBackwardButton'; +import { Input } from '@/shared/Input'; +import { Button } from '@/shared/Button'; +import { openOverlaysDrawer } from '@/redux/drawer/drawer.slice'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useOverlayGroupForm } from '@/components/Map/Drawer/OverlayGroupDrawer/hooks/useOverlayGroupForm'; + +export const OverlayGroupDrawer = (): JSX.Element => { + const dispatch = useAppDispatch(); + const navigateToOverlays = (): void => { + dispatch(openOverlaysDrawer()); + }; + + const { name, handleChangeName, handleSubmit } = useOverlayGroupForm(); + + return ( + <> + <DrawerHeadingBackwardButton backwardFunction={navigateToOverlays}> + Add overlay group + </DrawerHeadingBackwardButton> + <form className="flex h-[calc(100%-93px)] max-h-[calc(100%-93px)] flex-col overflow-y-auto p-6"> + <label className="mb-2.5 text-sm" htmlFor="name"> + Name + <Input + type="text" + name="name" + id="name" + data-testid="overlay-name" + value={name} + onChange={handleChangeName} + placeholder="Fancy group name" + sizeVariant="medium" + className="mt-2.5 text-xs" + /> + </label> + + <Button + className="mt-2.5 items-center justify-center self-start" + onClick={handleSubmit} + aria-label="add overlay" + > + Add + </Button> + </form> + </> + ); +}; diff --git a/src/components/Map/Drawer/OverlayGroupDrawer/hooks/useOverlayGroupForm.ts b/src/components/Map/Drawer/OverlayGroupDrawer/hooks/useOverlayGroupForm.ts new file mode 100644 index 0000000000000000000000000000000000000000..28b867242594f429e2be018713bae2974c5f216b --- /dev/null +++ b/src/components/Map/Drawer/OverlayGroupDrawer/hooks/useOverlayGroupForm.ts @@ -0,0 +1,40 @@ +import { useState, ChangeEvent } from 'react'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { overlayGroupsSelector } from '@/redux/overlayGroup/overlayGroup.selectors'; +import { addOverlayGroup } from '@/redux/overlayGroup/overlayGroup.thunks'; + +type ReturnType = { + name: string; + handleChangeName: (e: ChangeEvent<HTMLInputElement>) => void; + handleSubmit: () => Promise<void>; +}; + +export const useOverlayGroupForm = (): ReturnType => { + const dispatch = useAppDispatch(); + const overlayGroups = useAppSelector(overlayGroupsSelector); + + const [name, setName] = useState(''); + + const handleChangeName = (e: ChangeEvent<HTMLInputElement>): void => { + setName(e.target.value); + }; + const handleSubmit = async (): Promise<void> => { + if (!name) return; + + dispatch( + addOverlayGroup({ + name, + order: overlayGroups.length, + }), + ); + + setName(''); + }; + + return { + name, + handleChangeName, + handleSubmit, + }; +}; diff --git a/src/components/Map/Drawer/OverlayGroupDrawer/index.ts b/src/components/Map/Drawer/OverlayGroupDrawer/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5859600f0413178df44b9092e312b1c01913d60f --- /dev/null +++ b/src/components/Map/Drawer/OverlayGroupDrawer/index.ts @@ -0,0 +1 @@ +export { OverlayGroupDrawer } from './OverlaysGroupDrawer.component'; diff --git a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx index 60d7cb013554265a54d899801694c7758f47c1e7..99b5704639c122dc610a5929a1c1096a1644bb7d 100644 --- a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx +++ b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx @@ -1,4 +1,7 @@ -import { displayAddOverlaysDrawer } from '@/redux/drawer/drawer.slice'; +import { + displayAddOverlayGroupDrawer, + displayAddOverlaysDrawer, +} from '@/redux/drawer/drawer.slice'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { authenticatedUserSelector, loadingUserSelector } from '@/redux/user/user.selectors'; @@ -17,6 +20,9 @@ export const UserOverlays = (): JSX.Element => { const handleAddOverlay = (): void => { dispatch(displayAddOverlaysDrawer()); }; + const handleAddOverlayGroup = (): void => { + dispatch(displayAddOverlayGroupDrawer()); + }; return ( <div className="py-6"> @@ -35,6 +41,9 @@ export const UserOverlays = (): JSX.Element => { <> <div className="flex items-center justify-between px-6"> <p className="font-semibold">User provided overlays:</p> + <Button onClick={handleAddOverlayGroup} aria-label="add overlay group button"> + Add group + </Button> <Button onClick={handleAddOverlay} aria-label="add overlay button"> Add overlay </Button> diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 4457eee6a6b3cc944c5637eccada6bbe0937d67d..d14f7fc5d1847e1188a31119f0fa4c5c11855550 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -135,4 +135,5 @@ export const apiPath = { getDrugAutocomplete: (): string => `projects/${PROJECT_ID}/drugs/suggestedQueryList`, getChemicalAutocomplete: (): string => `projects/${PROJECT_ID}/chemicals/suggestedQueryList`, getOverlayGroups: (): string => `projects/${PROJECT_ID}/overlay_groups/`, + addOverlayGroup: (): string => `projects/${PROJECT_ID}/overlay_groups/`, }; diff --git a/src/redux/drawer/drawer.reducers.ts b/src/redux/drawer/drawer.reducers.ts index 88f1096c6e38f71dcc0442b506fa9a832e6e9434..156866b2cfc3c75af9c2bea6a0980bce988dce42 100644 --- a/src/redux/drawer/drawer.reducers.ts +++ b/src/redux/drawer/drawer.reducers.ts @@ -45,6 +45,12 @@ export const displayAddOverlaysDrawerReducer = (state: DrawerState): void => { state.overlayDrawerState.currentStep = STEP.SECOND; }; +export const displayAddOverlayGroupDrawerReducer = (state: DrawerState): void => { + state.isOpen = true; + state.drawerName = 'overlay-group'; + state.overlayDrawerState.currentStep = STEP.FIRST; +}; + export const selectTabReducer = ( state: DrawerState, action: OpenSearchDrawerWithSelectedTabReducerAction, diff --git a/src/redux/drawer/drawer.slice.ts b/src/redux/drawer/drawer.slice.ts index ddb7c23e04cf509ffc3cf0ee470a175512ad0036..fe752832184d68b7bf87c7ecca2e2071624a1926 100644 --- a/src/redux/drawer/drawer.slice.ts +++ b/src/redux/drawer/drawer.slice.ts @@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { DRAWER_INITIAL_STATE } from './drawer.constants'; import { closeDrawerReducer, + displayAddOverlayGroupDrawerReducer, displayAddOverlaysDrawerReducer, displayBioEntitiesListReducer, displayChemicalsListReducer, @@ -29,6 +30,7 @@ const drawerSlice = createSlice({ openSubmapsDrawer: openSubmapsDrawerReducer, openOverlaysDrawer: openOverlaysDrawerReducer, displayAddOverlaysDrawer: displayAddOverlaysDrawerReducer, + displayAddOverlayGroupDrawer: displayAddOverlayGroupDrawerReducer, selectTab: selectTabReducer, closeDrawer: closeDrawerReducer, displayDrugsList: displayDrugsListReducer, @@ -52,6 +54,7 @@ export const { openSubmapsDrawer, openOverlaysDrawer, displayAddOverlaysDrawer, + displayAddOverlayGroupDrawer, selectTab, closeDrawer, displayDrugsList, diff --git a/src/redux/overlayGroup/overlayGroup.selectors.ts b/src/redux/overlayGroup/overlayGroup.selectors.ts index bd26e08fbc60087b2f41465085fb7179b5747df4..8c2831ee30219bc802a9ce351a4337a91e992a18 100644 --- a/src/redux/overlayGroup/overlayGroup.selectors.ts +++ b/src/redux/overlayGroup/overlayGroup.selectors.ts @@ -10,5 +10,7 @@ export const overlayGroupsSelector = createSelector(overlayGroupSelector, overla if (overlayGroup?.data) { result = result.concat(overlayGroup?.data); } + // eslint-disable-next-line no-console + console.log(result); return result; }); diff --git a/src/redux/overlayGroup/overlayGroup.thunks.ts b/src/redux/overlayGroup/overlayGroup.thunks.ts index aed7d0a0fc3294d7ab7d6d4b8252d0996cde2a1a..711bb8ef3b8b60fdc111dcca8e311691b2664adf 100644 --- a/src/redux/overlayGroup/overlayGroup.thunks.ts +++ b/src/redux/overlayGroup/overlayGroup.thunks.ts @@ -1,11 +1,13 @@ import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; -import { OverlayGroup, PageOf } from '@/types/models'; +import { MapOverlay, OverlayGroup, PageOf } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { ThunkConfig } from '@/types/store'; import { getError } from '@/utils/error-report/getError'; import { pageableSchema } from '@/models/pageableSchema'; import { overlayGroupSchema } from '@/models/overlayGroupSchema'; +import { showToast } from '@/utils/showToast'; +import axios from 'axios'; import { apiPath } from '../apiPath'; export const getOverlayGroups = createAsyncThunk<OverlayGroup[], void, ThunkConfig>( @@ -14,6 +16,9 @@ export const getOverlayGroups = createAsyncThunk<OverlayGroup[], void, ThunkConf try { const response = await axiosInstanceNewAPI.get<PageOf<OverlayGroup>>( apiPath.getOverlayGroups(), + { + withCredentials: true, + }, ); const isDataValid = validateDataUsingZodSchema( @@ -27,3 +32,49 @@ export const getOverlayGroups = createAsyncThunk<OverlayGroup[], void, ThunkConf } }, ); + +type AddOverlayGroupArgs = { + name: string; + order: number; +}; + +export const addOverlayGroup = createAsyncThunk<undefined, AddOverlayGroupArgs, ThunkConfig>( + 'overlays/addOverlayGroup', + async ( + { name, order }, + { dispatch }, + // eslint-disable-next-line consistent-return + ) => { + try { + const response = await axiosInstanceNewAPI.post<MapOverlay>( + apiPath.addOverlayGroup(), + { + name, + order, + }, + { + withCredentials: true, + }, + ); + + const isDataValid = validateDataUsingZodSchema(response.data, overlayGroupSchema); + if (!isDataValid) { + showToast({ + type: 'error', + message: 'Problem with adding group encountered', + duration: 120000, + }); + } else { + showToast({ type: 'success', message: 'Overlay group added successfully' }); + } + await dispatch(getOverlayGroups()); + } catch (error) { + if (axios.isAxiosError(error) && error.code === 'ERR_BAD_REQUEST') { + const data = error.response?.data; + showToast({ type: 'error', message: data.reason, duration: 120000 }); + } else { + return Promise.reject(getError({ error, prefix: 'Failed to add overlay group' })); + } + } + }, +); diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts index 4514bd872f4dd44518cf8d19daf601db84044b98..bc082c8ec9edc5cb2a87d3b25987e2562b35f2d3 100644 --- a/src/redux/root/init.thunks.ts +++ b/src/redux/root/init.thunks.ts @@ -23,6 +23,7 @@ import { } from '@/components/FunctionalArea/CookieBanner/CookieBanner.constants'; import { injectMatomoTracking } from '@/utils/injectMatomoTracking'; import { getGlyphs } from '@/redux/glyphs/glyphs.thunks'; +import { getOverlayGroups } from '@/redux/overlayGroup/overlayGroup.thunks'; import { getConfiguration, getConfigurationOptions } from '../configuration/configuration.thunks'; import { initMapBackground, @@ -145,6 +146,7 @@ export const fetchInitialAppData = createAsyncThunk< } await dispatch(getAllUserOverlaysByCreator()); + await dispatch(getOverlayGroups()); /** fetch overlays */ if (queryData.overlaysId) { dispatch(getInitOverlays({ overlaysId: queryData.overlaysId })); diff --git a/src/types/drawerName.ts b/src/types/drawerName.ts index 4a7f5114bd6949063557d0098d406b1b0a07bb2a..36e1055b0df5ff073f9d4d48052a99aa6769f878 100644 --- a/src/types/drawerName.ts +++ b/src/types/drawerName.ts @@ -8,6 +8,7 @@ export type DrawerName = | 'submaps' | 'reaction' | 'overlays' + | 'overlay-group' | 'bio-entity' | 'comment' | 'available-plugins'