From 087f0a7d8e928d81a09b7b3a31d9e23ae3d8678c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com>
Date: Tue, 7 Nov 2023 03:08:36 +0100
Subject: [PATCH] feat: extract set map position action from the set map data
 action

---
 .../utils/config/useOlMapView.test.ts         |  4 +--
 .../utils/listeners/onMapPositionChange.ts    |  4 +--
 src/redux/map/map.constants.ts                |  2 +-
 src/redux/map/map.reducers.ts                 | 34 +++++++++++++++----
 src/redux/map/map.slice.ts                    | 11 ++++--
 src/redux/map/map.thunks.ts                   | 22 ++++++++++--
 src/redux/map/map.types.ts                    |  7 ++--
 .../map/middleware/map.middleware.test.ts     | 10 ++++--
 src/redux/map/middleware/map.middleware.ts    | 15 ++++++--
 src/utils/initialize/useInitializeStore.ts    |  3 +-
 src/utils/map/getUpdatedMapData.ts            |  8 +++--
 11 files changed, 93 insertions(+), 27 deletions(-)

diff --git a/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts
index 8a78f734..6ff16dd9 100644
--- a/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts
+++ b/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts
@@ -1,5 +1,5 @@
 /* eslint-disable no-magic-numbers */
-import mapSlice, { setMapData } from '@/redux/map/map.slice';
+import mapSlice, { setMapPosition } from '@/redux/map/map.slice';
 import { getReduxWrapperUsingSliceReducer } from '@/utils/testing/getReduxWrapperUsingSliceReducer';
 import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
 import { renderHook, waitFor } from '@testing-library/react';
@@ -42,7 +42,7 @@ describe('useOlMapView - util', () => {
     const CALLED_ONCE = 1;
 
     store.dispatch(
-      setMapData({
+      setMapPosition({
         position: {
           initial: {
             x: 0,
diff --git a/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts b/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts
index 3873bf5b..482f0509 100644
--- a/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts
+++ b/src/components/Map/MapViewer/utils/listeners/onMapPositionChange.ts
@@ -1,4 +1,4 @@
-import { setMapData } from '@/redux/map/map.slice';
+import { setMapPosition } from '@/redux/map/map.slice';
 import { MapSize } from '@/redux/map/map.types';
 import { AppDispatch } from '@/redux/store';
 import { latLngToPoint } from '@/utils/map/latLngToPoint';
@@ -15,7 +15,7 @@ export const onMapPositionChange =
       const { x, y } = latLngToPoint([lat, lng], mapSize, { rounded: true });
 
       dispatch(
-        setMapData({
+        setMapPosition({
           position: {
             last: {
               x,
diff --git a/src/redux/map/map.constants.ts b/src/redux/map/map.constants.ts
index 34be216d..de82f296 100644
--- a/src/redux/map/map.constants.ts
+++ b/src/redux/map/map.constants.ts
@@ -30,4 +30,4 @@ export const MAP_DATA_INITIAL_STATE: MapData = {
   },
 };
 
-export const MIDDLEWARE_ALLOWED_ACTIONS: string[] = ['map/setMapData', 'map/initMapData'];
+export const MIDDLEWARE_ALLOWED_ACTIONS: string[] = ['map/setMapData'];
diff --git a/src/redux/map/map.reducers.ts b/src/redux/map/map.reducers.ts
index 25099909..7dc80074 100644
--- a/src/redux/map/map.reducers.ts
+++ b/src/redux/map/map.reducers.ts
@@ -1,17 +1,24 @@
 import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
-import merge from 'ts-deepmerge';
 import { getPointMerged } from '../../utils/object/getPointMerged';
-import { initMapData } from './map.thunks';
-import { MapState, SetMapDataAction } from './map.types';
+import { initMapData, initMapPosition } from './map.thunks';
+import { MapState, SetMapDataAction, SetMapPositionDataAction } from './map.types';
 
 export const setMapDataReducer = (state: MapState, action: SetMapDataAction): void => {
   const payload = action.payload || {};
-  const payloadPosition = payload?.position || {};
-  const statePosition = state.data.position;
 
   state.data = {
     ...state.data,
     ...payload,
+  };
+};
+
+export const setMapPositionReducer = (state: MapState, action: SetMapPositionDataAction): void => {
+  const payload = action.payload || {};
+  const payloadPosition = 'position' in payload ? payload.position : undefined;
+  const statePosition = state.data.position;
+
+  state.data = {
+    ...state.data,
     position: {
       initial: getPointMerged(payloadPosition?.initial || {}, statePosition.initial),
       last: getPointMerged(payloadPosition?.last || {}, statePosition.last),
@@ -25,7 +32,7 @@ export const getMapReducers = (builder: ActionReducerMapBuilder<MapState>): void
   });
   builder.addCase(initMapData.fulfilled, (state, action) => {
     const payload = action.payload || {};
-    state.data = merge(state.data, payload);
+    state.data = { ...state.data, ...payload };
     state.loading = 'succeeded';
   });
   builder.addCase(initMapData.rejected, state => {
@@ -33,3 +40,18 @@ export const getMapReducers = (builder: ActionReducerMapBuilder<MapState>): void
     // TODO to discuss manage state of failure
   });
 };
+
+export const getMapPositionReducers = (builder: ActionReducerMapBuilder<MapState>): void => {
+  builder.addCase(initMapPosition.pending, state => {
+    state.loading = 'pending';
+  });
+  builder.addCase(initMapPosition.fulfilled, (state, action) => {
+    const payload = action.payload || {};
+    state.data = { ...state.data, ...payload };
+    state.loading = 'succeeded';
+  });
+  builder.addCase(initMapPosition.rejected, state => {
+    state.loading = 'failed';
+    // TODO to discuss manage state of failure
+  });
+};
diff --git a/src/redux/map/map.slice.ts b/src/redux/map/map.slice.ts
index 3565dc25..49a21589 100644
--- a/src/redux/map/map.slice.ts
+++ b/src/redux/map/map.slice.ts
@@ -1,6 +1,11 @@
 import { createSlice } from '@reduxjs/toolkit';
 import { MAP_DATA_INITIAL_STATE } from './map.constants';
-import { getMapReducers, setMapDataReducer } from './map.reducers';
+import {
+  getMapPositionReducers,
+  getMapReducers,
+  setMapDataReducer,
+  setMapPositionReducer,
+} from './map.reducers';
 import { MapState } from './map.types';
 
 const initialState: MapState = {
@@ -14,12 +19,14 @@ const mapSlice = createSlice({
   initialState,
   reducers: {
     setMapData: setMapDataReducer,
+    setMapPosition: setMapPositionReducer,
   },
   extraReducers: builder => {
     getMapReducers(builder);
+    getMapPositionReducers(builder);
   },
 });
 
-export const { setMapData } = mapSlice.actions;
+export const { setMapData, setMapPosition } = mapSlice.actions;
 
 export default mapSlice.reducer;
diff --git a/src/redux/map/map.thunks.ts b/src/redux/map/map.thunks.ts
index 062328d6..0ebf3326 100644
--- a/src/redux/map/map.thunks.ts
+++ b/src/redux/map/map.thunks.ts
@@ -1,6 +1,6 @@
 import { PROJECT_ID } from '@/constants';
 import { QueryData } from '@/types/query';
-import { getUpdatedMapData } from '@/utils/map/getUpdatedMapData';
+import { GetUpdatedMapDataResult, getUpdatedMapData } from '@/utils/map/getUpdatedMapData';
 import { createAsyncThunk } from '@reduxjs/toolkit';
 import { backgroundsDataSelector } from '../backgrounds/background.selectors';
 import { getAllBackgroundsByProjectId } from '../backgrounds/backgrounds.thunks';
@@ -8,12 +8,16 @@ import { modelsDataSelector } from '../models/models.selectors';
 import { getModels } from '../models/models.thunks';
 import { getAllPublicOverlaysByProjectId } from '../overlays/overlays.thunks';
 import type { AppDispatch, RootState } from '../store';
-import { InitMapDataActionParams, InitMapDataActionPayload } from './map.types';
+import {
+  InitMapDataActionParams,
+  InitMapDataActionPayload,
+  SetMapPositionDataActionPayload,
+} from './map.types';
 
 const getInitMapDataPayload = (
   state: RootState,
   queryData: QueryData,
-): InitMapDataActionPayload => {
+): GetUpdatedMapDataResult | object => {
   const FIRST = 0;
   const models = modelsDataSelector(state);
   const backgrounds = backgroundsDataSelector(state);
@@ -54,3 +58,15 @@ export const initMapData = createAsyncThunk<
     return getInitMapDataPayload(state, queryData);
   },
 );
+
+export const initMapPosition = createAsyncThunk<
+  InitMapDataActionPayload,
+  InitMapDataActionParams,
+  { dispatch: AppDispatch; state: RootState }
+>(
+  'map/initMapPosition',
+  async ({ queryData }, { getState }): Promise<SetMapPositionDataActionPayload> => {
+    const state = getState();
+    return getInitMapDataPayload(state, queryData);
+  },
+);
diff --git a/src/redux/map/map.types.ts b/src/redux/map/map.types.ts
index 2f723872..db099d11 100644
--- a/src/redux/map/map.types.ts
+++ b/src/redux/map/map.types.ts
@@ -1,7 +1,7 @@
 import { FetchDataState } from '@/types/fetchDataState';
 import { Point } from '@/types/map';
 import { QueryData } from '@/types/query';
-import { DeepPartial, PayloadAction } from '@reduxjs/toolkit';
+import { PayloadAction } from '@reduxjs/toolkit';
 
 export interface MapSize {
   width: number;
@@ -32,7 +32,6 @@ export type MapState = FetchDataState<MapData, MapData>;
 
 export type SetMapDataActionPayload =
   | (Omit<Partial<MapData>, 'position' | 'projectId'> & {
-      position?: DeepPartial<MapData['position']>;
       projectId?: string;
     })
   | undefined;
@@ -55,3 +54,7 @@ export type SetMapDataByQueryDataActionPayload = Pick<
   MapData,
   'modelId' | 'backgroundId' | 'position'
 >;
+
+export type SetMapPositionDataActionPayload = Pick<MapData, 'position'> | object;
+
+export type SetMapPositionDataAction = PayloadAction<SetMapPositionDataActionPayload>;
diff --git a/src/redux/map/middleware/map.middleware.test.ts b/src/redux/map/middleware/map.middleware.test.ts
index c821eeda..3359b92a 100644
--- a/src/redux/map/middleware/map.middleware.test.ts
+++ b/src/redux/map/middleware/map.middleware.test.ts
@@ -4,7 +4,7 @@ import { Loading } from '@/types/loadingState';
 import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
 import { Action } from '@reduxjs/toolkit';
 import { MAP_DATA_INITIAL_STATE, MIDDLEWARE_ALLOWED_ACTIONS } from '../map.constants';
-import * as setMapData from '../map.slice';
+import * as mapSlice from '../map.slice';
 import * as checkIfIsMapUpdateActionValid from './checkIfIsMapUpdateActionValid';
 import * as getUpdatedModel from './getUpdatedModel';
 import { mapDataMiddlewareListener } from './map.middleware';
@@ -48,7 +48,8 @@ const checkIfIsMapUpdateActionValidSpy = jest.spyOn(
   'checkIfIsMapUpdateActionValid',
 );
 const getUpdatedModelSpy = jest.spyOn(getUpdatedModel, 'getUpdatedModel');
-const setMapDataSpy = jest.spyOn(setMapData, 'setMapData');
+const setMapDataSpy = jest.spyOn(mapSlice, 'setMapData');
+const setMapPositionSpy = jest.spyOn(mapSlice, 'setMapPosition');
 
 const { store } = getReduxWrapperWithStore({
   map: {
@@ -81,7 +82,7 @@ describe('map middleware', () => {
     });
 
     describe('when model is valid and different than current', () => {
-      it.each(MIDDLEWARE_ALLOWED_ACTIONS)('should dispatch setMapData', async actionType => {
+      it.each(MIDDLEWARE_ALLOWED_ACTIONS)('should dispatch setMapData, %s', async actionType => {
         const model = modelsFixture[1];
 
         const action = {
@@ -95,6 +96,7 @@ describe('map middleware', () => {
         expect(checkIfIsMapUpdateActionValidSpy).toHaveLastReturnedWith(true);
         expect(getUpdatedModelSpy).toHaveLastReturnedWith(model);
         expect(setMapDataSpy).toBeCalled();
+        expect(setMapPositionSpy).toBeCalled();
       });
     });
 
@@ -112,6 +114,7 @@ describe('map middleware', () => {
         expect(checkIfIsMapUpdateActionValidSpy).toHaveLastReturnedWith(false);
         expect(getUpdatedModelSpy).toHaveLastReturnedWith(model);
         expect(setMapDataSpy).not.toBeCalled();
+        expect(setMapPositionSpy).not.toBeCalled();
       });
     });
 
@@ -128,6 +131,7 @@ describe('map middleware', () => {
         expect(checkIfIsMapUpdateActionValidSpy).toHaveLastReturnedWith(true);
         expect(getUpdatedModelSpy).toHaveLastReturnedWith(undefined);
         expect(setMapDataSpy).not.toBeCalled();
+        expect(setMapPositionSpy).not.toBeCalled();
       });
     });
   });
diff --git a/src/redux/map/middleware/map.middleware.ts b/src/redux/map/middleware/map.middleware.ts
index a09dc6bc..f3e12654 100644
--- a/src/redux/map/middleware/map.middleware.ts
+++ b/src/redux/map/middleware/map.middleware.ts
@@ -1,8 +1,8 @@
 import { currentBackgroundSelector } from '@/redux/backgrounds/background.selectors';
-import type { AppListenerEffectAPI, AppStartListening } from '@/redux/store';
-import { getUpdatedMapData } from '@/utils/map/getUpdatedMapData';
+import type { AppDispatch, AppListenerEffectAPI, AppStartListening } from '@/redux/store';
+import { GetUpdatedMapDataResult, getUpdatedMapData } from '@/utils/map/getUpdatedMapData';
 import { Action, createListenerMiddleware } from '@reduxjs/toolkit';
-import { setMapData } from '../map.slice';
+import { setMapData, setMapPosition } from '../map.slice';
 import { checkIfIsMapUpdateActionValid } from './checkIfIsMapUpdateActionValid';
 import { getUpdatedModel } from './getUpdatedModel';
 
@@ -10,6 +10,14 @@ export const mapListenerMiddleware = createListenerMiddleware();
 
 const startListening = mapListenerMiddleware.startListening as AppStartListening;
 
+/* prettier-ignore */
+export const dispatchMapDataWithPosition =
+  (updatedMapData: GetUpdatedMapDataResult) =>
+    (dispatch: AppDispatch): void => {
+      dispatch(setMapData(updatedMapData));
+      dispatch(setMapPosition(updatedMapData));
+    };
+
 export const mapDataMiddlewareListener = async (
   action: Action,
   { getOriginalState, dispatch }: AppListenerEffectAPI,
@@ -25,6 +33,7 @@ export const mapDataMiddlewareListener = async (
   const background = currentBackgroundSelector(state);
   const updatedMapData = getUpdatedMapData({ model: updatedModel, background });
   dispatch(setMapData(updatedMapData));
+  dispatch(setMapPosition(updatedMapData));
 };
 
 startListening({
diff --git a/src/utils/initialize/useInitializeStore.ts b/src/utils/initialize/useInitializeStore.ts
index 68a6100e..207bd0ca 100644
--- a/src/utils/initialize/useInitializeStore.ts
+++ b/src/utils/initialize/useInitializeStore.ts
@@ -1,6 +1,6 @@
 import { PROJECT_ID } from '@/constants';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
-import { initMapData } from '@/redux/map/map.thunks';
+import { initMapData, initMapPosition } from '@/redux/map/map.thunks';
 import { getProjectById } from '@/redux/project/project.thunks';
 import { initDataLoadingInitialized } from '@/redux/root/init.selectors';
 import { AppDispatch } from '@/redux/store';
@@ -20,6 +20,7 @@ export const getInitStoreData =
     (dispatch: AppDispatch): void => {
       dispatch(getProjectById(PROJECT_ID));
       dispatch(initMapData({ queryData }));
+      dispatch(initMapPosition({ queryData }));
     };
 
 export const useInitializeStore = (): void => {
diff --git a/src/utils/map/getUpdatedMapData.ts b/src/utils/map/getUpdatedMapData.ts
index c3ebf2a0..dbfcf755 100644
--- a/src/utils/map/getUpdatedMapData.ts
+++ b/src/utils/map/getUpdatedMapData.ts
@@ -1,6 +1,10 @@
 import { DEFAULT_ZOOM } from '@/constants/map';
 import { MAP_DATA_INITIAL_STATE } from '@/redux/map/map.constants';
-import { MapData, SetMapDataActionPayload } from '@/redux/map/map.types';
+import {
+  MapData,
+  SetMapDataActionPayload,
+  SetMapPositionDataActionPayload,
+} from '@/redux/map/map.types';
 import { MapBackground, MapModel } from '@/types/models';
 import { DeepPartial } from '@reduxjs/toolkit';
 import { getPointMerged } from '../object/getPointMerged';
@@ -11,7 +15,7 @@ interface GetUpdatedMapDataArgs {
   background?: MapBackground;
 }
 
-type GetUpdatedMapDataResult = SetMapDataActionPayload;
+export type GetUpdatedMapDataResult = SetMapDataActionPayload & SetMapPositionDataActionPayload;
 
 const HALF = 2;
 
-- 
GitLab