From 8292e62c6ab0cc3a94e11877a00429ab2e714cbd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tadeusz=20Miesi=C4=85c?= <tadeusz.miesiac@gmail.com>
Date: Wed, 8 Nov 2023 21:03:41 +0100
Subject: [PATCH] feat(submaps): saving position on tab switch

---
 src/redux/map/map.constants.ts             |  4 ++-
 src/redux/map/map.reducers.ts              | 40 ++++++++++++++--------
 src/redux/map/map.selectors.ts             |  6 ++++
 src/redux/map/map.slice.ts                 |  4 +--
 src/redux/map/map.thunks.test.ts           | 32 ++++++++++++++---
 src/redux/map/map.thunks.ts                | 33 ++++++++++++++----
 src/redux/map/map.types.ts                 | 25 ++++++++++----
 src/redux/map/middleware/map.middleware.ts | 31 ++++++++++-------
 src/utils/initialize/useInitializeStore.ts |  1 +
 src/utils/map/getUpdatedMapData.ts         |  8 +----
 10 files changed, 128 insertions(+), 56 deletions(-)

diff --git a/src/redux/map/map.constants.ts b/src/redux/map/map.constants.ts
index 46ead440..333a2062 100644
--- a/src/redux/map/map.constants.ts
+++ b/src/redux/map/map.constants.ts
@@ -12,11 +12,13 @@ export const MAIN_MAP = 'Main map';
 
 export const MODEL_ID_DEFAULT: number = 0;
 
+export const BACKGROUND_ID_DEFAULT: number = 0;
+
 export const MAP_DATA_INITIAL_STATE: MapData = {
   projectId: PROJECT_ID,
   meshId: '',
   modelId: MODEL_ID_DEFAULT,
-  backgroundId: 0,
+  backgroundId: BACKGROUND_ID_DEFAULT,
   overlaysIds: [],
   position: {
     last: DEFAULT_CENTER_POINT,
diff --git a/src/redux/map/map.reducers.ts b/src/redux/map/map.reducers.ts
index e83ea7bc..e6cacdbc 100644
--- a/src/redux/map/map.reducers.ts
+++ b/src/redux/map/map.reducers.ts
@@ -35,22 +35,16 @@ export const setMapPositionReducer = (state: MapState, action: SetMapPositionDat
   };
 };
 
-export const getMapReducers = (builder: ActionReducerMapBuilder<MapState>): void => {
-  builder.addCase(initMapData.pending, state => {
-    state.loading = 'pending';
-  });
-  builder.addCase(initMapData.fulfilled, (state, action) => {
-    const payload = action.payload || {};
-    state.data = { ...state.data, ...payload };
-    state.loading = 'succeeded';
-  });
-  builder.addCase(initMapData.rejected, state => {
-    state.loading = 'failed';
-    // TODO to discuss manage state of failure
-  });
+const updateLastPositionOfCurrentlyActiveMap = (state: MapState): void => {
+  const currentMapId = state.data.modelId;
+  const currentOpenedMap = state.openedMaps.find(openedMap => openedMap.modelId === currentMapId);
+  if (currentOpenedMap) {
+    currentOpenedMap.lastPosition = state.data.position.last;
+  }
 };
 
 export const setActiveMapReducer = (state: MapState, action: SetActiveMapAction): void => {
+  updateLastPositionOfCurrentlyActiveMap(state);
   state.data.modelId = action.payload.modelId;
 };
 
@@ -58,6 +52,8 @@ export const openMapAndSetActiveReducer = (
   state: MapState,
   action: OpenMapAndSetActiveAction,
 ): void => {
+  updateLastPositionOfCurrentlyActiveMap(state);
+
   state.openedMaps.push({
     modelId: action.payload.modelId,
     modelName: action.payload.modelName,
@@ -83,7 +79,23 @@ export const closeMapAndSetMainMapActiveReducer = (
     state.openedMaps.find(openedMap => openedMap.modelName === MAIN_MAP)?.modelId || ZERO;
 };
 
-export const getMapPositionReducers = (builder: ActionReducerMapBuilder<MapState>): void => {
+export const getMapReducers = (builder: ActionReducerMapBuilder<MapState>): void => {
+  builder.addCase(initMapData.pending, state => {
+    state.loading = 'pending';
+  });
+  builder.addCase(initMapData.fulfilled, (state, action) => {
+    const payload = action.payload || {};
+    state.data = { ...state.data, ...payload.data };
+    state.openedMaps = payload.openedMaps;
+    state.loading = 'succeeded';
+  });
+  builder.addCase(initMapData.rejected, state => {
+    state.loading = 'failed';
+    // TODO to discuss manage state of failure
+  });
+};
+
+export const initMapPositionReducers = (builder: ActionReducerMapBuilder<MapState>): void => {
   builder.addCase(initMapPosition.pending, state => {
     state.loading = 'pending';
   });
diff --git a/src/redux/map/map.selectors.ts b/src/redux/map/map.selectors.ts
index 303e21f5..3f9d9d86 100644
--- a/src/redux/map/map.selectors.ts
+++ b/src/redux/map/map.selectors.ts
@@ -17,6 +17,12 @@ export const mapDataInitialPositionSelector = createSelector(
   position => position.initial,
 );
 
+export const mapOpenedMapPositionByIdSelector = createSelector(
+  [mapOpenedMapsSelector, (_state, modelId: number): number => modelId],
+  (openedMaps, modelId) =>
+    openedMaps.find(openedMap => openedMap.modelId === modelId)?.lastPosition,
+);
+
 export const mapDataLastPositionSelector = createSelector(
   mapDataPositionSelector,
   position => position.last,
diff --git a/src/redux/map/map.slice.ts b/src/redux/map/map.slice.ts
index d03d62f0..5c28c40e 100644
--- a/src/redux/map/map.slice.ts
+++ b/src/redux/map/map.slice.ts
@@ -7,7 +7,7 @@ import {
   openMapAndSetActiveReducer,
   setActiveMapReducer,
   setMapDataReducer,
-  getMapPositionReducers,
+  initMapPositionReducers,
   setMapPositionReducer,
 } from './map.reducers';
 import { MapState } from './map.types';
@@ -32,7 +32,7 @@ const mapSlice = createSlice({
   },
   extraReducers: builder => {
     getMapReducers(builder);
-    getMapPositionReducers(builder);
+    initMapPositionReducers(builder);
   },
 });
 
diff --git a/src/redux/map/map.thunks.test.ts b/src/redux/map/map.thunks.test.ts
index d717ad71..39b347d2 100644
--- a/src/redux/map/map.thunks.test.ts
+++ b/src/redux/map/map.thunks.test.ts
@@ -61,10 +61,29 @@ describe('map thunks', () => {
 
       it('should return valid payload', () => {
         const FIRST = 0;
-
         expect(payload).toMatchObject({
-          modelId: modelsFixture[FIRST].idObject,
-          backgroundId: backgroundsFixture[FIRST].id,
+          data: {
+            modelId: modelsFixture[FIRST].idObject,
+            backgroundId: backgroundsFixture[FIRST].id,
+            size: {
+              width: 66.1207745783031,
+              height: -54.25165700726211,
+              tileSize: 85.73858779855072,
+              minZoom: 19.16961562819779,
+              maxZoom: 78.78634324297309,
+            },
+            position: {
+              initial: { x: -47.612417908385396, y: -27.125828503631055, z: -97.42596028372645 },
+              last: { x: -47.612417908385396, y: -27.125828503631055, z: -97.42596028372645 },
+            },
+          },
+          openedMaps: [
+            {
+              modelId: 63.59699326567352,
+              modelName: 'Main map',
+              lastPosition: { x: 0, y: 0, z: 0 },
+            },
+          ],
         });
       });
     });
@@ -88,8 +107,11 @@ describe('map thunks', () => {
           .payload as InitMapDataActionPayload;
       });
 
-      it('should return empty payload', () => {
-        expect(payload).toStrictEqual({});
+      it('should return empty values for data and openedMaps in payload', () => {
+        expect(payload).toStrictEqual({
+          data: {},
+          openedMaps: [{ modelId: 0, modelName: 'Main map', lastPosition: { x: 0, y: 0, z: 0 } }],
+        });
       });
     });
   });
diff --git a/src/redux/map/map.thunks.ts b/src/redux/map/map.thunks.ts
index a5823110..45251da5 100644
--- a/src/redux/map/map.thunks.ts
+++ b/src/redux/map/map.thunks.ts
@@ -1,7 +1,8 @@
 /* eslint-disable no-magic-numbers */
 import { PROJECT_ID } from '@/constants';
 import { QueryData } from '@/types/query';
-import { GetUpdatedMapDataResult, getUpdatedMapData } from '@/utils/map/getUpdatedMapData';
+import { ZERO } from '@/constants/common';
+import { getUpdatedMapData } from '@/utils/map/getUpdatedMapData';
 import { createAsyncThunk } from '@reduxjs/toolkit';
 import { backgroundsDataSelector } from '../backgrounds/background.selectors';
 import { getAllBackgroundsByProjectId } from '../backgrounds/backgrounds.thunks';
@@ -10,10 +11,13 @@ import { getModels } from '../models/models.thunks';
 import { getAllPublicOverlaysByProjectId } from '../overlays/overlays.thunks';
 import type { AppDispatch, RootState } from '../store';
 import {
+  GetUpdatedMapDataResult,
   InitMapDataActionParams,
   InitMapDataActionPayload,
+  OppenedMap,
   SetMapPositionDataActionPayload,
 } from './map.types';
+import { DEFAULT_POSITION, MAIN_MAP } from './map.constants';
 
 const getInitMapDataPayload = (
   state: RootState,
@@ -22,7 +26,7 @@ const getInitMapDataPayload = (
   const FIRST = 0;
   const models = modelsDataSelector(state);
   const backgrounds = backgroundsDataSelector(state);
-  const modelId = queryData?.modelId || models?.[FIRST]?.idObject;
+  const modelId = queryData?.modelId || models?.[FIRST]?.idObject || ZERO; // TS does not get the type correctly. It might be undefined so fallback to 0 is needed
   const backgroundId = queryData?.backgroundId || backgrounds?.[FIRST]?.id;
   const model = models.find(({ idObject }) => idObject === modelId);
   const background = backgrounds.find(({ id }) => id === backgroundId);
@@ -42,6 +46,18 @@ const getInitMapDataPayload = (
   });
 };
 
+const getUpdatedOpenedMapWithMainMap = (state: RootState): OppenedMap[] => {
+  const FIRST = 0;
+  const models = modelsDataSelector(state);
+  const mainMapId = models?.[FIRST]?.idObject || ZERO;
+
+  const openedMaps: OppenedMap[] = [
+    { modelId: mainMapId, modelName: MAIN_MAP, lastPosition: DEFAULT_POSITION },
+  ];
+
+  return openedMaps;
+};
+
 export const initMapData = createAsyncThunk<
   InitMapDataActionPayload,
   InitMapDataActionParams,
@@ -56,18 +72,21 @@ export const initMapData = createAsyncThunk<
     ]);
 
     const state = getState();
-    return getInitMapDataPayload(state, queryData);
+    const mapDataPayload = getInitMapDataPayload(state, queryData);
+    const openedMapsPayload = getUpdatedOpenedMapWithMainMap(state);
+    return { data: mapDataPayload, openedMaps: openedMapsPayload };
   },
 );
-
 export const initMapPosition = createAsyncThunk<
-  InitMapDataActionPayload,
+  SetMapPositionDataActionPayload,
   InitMapDataActionParams,
   { dispatch: AppDispatch; state: RootState }
 >(
   'map/initMapPosition',
-  async ({ queryData }, { getState }): Promise<SetMapPositionDataActionPayload> => {
+  async ({ queryData }, { getState }): Promise<GetUpdatedMapDataResult | object> => {
     const state = getState();
-    return getInitMapDataPayload(state, queryData);
+
+    const mapDataPayload = getInitMapDataPayload(state, queryData);
+    return mapDataPayload;
   },
 );
diff --git a/src/redux/map/map.types.ts b/src/redux/map/map.types.ts
index e9f01c12..c08d05a4 100644
--- a/src/redux/map/map.types.ts
+++ b/src/redux/map/map.types.ts
@@ -42,6 +42,10 @@ export type SetMapDataActionPayload =
     })
   | undefined;
 
+export type UpdateOpenedMainMapActionPayload = Pick<OppenedMap, 'modelId' | 'lastPosition'>;
+
+export type UpdateOpenedMainMapAction = PayloadAction<UpdateOpenedMainMapActionPayload>;
+
 export type SetMapDataAction = PayloadAction<SetMapDataActionPayload>;
 
 export type SetActiveMapActionPayload = Pick<OppenedMap, 'modelId'>;
@@ -60,14 +64,8 @@ export type CloseMapAction = PayloadAction<CloseMapActionPayload>;
 
 export type InitMapDataActionParams = { queryData: QueryData };
 
-export type InitMapDataActionPayload = SetMapDataActionPayload | object;
-
 export type InitMapDataAction = PayloadAction<SetMapDataAction>;
 
-export type MiddlewareAllowedAction = PayloadAction<
-  SetMapDataActionPayload | InitMapDataActionPayload
->;
-
 export type SetMapDataByQueryDataActionParams = { queryData: QueryData };
 
 export type SetMapDataByQueryDataActionPayload = Pick<
@@ -75,6 +73,19 @@ export type SetMapDataByQueryDataActionPayload = Pick<
   'modelId' | 'backgroundId' | 'position'
 >;
 
-export type SetMapPositionDataActionPayload = Pick<MapData, 'position'> | object;
+export type GetUpdatedMapDataResult = Pick<
+  MapData,
+  'modelId' | 'backgroundId' | 'size' | 'position'
+>;
+
+export type SetMapPositionDataActionPayload = GetUpdatedMapDataResult | object;
 
 export type SetMapPositionDataAction = PayloadAction<SetMapPositionDataActionPayload>;
+
+export type InitMapDataActionPayload = {
+  data: GetUpdatedMapDataResult | object;
+  openedMaps: OppenedMap[];
+};
+export type MiddlewareAllowedAction = PayloadAction<
+  SetMapDataActionPayload | InitMapDataActionPayload
+>;
diff --git a/src/redux/map/middleware/map.middleware.ts b/src/redux/map/middleware/map.middleware.ts
index 90ba6609..5658c145 100644
--- a/src/redux/map/middleware/map.middleware.ts
+++ b/src/redux/map/middleware/map.middleware.ts
@@ -1,23 +1,22 @@
 import { currentBackgroundSelector } from '@/redux/backgrounds/background.selectors';
-import type { AppDispatch, AppListenerEffectAPI, AppStartListening } from '@/redux/store';
-import { GetUpdatedMapDataResult, getUpdatedMapData } from '@/utils/map/getUpdatedMapData';
+import type { AppListenerEffectAPI, AppStartListening } from '@/redux/store';
+import { getUpdatedMapData } from '@/utils/map/getUpdatedMapData';
 import { Action, createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit';
-import { setMapData, setMapPosition } from '../map.slice';
+import {
+  openMapAndSetActive,
+  setActiveMap,
+  setMapData,
+  setMapPosition,
+  closeMapAndSetMainMapActive,
+} from '../map.slice';
 import { checkIfIsMapUpdateActionValid } from './checkIfIsMapUpdateActionValid';
 import { getUpdatedModel } from './getUpdatedModel';
+import { mapOpenedMapPositionByIdSelector } from '../map.selectors';
 
 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,
@@ -31,12 +30,18 @@ export const mapDataMiddlewareListener = async (
   }
 
   const background = currentBackgroundSelector(state);
-  const updatedMapData = getUpdatedMapData({ model: updatedModel, background });
+  const modelId = updatedModel.idObject;
+  const lastPosition = mapOpenedMapPositionByIdSelector(state, modelId);
+  const updatedMapData = getUpdatedMapData({
+    model: updatedModel,
+    position: { initial: lastPosition, last: lastPosition },
+    background,
+  });
   dispatch(setMapData(updatedMapData));
   dispatch(setMapPosition(updatedMapData));
 };
 
 startListening({
-  matcher: isAnyOf(setMapData),
+  matcher: isAnyOf(setMapData, setActiveMap, openMapAndSetActive, closeMapAndSetMainMapActive),
   effect: mapDataMiddlewareListener,
 });
diff --git a/src/utils/initialize/useInitializeStore.ts b/src/utils/initialize/useInitializeStore.ts
index 207bd0ca..6c620809 100644
--- a/src/utils/initialize/useInitializeStore.ts
+++ b/src/utils/initialize/useInitializeStore.ts
@@ -19,6 +19,7 @@ export const getInitStoreData =
   ({ queryData }: GetInitStoreDataArgs) =>
     (dispatch: AppDispatch): void => {
       dispatch(getProjectById(PROJECT_ID));
+      // when app loads
       dispatch(initMapData({ queryData }));
       dispatch(initMapPosition({ queryData }));
     };
diff --git a/src/utils/map/getUpdatedMapData.ts b/src/utils/map/getUpdatedMapData.ts
index dbfcf755..6baeeae0 100644
--- a/src/utils/map/getUpdatedMapData.ts
+++ b/src/utils/map/getUpdatedMapData.ts
@@ -1,10 +1,6 @@
 import { DEFAULT_ZOOM } from '@/constants/map';
 import { MAP_DATA_INITIAL_STATE } from '@/redux/map/map.constants';
-import {
-  MapData,
-  SetMapDataActionPayload,
-  SetMapPositionDataActionPayload,
-} from '@/redux/map/map.types';
+import { GetUpdatedMapDataResult, MapData } from '@/redux/map/map.types';
 import { MapBackground, MapModel } from '@/types/models';
 import { DeepPartial } from '@reduxjs/toolkit';
 import { getPointMerged } from '../object/getPointMerged';
@@ -15,8 +11,6 @@ interface GetUpdatedMapDataArgs {
   background?: MapBackground;
 }
 
-export type GetUpdatedMapDataResult = SetMapDataActionPayload & SetMapPositionDataActionPayload;
-
 const HALF = 2;
 
 export const getUpdatedMapData = ({
-- 
GitLab