diff --git a/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx b/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx index a6811e654b1c3cb140eced363877b627cdb4ea6d..59bb7f07e6d101242a35b68793a472901c532eb1 100644 --- a/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx +++ b/src/components/FunctionalArea/ContextMenu/ContextMenu.component.tsx @@ -3,7 +3,7 @@ import { contextMenuSelector } from '@/redux/contextMenu/contextMenu.selector'; import { closeContextMenu } from '@/redux/contextMenu/contextMenu.slice'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { openMolArtModalById } from '@/redux/modal/modal.slice'; +import { openAddCommentModal, openMolArtModalById } from '@/redux/modal/modal.slice'; import React from 'react'; import { twMerge } from 'tailwind-merge'; @@ -27,6 +27,11 @@ export const ContextMenu = (): React.ReactNode => { } }; + const handleAddCommentClick = (): void => { + dispatch(closeContextMenu()); + dispatch(openAddCommentModal()); + }; + return ( <div className={twMerge( @@ -50,6 +55,15 @@ export const ContextMenu = (): React.ReactNode => { > Open MolArt ({getUnitProtId()}) </button> + <hr /> + <button + className={twMerge('cursor-pointer text-xs font-normal')} + onClick={handleAddCommentClick} + type="button" + data-testid="add-comment" + > + Add comment + </button> </div> ); }; diff --git a/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.test.tsx b/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0ba098a130cab5a0631547aa7414526ed2a7d0fa --- /dev/null +++ b/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.test.tsx @@ -0,0 +1,87 @@ +import { StoreType } from '@/redux/store'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { act } from 'react-dom/test-utils'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { MODAL_INITIAL_STATE_MOCK } from '@/redux/modal/modal.mock'; +import { AddCommentModal } from '@/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component'; +import { apiPath } from '@/redux/apiPath'; +import { HttpStatusCode } from 'axios'; +import { ZERO } from '@/constants/common'; +import { commentFixture } from '@/models/fixtures/commentFixture'; +import { commentsFixture } from '@/models/fixtures/commentsFixture'; + +const mockedAxiosClient = mockNetworkResponse(); + +const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); + + return ( + render( + <Wrapper> + <AddCommentModal /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('AddCommentModal - component', () => { + test('renders AddCommentModal component', () => { + renderComponent(); + + const emailInput = screen.getByLabelText(/email/i); + const contentInput = screen.getByLabelText(/content/i); + expect(emailInput).toBeInTheDocument(); + expect(contentInput).toBeInTheDocument(); + }); + + test('handles input change correctly', () => { + renderComponent(); + + const emailInput: HTMLInputElement = screen.getByLabelText(/email/i); + const contentInput: HTMLInputElement = screen.getByLabelText(/content/i); + + fireEvent.change(emailInput, { target: { value: 'test@email.pl' } }); + fireEvent.change(contentInput, { target: { value: 'test content' } }); + + expect(emailInput.value).toBe('test@email.pl'); + expect(contentInput.value).toBe('test content'); + }); + + test('submits form', async () => { + mockedAxiosClient + .onPost(apiPath.addComment(ZERO, ZERO, ZERO)) + .reply(HttpStatusCode.Ok, commentFixture); + mockedAxiosClient.onGet(apiPath.getComments()).reply(HttpStatusCode.Ok, commentsFixture); + + const { store } = renderComponent({ + modal: { + ...MODAL_INITIAL_STATE_MOCK, + isOpen: true, + modalName: 'add-comment', + }, + }); + + const emailInput: HTMLInputElement = screen.getByLabelText(/email/i); + const contentInput: HTMLInputElement = screen.getByLabelText(/content/i); + const submitButton = screen.getByText(/submit/i); + + fireEvent.change(emailInput, { target: { value: 'test@email.pl' } }); + fireEvent.change(contentInput, { target: { value: 'test content' } }); + act(() => { + submitButton.click(); + }); + + await waitFor(() => { + const modalState = store.getState().modal; + expect(modalState.modalName).toBe('none'); + expect(modalState.isOpen).toBeFalsy(); + }); + }); +}); diff --git a/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.tsx b/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9c4fde54b754948a099b53bbaf8626b78f2bbb2c --- /dev/null +++ b/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.tsx @@ -0,0 +1,68 @@ +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { Button } from '@/shared/Button'; +import { Input } from '@/shared/Input'; +import React from 'react'; + +import { addComment } from '@/redux/comment/thunks/addComment'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { lastRightClickSelector } from '@/redux/models/models.selectors'; +import { closeModal } from '@/redux/modal/modal.slice'; +import { getComments } from '@/redux/comment/thunks/getComments'; + +export const AddCommentModal: React.FC = () => { + const dispatch = useAppDispatch(); + const lastClick = useAppSelector(lastRightClickSelector); + + const [data, setData] = React.useState({ + email: '', + content: '', + modelId: lastClick.modelId, + position: lastClick.position, + }); + + const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => { + const { name, value } = e.target; + setData(prevData => ({ ...prevData, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => { + e.preventDefault(); + await dispatch(addComment(data)); + dispatch(closeModal()); + dispatch(getComments()); + }; + + return ( + <div className="w-[400px] border border-t-[#E1E0E6] bg-white p-[24px]"> + <form onSubmit={handleSubmit}> + <label className="mb-5 block text-sm font-semibold" htmlFor="email"> + Email (visible only to moderators): + <Input + type="text" + name="email" + id="email" + placeholder="Your email here..." + value={data.email} + onChange={handleChange} + className="mt-2.5 text-sm font-medium text-font-400" + /> + </label> + <label className="text-sm font-semibold" htmlFor="content"> + Content: + <Input + type="textarea" + name="content" + id="content" + placeholder="Message here..." + value={data.content} + onChange={handleChange} + className="mt-2.5 text-sm font-medium text-font-400" + /> + </label> + <Button type="submit" className="w-full justify-center text-base font-medium"> + Submit + </Button> + </form> + </div> + ); +}; diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx index 44005fcd0cd9fafe6b29b870f0137613faa35e21..8e2a02b528cf172eebb868fd183008da565dde12 100644 --- a/src/components/FunctionalArea/Modal/Modal.component.tsx +++ b/src/components/FunctionalArea/Modal/Modal.component.tsx @@ -2,6 +2,7 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { modalSelector } from '@/redux/modal/modal.selector'; import dynamic from 'next/dynamic'; import { AccessDeniedModal } from '@/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component'; +import { AddCommentModal } from '@/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component'; import { EditOverlayModal } from './EditOverlayModal'; import { LoginModal } from './LoginModal'; import { ErrorReportModal } from './ErrorReportModal'; @@ -56,6 +57,11 @@ export const Modal = (): React.ReactNode => { <AccessDeniedModal /> </ModalLayout> )} + {isOpen && modalName === 'add-comment' && ( + <ModalLayout> + <AddCommentModal /> + </ModalLayout> + )} </> ); }; diff --git a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx index b2630c627897de41145ede1fe6c00a52df95bc10..3afcf1f8cfc442b64f8ced8934545a33337b4d2c 100644 --- a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx +++ b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx @@ -29,6 +29,7 @@ export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => { 'flex h-5/6 w-10/12 flex-col overflow-hidden rounded-lg', modalName === 'login' && 'h-auto w-[400px]', modalName === 'access-denied' && 'h-auto w-[400px]', + modalName === 'add-comment' && 'h-auto w-[400px]', modalName === 'error-report' && 'h-auto w-[800px]', ['edit-overlay', 'logged-in-menu'].includes(modalName) && 'h-auto w-[432px]', )} diff --git a/src/components/Map/Drawer/BioEntityDrawer/Comments/CommentItem.component.tsx b/src/components/Map/Drawer/BioEntityDrawer/Comments/CommentItem.component.tsx index 282ee447d17e2583ad08309f2f9407635debdbc9..f080077c6a0ff6873f14cbb0eabd70f9ea5a6f0e 100644 --- a/src/components/Map/Drawer/BioEntityDrawer/Comments/CommentItem.component.tsx +++ b/src/components/Map/Drawer/BioEntityDrawer/Comments/CommentItem.component.tsx @@ -7,13 +7,9 @@ interface CommentItemProps { export const CommentItem = (commentItemProps: CommentItemProps): JSX.Element => { const { comment } = commentItemProps; - let { owner } = comment; - if (!owner) { - owner = 'Anonymous'; - } return ( <div className="border border-slate-400"> - <div className="p-4 font-bold"> {owner} </div> + <div className="p-4 font-bold"> #{comment.id} </div> <div className="p-4"> {comment.content} </div> </div> ); diff --git a/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.test.ts b/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.test.ts index 2992cfaae4300662cfb1b64fdfe74295738a29cc..bead3d12015b534bd89e5f5cc0beff50f93caeae 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.test.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.test.ts @@ -8,7 +8,7 @@ import { ELEMENT_SEARCH_RESULT_MOCK_REACTION, } from '@/models/mocks/elementSearchResultMock'; import { waitFor } from '@testing-library/react'; -import { FIRST_ARRAY_ELEMENT, SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; +import { FIRST_ARRAY_ELEMENT, SECOND_ARRAY_ELEMENT, SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { onMapRightClick } from './onMapRightClick'; import * as handleDataReset from '../mapSingleClick/handleDataReset'; import * as handleSearchResultForRightClickAction from './handleSearchResultForRightClickAction'; @@ -58,7 +58,8 @@ describe('onMapRightClick - util', () => { it('should fire open context menu handler', async () => { const actions = store.getActions(); expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY); - expect(actions[FIRST_ARRAY_ELEMENT].type).toEqual('contextMenu/openContextMenu'); + expect(actions[FIRST_ARRAY_ELEMENT].type).toEqual('map/updateLastRightClick'); + expect(actions[SECOND_ARRAY_ELEMENT].type).toEqual('contextMenu/openContextMenu'); }); }); diff --git a/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts b/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts index 2288c0c6deb6bedef430c00b506fbd67ff5c7fe8..d5d284892aeb7aeff9b263df4d8db19e93d50dfb 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapRightClick/onMapRightClick.ts @@ -1,9 +1,12 @@ -import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { openContextMenu } from '@/redux/contextMenu/contextMenu.slice'; +import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common'; import { MapSize } from '@/redux/map/map.types'; import { AppDispatch } from '@/redux/store'; import { Coordinate } from 'ol/coordinate'; import { Pixel } from 'ol/pixel'; +import { updateLastRightClick } from '@/redux/map/map.slice'; +import { toLonLat } from 'ol/proj'; +import { latLngToPoint } from '@/utils/map/latLngToPoint'; import { getSearchResults } from '../mapSingleClick/getSearchResults'; import { handleDataReset } from '../mapSingleClick/handleDataReset'; import { handleSearchResultForRightClickAction } from './handleSearchResultForRightClickAction'; @@ -11,6 +14,11 @@ import { handleSearchResultForRightClickAction } from './handleSearchResultForRi /* prettier-ignore */ export const onMapRightClick = (mapSize: MapSize, modelId: number, dispatch: AppDispatch) => async (coordinate: Coordinate, pixel: Pixel): Promise<void> => { + const [lng, lat] = toLonLat(coordinate); + const point = latLngToPoint([lat, lng], mapSize); + + dispatch(updateLastRightClick({coordinates:point, modelId})); + dispatch(handleDataReset); dispatch(openContextMenu(pixel)); diff --git a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts index cb09d0a5bccf2493ae96b6361ca3e5921ee8e9ef..e04f2880a546ad753225e2301de34487753f3c53 100644 --- a/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts +++ b/src/components/Map/MapViewer/utils/listeners/mapSingleClick/onMapSingleClick.ts @@ -4,6 +4,9 @@ import { AppDispatch } from '@/redux/store'; import { Map, MapBrowserEvent } from 'ol'; import { FeatureLike } from 'ol/Feature'; import { Comment } from '@/types/models'; +import { updateLastClick } from '@/redux/map/map.slice'; +import { toLonLat } from 'ol/proj'; +import { latLngToPoint } from '@/utils/map/latLngToPoint'; import { getSearchResults } from './getSearchResults'; import { handleDataReset } from './handleDataReset'; import { handleFeaturesClick } from './handleFeaturesClick'; @@ -14,6 +17,11 @@ export const onMapSingleClick = (mapSize: MapSize, modelId: number, dispatch: AppDispatch, searchDistance: string | undefined, maxZoom: number, zoom: number, isResultDrawerOpen: boolean, comments: Comment[]) => async ({ coordinate, pixel }: Pick<MapBrowserEvent<UIEvent>, 'coordinate' | 'pixel'>, mapInstance: Map): Promise<void> => { + const [lng, lat] = toLonLat(coordinate); + const point = latLngToPoint([lat, lng], mapSize); + + dispatch(updateLastClick({coordinates:point, modelId})); + const featuresAtPixel: FeatureLike[] = []; mapInstance.forEachFeatureAtPixel(pixel, (feature) => featuresAtPixel.push(feature)); const { shouldBlockCoordSearch } = handleFeaturesClick(featuresAtPixel, dispatch, comments); @@ -26,7 +34,7 @@ export const onMapSingleClick = // so we need to reset all the data before updating dispatch(handleDataReset); - const {searchResults, point} = await getSearchResults({ coordinate, mapSize, modelId }); + const {searchResults} = await getSearchResults({ coordinate, mapSize, modelId }); if (!searchResults || searchResults.length === SIZE_OF_EMPTY_ARRAY) { return; } diff --git a/src/models/fixtures/commentFixture.ts b/src/models/fixtures/commentFixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..a28e2618e273c2cdf736be26c231cfce0ba5a53a --- /dev/null +++ b/src/models/fixtures/commentFixture.ts @@ -0,0 +1,8 @@ +import { ZOD_SEED } from '@/constants'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { commentSchema } from '@/models/commentSchema'; + +export const commentFixture = createFixture(commentSchema, { + seed: ZOD_SEED, +}); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index ffb7a308907de9364ea48fa05affdc5695de7005..22f16e77a7483b0fdd2f86ce20aaee85bc1f5c2f 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -103,4 +103,6 @@ export const apiPath = { submitError: (): string => `minervanet/submitError`, userPrivileges: (login: string): string => `users/${login}?columns=privileges`, getComments: (): string => `projects/${PROJECT_ID}/comments/models/*/`, + addComment: (modelId: number, x: number, y: number): string => + `projects/${PROJECT_ID}/comments/models/${modelId}/points/${x},${y}/`, }; diff --git a/src/redux/comment/comment.types.ts b/src/redux/comment/comment.types.ts index 581da6e4959f40728d402d1dcf51750c731cb15b..8cb0e4b59872fc53a0ddeb147b506628abf4f1af 100644 --- a/src/redux/comment/comment.types.ts +++ b/src/redux/comment/comment.types.ts @@ -1,6 +1,7 @@ import { FetchDataState } from '@/types/fetchDataState'; import { BioEntity, Comment, Reaction } from '@/types/models'; import { PayloadAction } from '@reduxjs/toolkit'; +import { Point } from '@/types/map'; export interface CommentsState extends FetchDataState<Comment[], []> { isOpen: boolean; @@ -15,3 +16,10 @@ export type GetElementProps = { elementId: number; modelId: number; }; + +export type AddCommentProps = { + email: string; + content: string; + modelId: number; + position: Point; +}; diff --git a/src/redux/comment/thunks/addComment.ts b/src/redux/comment/thunks/addComment.ts new file mode 100644 index 0000000000000000000000000000000000000000..cbb3a4e177da924596b6e4068a05afea4f6d7d93 --- /dev/null +++ b/src/redux/comment/thunks/addComment.ts @@ -0,0 +1,28 @@ +import { commentSchema } from '@/models/commentSchema'; +import { apiPath } from '@/redux/apiPath'; +import { axiosInstance } from '@/services/api/utils/axiosInstance'; +import { ThunkConfig } from '@/types/store'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { Comment } from '@/types/models'; +import { AddCommentProps } from '@/redux/comment/comment.types'; +import { getError } from '@/utils/error-report/getError'; + +export const addComment = createAsyncThunk<Comment | null, AddCommentProps, ThunkConfig>( + 'project/addComment', + async ({ email, content, modelId, position }) => { + try { + const payload = new URLSearchParams({ email, content }); + const response = await axiosInstance.post<Comment>( + apiPath.addComment(modelId, Math.trunc(position.x), Math.trunc(position.y)), + payload, + ); + + const isDataValid = validateDataUsingZodSchema(response.data, commentSchema); + + return isDataValid ? response.data : null; + } catch (error) { + return Promise.reject(getError({ error })); + } + }, +); diff --git a/src/redux/comment/thunks/getComments.ts b/src/redux/comment/thunks/getComments.ts index 7f526700e26f80c3120427bf573ec881ce81b3e2..05156ede750d4f74c8f43fce95d44ef4690f13f5 100644 --- a/src/redux/comment/thunks/getComments.ts +++ b/src/redux/comment/thunks/getComments.ts @@ -10,6 +10,7 @@ import { bioEntitySchema } from '@/models/bioEntitySchema'; import { GetElementProps } from '@/redux/comment/comment.types'; import { reactionSchema } from '@/models/reaction'; import { ZERO } from '@/constants/common'; +import { getError } from '@/utils/error-report/getError'; export const getComments = createAsyncThunk<Comment[], void, ThunkConfig>( 'project/getComments', @@ -21,7 +22,7 @@ export const getComments = createAsyncThunk<Comment[], void, ThunkConfig>( return isDataValid ? response.data : []; } catch (error) { - return Promise.reject(error); + return Promise.reject(getError({ error })); } }, ); @@ -38,7 +39,7 @@ export const getCommentElement = createAsyncThunk<BioEntity | null, GetElementPr return isDataValid ? response.data : null; } catch (error) { - return Promise.reject(error); + return Promise.reject(getError({ error })); } }, ); @@ -55,7 +56,7 @@ export const getCommentReaction = createAsyncThunk<Reaction | null, GetElementPr return isDataValid && response.data.length > ZERO ? response.data[ZERO] : null; } catch (error) { - return Promise.reject(error); + return Promise.reject(getError({ error })); } }, ); diff --git a/src/redux/map/map.constants.ts b/src/redux/map/map.constants.ts index 70126c71e6dbd43d510b1662096b9178d20452f1..3b9e9e530e5ee7b93e2c5e7141b7d79b74ab72fc 100644 --- a/src/redux/map/map.constants.ts +++ b/src/redux/map/map.constants.ts @@ -39,6 +39,20 @@ export const MAP_DATA_INITIAL_STATE: MapData = { minZoom: DEFAULT_MIN_ZOOM, maxZoom: DEFAULT_MAX_ZOOM, }, + lastClick: { + modelId: MODEL_ID_DEFAULT, + position: { + x: 0, + y: 0, + }, + }, + lastRightClick: { + modelId: MODEL_ID_DEFAULT, + position: { + x: 0, + y: 0, + }, + }, }; export const DEFAULT_POSITION: Point = { x: 0, y: 0, z: 0 }; diff --git a/src/redux/map/map.fixtures.ts b/src/redux/map/map.fixtures.ts index be022b6972ad0c42f4ff1c7c9e2c95351f47af4e..049bdc645285fcfcaa89883d21fe85fd86f6884e 100644 --- a/src/redux/map/map.fixtures.ts +++ b/src/redux/map/map.fixtures.ts @@ -1,4 +1,5 @@ import { DEFAULT_ERROR } from '@/constants/errors'; +import { MODEL_ID_DEFAULT } from '@/redux/map/map.constants'; import { MapData, MapState, OppenedMap } from './map.types'; export const openedMapsInitialValueFixture: OppenedMap[] = [ @@ -32,6 +33,20 @@ export const initialMapDataFixture: MapData = { minZoom: 2, maxZoom: 9, }, + lastClick: { + modelId: MODEL_ID_DEFAULT, + position: { + x: 0, + y: 0, + }, + }, + lastRightClick: { + modelId: MODEL_ID_DEFAULT, + position: { + x: 0, + y: 0, + }, + }, }; export const initialMapStateFixture: MapState = { diff --git a/src/redux/map/map.reducers.ts b/src/redux/map/map.reducers.ts index c65bfe9746acc4cd2197cab5200fc891066ba834..f193e7536e94936d2662d7bb69f102f2149f569c 100644 --- a/src/redux/map/map.reducers.ts +++ b/src/redux/map/map.reducers.ts @@ -15,6 +15,7 @@ import { OpenMapAndSetActiveAction, SetActiveMapAction, SetBackgroundAction, + SetLastClickPositionAction, SetLastPositionZoomAction, SetLastPositionZoomWithDeltaAction, SetMapDataAction, @@ -100,6 +101,22 @@ export const setLastPositionZoomReducer = ( state.data.position.initial.z = zoom; }; +export const updateLastClickReducer = ( + state: MapState, + action: SetLastClickPositionAction, +): void => { + state.data.lastClick.modelId = action.payload.modelId; + state.data.lastClick.position = action.payload.coordinates; +}; + +export const updateLastRightClickReducer = ( + state: MapState, + action: SetLastClickPositionAction, +): void => { + state.data.lastRightClick.modelId = action.payload.modelId; + state.data.lastRightClick.position = action.payload.coordinates; +}; + const updateLastPositionOfCurrentlyActiveMap = (state: MapState): void => { const currentMapId = state.data.modelId; const currentOpenedMap = state.openedMaps.find(openedMap => openedMap.modelId === currentMapId); diff --git a/src/redux/map/map.slice.ts b/src/redux/map/map.slice.ts index 46f68b78379b43eea92e3897455d4ddd82f5668b..3106a118eaa14cdcc373ea0366b74c53e121dffb 100644 --- a/src/redux/map/map.slice.ts +++ b/src/redux/map/map.slice.ts @@ -14,6 +14,8 @@ import { setMapBackgroundReducer, setMapDataReducer, setMapPositionReducer, + updateLastClickReducer, + updateLastRightClickReducer, varyPositionZoomReducer, } from './map.reducers'; @@ -31,6 +33,8 @@ const mapSlice = createSlice({ setMapBackground: setMapBackgroundReducer, openMapAndOrSetActiveIfSelected: openMapAndOrSetActiveIfSelectedReducer, setLastPositionZoom: setLastPositionZoomReducer, + updateLastClick: updateLastClickReducer, + updateLastRightClick: updateLastRightClickReducer, }, extraReducers: builder => { initMapPositionReducers(builder); @@ -51,6 +55,8 @@ export const { varyPositionZoom, openMapAndOrSetActiveIfSelected, setLastPositionZoom, + updateLastClick, + updateLastRightClick, } = mapSlice.actions; export default mapSlice.reducer; diff --git a/src/redux/map/map.types.ts b/src/redux/map/map.types.ts index 727df76f71b29da80f9690edcc3e9e86c9888d12..72b600dcf1a42cc08ea329d65d82144db088abaa 100644 --- a/src/redux/map/map.types.ts +++ b/src/redux/map/map.types.ts @@ -30,6 +30,14 @@ export type MapData = { overlaysIds: number[]; size: MapSize; position: Position; + lastClick: { + position: Point; + modelId: number; + }; + lastRightClick: { + position: Point; + modelId: number; + }; show: { legend: boolean; comments: boolean; @@ -101,6 +109,13 @@ export type SetLastPositionZoomActionPayload = { export type SetLastPositionZoomAction = PayloadAction<SetLastPositionZoomActionPayload>; +export type SetLastClickPositionActionPayload = { + modelId: number; + coordinates: Point; +}; + +export type SetLastClickPositionAction = PayloadAction<SetLastClickPositionActionPayload>; + export type InitMapDataActionPayload = { data: GetUpdatedMapDataResult | object; openedMaps: OppenedMap[]; diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts index a93e5fd119e12aac58667c6dc61dca3f442176ad..f9d88800610efc49275e3cdad86a19a2396a4d97 100644 --- a/src/redux/modal/modal.reducers.ts +++ b/src/redux/modal/modal.reducers.ts @@ -43,6 +43,12 @@ export const openLoginModalReducer = (state: ModalState): void => { state.modalTitle = 'You need to login'; }; +export const openAddCommentModalReducer = (state: ModalState): void => { + state.isOpen = true; + state.modalName = 'add-comment'; + state.modalTitle = 'Add comment'; +}; + export const openLoggedInMenuModalReducer = (state: ModalState): void => { state.isOpen = true; state.modalName = 'logged-in-menu'; diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts index b0b76cf56c66de2674305fb57b1f864de18d7f48..57d852cd19596f39ccb2476e0aae599d4927be4f 100644 --- a/src/redux/modal/modal.slice.ts +++ b/src/redux/modal/modal.slice.ts @@ -10,6 +10,7 @@ import { openPublicationsModalReducer, openEditOverlayModalReducer, openLoggedInMenuModalReducer, + openAddCommentModalReducer, openErrorReportModalReducer, openAccessDeniedModalReducer, } from './modal.reducers'; @@ -24,6 +25,7 @@ const modalSlice = createSlice({ openMolArtModalById: openMolArtModalByIdReducer, setOverviewImageId: setOverviewImageIdReducer, openLoginModal: openLoginModalReducer, + openAddCommentModal: openAddCommentModalReducer, openPublicationsModal: openPublicationsModalReducer, openEditOverlayModal: openEditOverlayModalReducer, openLoggedInMenuModal: openLoggedInMenuModalReducer, @@ -37,6 +39,7 @@ export const { closeModal, openOverviewImagesModalById, setOverviewImageId, + openAddCommentModal, openMolArtModalById, openLoginModal, openPublicationsModal, diff --git a/src/redux/models/models.selectors.ts b/src/redux/models/models.selectors.ts index 99d94b7648a22f1d07d77ac63cdf69592d55c0f9..406e1f3c28623f2cff51abac303dd841016fb30b 100644 --- a/src/redux/models/models.selectors.ts +++ b/src/redux/models/models.selectors.ts @@ -33,6 +33,12 @@ export const currentModelIdSelector = createSelector( model => model?.idObject || MODEL_ID_DEFAULT, ); +export const lastClickSelector = createSelector(mapDataSelector, mapData => mapData.lastClick); +export const lastRightClickSelector = createSelector( + mapDataSelector, + mapData => mapData.lastRightClick, +); + export const currentModelNameSelector = createSelector( currentModelSelector, model => model?.name || '', diff --git a/src/types/modal.ts b/src/types/modal.ts index 499406998946f45b8b045ee95fae952b05594387..90e7c20d501aa6c7cb50642df1eea295c6406816 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -1,5 +1,6 @@ export type ModalName = | 'none' + | 'add-comment' | 'overview-images' | 'mol-art' | 'login' diff --git a/src/utils/error-report/getErrorStack.ts b/src/utils/error-report/getErrorStack.ts index e148696f16214a786cb7a48ee1862fa3743bbbe2..d57851b3b3c04a3ab0d76af7ea297930e47a2c1a 100644 --- a/src/utils/error-report/getErrorStack.ts +++ b/src/utils/error-report/getErrorStack.ts @@ -5,7 +5,6 @@ export const getErrorStack = (error: unknown): string => { let stack = null; if (axios.isAxiosError(error)) { const url = getErrorUrl(error); - stack = (url ? `(Request URL: ${url}) ` : '') + error.stack; } else if (error instanceof Error) { stack = error.stack; diff --git a/src/utils/error-report/getErrorUrl.ts b/src/utils/error-report/getErrorUrl.ts index 53ef46e9f4e5c4bf4bb9541f252594f357c8de8f..0edd3d69702b291cb8a7aa18c9feb53461520226 100644 --- a/src/utils/error-report/getErrorUrl.ts +++ b/src/utils/error-report/getErrorUrl.ts @@ -6,6 +6,10 @@ export const getErrorUrl = (error: unknown): string | null => { if (error.request.responseURL) { return error.request.responseURL; } + } else if (error.config) { + if (error.config.url) { + return error.config.url; + } } } return null;