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