diff --git a/src/constants/errors.ts b/src/constants/errors.ts new file mode 100644 index 0000000000000000000000000000000000000000..887f64f35d00d60ce428da9f5b4239c0d8c6cbaa --- /dev/null +++ b/src/constants/errors.ts @@ -0,0 +1 @@ +export const DEFAULT_ERROR: Error = { message: '', name: '' }; diff --git a/src/redux/backgrounds/background.mock.ts b/src/redux/backgrounds/background.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..515bb7249c74207a0435d8a220ac24da9ad55b6e --- /dev/null +++ b/src/redux/backgrounds/background.mock.ts @@ -0,0 +1,60 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { MapBackground } from '@/types/models'; +import { BackgroundsState } from './backgrounds.types'; + +export const BACKGROUND_INITIAL_STATE_MOCK: BackgroundsState = { + data: undefined, + loading: 'idle', + error: DEFAULT_ERROR, +}; + +export const BACKGROUNDS_MOCK: MapBackground[] = [ + { + id: 13, + name: 'Pathways and compartments', + defaultOverlay: false, + project: { + projectId: 'pdmap_appu_test', + }, + creator: { + login: 'admin', + }, + status: 'NA', + progress: 0, + description: null, + order: 0, + images: [], + }, + { + id: 14, + name: 'Network', + defaultOverlay: false, + project: { + projectId: 'pdmap_appu_test', + }, + creator: { + login: 'admin', + }, + status: 'NA', + progress: 0, + description: null, + order: 1, + images: [], + }, + { + id: 15, + name: 'Empty', + defaultOverlay: false, + project: { + projectId: 'pdmap_appu_test', + }, + creator: { + login: 'admin', + }, + status: 'NA', + progress: 0, + description: null, + order: 2, + images: [], + }, +]; diff --git a/src/redux/backgrounds/background.selectors.ts b/src/redux/backgrounds/background.selectors.ts index 16b233972a19c5eb7edc916f74c716c41550e48b..319bfc27376c4a099fe51ee0fee14b4a2e73348d 100644 --- a/src/redux/backgrounds/background.selectors.ts +++ b/src/redux/backgrounds/background.selectors.ts @@ -9,6 +9,12 @@ export const backgroundsDataSelector = createSelector( backgrounds => backgrounds?.data || [], ); +const MAIN_BACKGROUND = 0; +export const mainBackgroundsDataSelector = createSelector( + backgroundsDataSelector, + backgrounds => backgrounds[MAIN_BACKGROUND], +); + export const currentBackgroundSelector = createSelector( backgroundsDataSelector, mapDataSelector, diff --git a/src/redux/bioEntity/bioEntity.mock.ts b/src/redux/bioEntity/bioEntity.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..3706162e8dd2ec52452f51cdbaef0c4dd50e027a --- /dev/null +++ b/src/redux/bioEntity/bioEntity.mock.ts @@ -0,0 +1,8 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { BioEntityContentsState } from './bioEntity.types'; + +export const BIOENTITY_INITIAL_STATE_MOCK: BioEntityContentsState = { + data: undefined, + loading: 'idle', + error: DEFAULT_ERROR, +}; diff --git a/src/redux/chemicals/chemicals.mock.ts b/src/redux/chemicals/chemicals.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..3b492a2e4656daa87eed1047a89ceed0049500e9 --- /dev/null +++ b/src/redux/chemicals/chemicals.mock.ts @@ -0,0 +1,8 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { ChemicalsState } from './chemicals.types'; + +export const CHEMICALS_INITIAL_STATE_MOCK: ChemicalsState = { + data: undefined, + loading: 'idle', + error: DEFAULT_ERROR, +}; diff --git a/src/redux/drugs/drugs.mock.ts b/src/redux/drugs/drugs.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..884c8de9aa8d89a6c2b830ed85427b0a77e06f37 --- /dev/null +++ b/src/redux/drugs/drugs.mock.ts @@ -0,0 +1,8 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { DrugsState } from './drugs.types'; + +export const DRUGS_INITIAL_STATE_MOCK: DrugsState = { + data: undefined, + loading: 'idle', + error: DEFAULT_ERROR, +}; diff --git a/src/redux/map/map.fixtures.ts b/src/redux/map/map.fixtures.ts index 2826117268dd476e2d73a17f640269ad273bbc2e..5cb2f16fe2683d2988f2a7bd16aff6e95a558eaf 100644 --- a/src/redux/map/map.fixtures.ts +++ b/src/redux/map/map.fixtures.ts @@ -1,4 +1,5 @@ -import { MapData, OppenedMap } from './map.types'; +import { DEFAULT_ERROR } from '@/constants/errors'; +import { MapData, MapState, OppenedMap } from './map.types'; export const openedMapsInitialValueFixture: OppenedMap[] = [ { modelId: 0, modelName: 'Main map', lastPosition: { x: 0, y: 0, z: 0 } }, @@ -32,3 +33,10 @@ export const initialMapDataFixture: MapData = { maxZoom: 9, }, }; + +export const initialMapStateFixture: MapState = { + data: initialMapDataFixture, + loading: 'idle', + error: DEFAULT_ERROR, + openedMaps: openedMapsInitialValueFixture, +}; diff --git a/src/redux/map/map.reducers.ts b/src/redux/map/map.reducers.ts index e6cacdbc8d7dab2d5ed856098d0db344a35cc9c3..b0ad81bf535fa81d7f6dc1678a8b2b6d7eaeee06 100644 --- a/src/redux/map/map.reducers.ts +++ b/src/redux/map/map.reducers.ts @@ -10,7 +10,12 @@ import { } from './map.types'; import { MAIN_MAP } from './map.constants'; import { getPointMerged } from '../../utils/object/getPointMerged'; -import { initMapData, initMapPosition } from './map.thunks'; +import { + initMapBackground, + initMapPosition, + initMapSizeAndModelId, + initOpenedMaps, +} from './map.thunks'; export const setMapDataReducer = (state: MapState, action: SetMapDataAction): void => { const payload = action.payload || {}; @@ -79,33 +84,28 @@ export const closeMapAndSetMainMapActiveReducer = ( state.openedMaps.find(openedMap => openedMap.modelName === MAIN_MAP)?.modelId || ZERO; }; -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 initMapSizeAndModelIdReducer = (builder: ActionReducerMapBuilder<MapState>): void => { + builder.addCase(initMapSizeAndModelId.fulfilled, (state, action) => { + state.data.modelId = action.payload.modelId; + state.data.size = action.payload.size; }); }; export const initMapPositionReducers = (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.data.position = action.payload; + }); +}; + +export const initMapBackgroundsReducer = (builder: ActionReducerMapBuilder<MapState>): void => { + builder.addCase(initMapBackground.fulfilled, (state, action) => { + state.data.backgroundId = action.payload; state.loading = 'succeeded'; }); - builder.addCase(initMapPosition.rejected, state => { - state.loading = 'failed'; - // TODO to discuss manage state of failure +}; + +export const initOpenedMapsReducer = (builder: ActionReducerMapBuilder<MapState>): void => { + builder.addCase(initOpenedMaps.fulfilled, (state, action) => { + state.openedMaps = action.payload; }); }; diff --git a/src/redux/map/map.slice.ts b/src/redux/map/map.slice.ts index 5c28c40e41bf546fd6ece105d1284951d0a39984..ca51213ca667fc5087cec6f45d988c61f508d8dc 100644 --- a/src/redux/map/map.slice.ts +++ b/src/redux/map/map.slice.ts @@ -3,12 +3,14 @@ import { MAP_DATA_INITIAL_STATE, OPENED_MAPS_INITIAL_STATE } from './map.constan import { closeMapAndSetMainMapActiveReducer, closeMapReducer, - getMapReducers, openMapAndSetActiveReducer, setActiveMapReducer, setMapDataReducer, initMapPositionReducers, setMapPositionReducer, + initOpenedMapsReducer, + initMapSizeAndModelIdReducer, + initMapBackgroundsReducer, } from './map.reducers'; import { MapState } from './map.types'; @@ -31,8 +33,10 @@ const mapSlice = createSlice({ setMapPosition: setMapPositionReducer, }, extraReducers: builder => { - getMapReducers(builder); initMapPositionReducers(builder); + initMapSizeAndModelIdReducer(builder); + initMapBackgroundsReducer(builder); + initOpenedMapsReducer(builder); }, }); diff --git a/src/redux/map/map.thunks.test.ts b/src/redux/map/map.thunks.test.ts index 39b347d21fc1a0fe0f942b8fa54f9f9da0940476..69a2e9e110194858eb8e5a986dc2ce0201afefdb 100644 --- a/src/redux/map/map.thunks.test.ts +++ b/src/redux/map/map.thunks.test.ts @@ -1,20 +1,11 @@ -import { PROJECT_ID } from '@/constants'; -import { backgroundsFixture } from '@/models/fixtures/backgroundsFixture'; -import { modelsFixture } from '@/models/fixtures/modelsFixture'; -import { overlaysFixture } from '@/models/fixtures/overlaysFixture'; +import { MODELS_MOCK } from '@/models/mocks/modelsMock'; +/* eslint-disable no-magic-numbers */ import { QueryData } from '@/types/query'; -import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; -import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; -import { HttpStatusCode } from 'axios'; -import { apiPath } from '../apiPath'; -import { backgroundsDataSelector } from '../backgrounds/background.selectors'; -import { modelsDataSelector } from '../models/models.selectors'; -import { overlaysDataSelector } from '../overlays/overlays.selectors'; -import { AppDispatch, StoreType } from '../store'; -import { initMapData } from './map.thunks'; -import { InitMapDataActionPayload } from './map.types'; - -const mockedAxiosClient = mockNetworkResponse(); +import { BACKGROUNDS_MOCK, BACKGROUND_INITIAL_STATE_MOCK } from '../backgrounds/background.mock'; +import { RootState } from '../store'; +import { INITIAL_STORE_STATE_MOCK } from '../root/root.fixtures'; +import { MODELS_INITIAL_STATE_MOCK } from '../models/models.mock'; +import { getBackgroundId, getInitMapPosition, getInitMapSizeAndModelId } from './map.thunks'; const EMPTY_QUERY_DATA: QueryData = { modelId: undefined, @@ -22,96 +13,98 @@ const EMPTY_QUERY_DATA: QueryData = { initialPosition: undefined, }; -describe('map thunks', () => { - describe('initMapData - thunk', () => { - describe('when API is returning valid data', () => { - let store = {} as StoreType; - let payload = {} as InitMapDataActionPayload; +const QUERY_DATA_WITH_BG: QueryData = { + modelId: undefined, + backgroundId: 21, + initialPosition: undefined, +}; - beforeAll(async () => { - mockedAxiosClient.resetHandlers(); - mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.Ok, modelsFixture); - mockedAxiosClient - .onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true })) - .reply(HttpStatusCode.Ok, overlaysFixture); - mockedAxiosClient - .onGet(apiPath.getAllBackgroundsByProjectIdQuery(PROJECT_ID)) - .reply(HttpStatusCode.Ok, backgroundsFixture); +const QUERY_DATA_WITH_MODELID: QueryData = { + modelId: 5054, + backgroundId: undefined, + initialPosition: undefined, +}; - store = getReduxWrapperWithStore().store; - const dispatch = store.dispatch as AppDispatch; - payload = (await dispatch(initMapData({ queryData: EMPTY_QUERY_DATA }))) - .payload as InitMapDataActionPayload; - }); +const QUERY_DATA_WITH_POSITION: QueryData = { + modelId: undefined, + backgroundId: undefined, + initialPosition: { + x: 21, + y: 3, + z: 7, + }, +}; - it('should fetch backgrounds data in store', async () => { - const data = backgroundsDataSelector(store.getState()); - expect(data).toEqual(backgroundsFixture); - }); +const STATE_WITH_MODELS: RootState = { + ...INITIAL_STORE_STATE_MOCK, + models: { ...MODELS_INITIAL_STATE_MOCK, data: MODELS_MOCK }, +}; - it('should fetch overlays data in store', async () => { - const data = overlaysDataSelector(store.getState()); - expect(data).toEqual(overlaysFixture); - }); +describe('map thunks - utils', () => { + describe('getBackgroundId', () => { + it('should return backgroundId value from queryData', () => { + const backgroundId = getBackgroundId(INITIAL_STORE_STATE_MOCK, QUERY_DATA_WITH_BG); + expect(backgroundId).toBe(21); + }); + it('should return main map background id if query param does not include background id', () => { + const store: RootState = { + ...INITIAL_STORE_STATE_MOCK, + backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK }, + }; + const backgroundId = getBackgroundId(store, EMPTY_QUERY_DATA); + + expect(backgroundId).toBe(13); + }); + it('should return default value (0) if query data does not include backgroundId and could not find main background in the store', () => { + const backgroundId = getBackgroundId(INITIAL_STORE_STATE_MOCK, EMPTY_QUERY_DATA); + + expect(backgroundId).toBe(0); + }); + }); - it('should fetch models data in store', async () => { - const data = modelsDataSelector(store.getState()); - expect(data).toEqual(modelsFixture); + describe('getInitMapPosition', () => { + it('should return valid map position from query params ', () => { + const position = getInitMapPosition(STATE_WITH_MODELS, QUERY_DATA_WITH_POSITION); + expect(position).toEqual({ + initial: { x: 21, y: 3, z: 7 }, + last: { x: 21, y: 3, z: 7 }, }); + }); - it('should return valid payload', () => { - const FIRST = 0; - expect(payload).toMatchObject({ - 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 }, - }, - ], - }); + it('should return valid map position if query params do not include position', () => { + const position = getInitMapPosition(STATE_WITH_MODELS, EMPTY_QUERY_DATA); + expect(position).toEqual({ + initial: { x: 13389.625, y: 6751.5, z: 5 }, + last: { x: 13389.625, y: 6751.5, z: 5 }, }); }); + it('should return default map position', () => { + const position = getInitMapPosition(INITIAL_STORE_STATE_MOCK, EMPTY_QUERY_DATA); - describe('when API is returning empty array', () => { - let store = {} as StoreType; - let payload = {} as InitMapDataActionPayload; + expect(position).toEqual({ initial: { x: 0, y: 0, z: 0 }, last: { x: 0, y: 0, z: 0 } }); + }); + }); - beforeEach(async () => { - mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.Ok, []); - mockedAxiosClient - .onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true })) - .reply(HttpStatusCode.Ok, []); - mockedAxiosClient - .onGet(apiPath.getAllBackgroundsByProjectIdQuery(PROJECT_ID)) - .reply(HttpStatusCode.Ok, []); + describe('getInitMapSizeAndModelId', () => { + it('should return correct mapsize and modelid when modelId is provided in queryData', () => { + const payload = getInitMapSizeAndModelId(STATE_WITH_MODELS, QUERY_DATA_WITH_MODELID); - store = getReduxWrapperWithStore().store; - const dispatch = store.dispatch as AppDispatch; - payload = (await dispatch(initMapData({ queryData: EMPTY_QUERY_DATA }))) - .payload as InitMapDataActionPayload; + expect(payload).toEqual({ + modelId: 5054, + size: { height: 1171.9429798877356, maxZoom: 5, minZoom: 2, tileSize: 256, width: 1652.75 }, }); - - 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 } }], - }); + }); + it('should return correct mapsize and modelId if query params do not include modelId', () => { + const payload = getInitMapSizeAndModelId(STATE_WITH_MODELS, EMPTY_QUERY_DATA); + expect(payload).toEqual({ + modelId: 5053, + size: { + height: 13503, + maxZoom: 9, + minZoom: 2, + tileSize: 256, + width: 26779.25, + }, }); }); }); diff --git a/src/redux/map/map.thunks.ts b/src/redux/map/map.thunks.ts index 45251da5653023fb04184c79c696f170b8ffff8e..f2ea946bf66c0f1086d3e73696fca480d42d1ede 100644 --- a/src/redux/map/map.thunks.ts +++ b/src/redux/map/map.thunks.ts @@ -1,92 +1,155 @@ /* eslint-disable no-magic-numbers */ -import { PROJECT_ID } from '@/constants'; -import { QueryData } from '@/types/query'; -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'; -import { modelsDataSelector } from '../models/models.selectors'; -import { getModels } from '../models/models.thunks'; -import { getAllPublicOverlaysByProjectId } from '../overlays/overlays.thunks'; +import { ZERO } from '@/constants/common'; +import { QueryData } from '@/types/query'; +import { DEFAULT_ZOOM } from '@/constants/map'; +import { getPointMerged } from '@/utils/object/getPointMerged'; import type { AppDispatch, RootState } from '../store'; import { - GetUpdatedMapDataResult, - InitMapDataActionParams, - InitMapDataActionPayload, + InitMapBackgroundActionPayload, + InitMapBackgroundParams, + InitMapPositionActionPayload, + InitMapPositionParams, + InitMapSizeAndModelIdActionPayload, + InitMapSizeAndModelIdParams, + InitOpenedMapsActionPayload, + InitOpenedMapsProps, + MapSizeAndModelId, OppenedMap, - SetMapPositionDataActionPayload, + Position, } from './map.types'; +import { mainBackgroundsDataSelector } from '../backgrounds/background.selectors'; +import { + currentModelSelector, + mainMapModelSelector, + modelByIdSelector, + modelsDataSelector, +} from '../models/models.selectors'; import { DEFAULT_POSITION, MAIN_MAP } from './map.constants'; -const getInitMapDataPayload = ( - state: RootState, - queryData: QueryData, -): GetUpdatedMapDataResult | object => { - const FIRST = 0; - const models = modelsDataSelector(state); - const backgrounds = backgroundsDataSelector(state); - 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); +/** UTILS - in the same file because of dependancy cycle */ + +export const getBackgroundId = (state: RootState, queryData: QueryData): number => { + const mainMapBackground = mainBackgroundsDataSelector(state); + const backgroundId = queryData?.backgroundId || mainMapBackground?.id || ZERO; + + return backgroundId; +}; + +export const getInitMapPosition = (state: RootState, queryData: QueryData): Position => { + const mainMapModel = mainMapModelSelector(state); + const modelId = queryData?.modelId || mainMapModel?.idObject || ZERO; + const currentModel = modelByIdSelector(state, modelId); const position = queryData?.initialPosition; + const HALF = 2; - if (!model || !background) { - return {}; + if (!currentModel) { + return { + last: DEFAULT_POSITION, + initial: DEFAULT_POSITION, + }; } - return getUpdatedMapData({ - model, - background, - position: { - last: position, - initial: position, + const defaultPosition = { + x: currentModel.defaultCenterX ?? currentModel.width / HALF, + y: currentModel.defaultCenterY ?? currentModel.height / HALF, + z: currentModel.defaultZoomLevel ?? DEFAULT_ZOOM, + }; + + const mergedPosition = getPointMerged(position || {}, defaultPosition); + + return { + last: mergedPosition, + initial: mergedPosition, + }; +}; + +export const getInitMapSizeAndModelId = ( + state: RootState, + queryData: QueryData, +): MapSizeAndModelId => { + const mainMapModel = mainMapModelSelector(state); + const modelId = queryData?.modelId || mainMapModel?.idObject || ZERO; + const currentModel = modelByIdSelector(state, modelId); + + return { + modelId: currentModel?.idObject || ZERO, + size: { + width: currentModel?.width || ZERO, + height: currentModel?.height || ZERO, + tileSize: currentModel?.tileSize || ZERO, + minZoom: currentModel?.minZoom || ZERO, + maxZoom: currentModel?.maxZoom || ZERO, }, - }); + }; }; -const getUpdatedOpenedMapWithMainMap = (state: RootState): OppenedMap[] => { +export const getOpenedMaps = (state: RootState, queryData: QueryData): OppenedMap[] => { const FIRST = 0; const models = modelsDataSelector(state); + const currentModel = currentModelSelector(state); const mainMapId = models?.[FIRST]?.idObject || ZERO; const openedMaps: OppenedMap[] = [ { modelId: mainMapId, modelName: MAIN_MAP, lastPosition: DEFAULT_POSITION }, ]; + if (queryData.modelId !== mainMapId) { + openedMaps.push({ + modelId: currentModel?.idObject || ZERO, + modelName: currentModel?.name || '', + lastPosition: { ...DEFAULT_POSITION, ...queryData.initialPosition }, + }); + } return openedMaps; }; -export const initMapData = createAsyncThunk< - InitMapDataActionPayload, - InitMapDataActionParams, +/** THUNKS */ + +export const initMapSizeAndModelId = createAsyncThunk< + InitMapSizeAndModelIdActionPayload, + InitMapSizeAndModelIdParams, { dispatch: AppDispatch; state: RootState } >( - 'map/initMapData', - async ({ queryData }, { dispatch, getState }): Promise<InitMapDataActionPayload> => { - await Promise.all([ - dispatch(getAllBackgroundsByProjectId(PROJECT_ID)), - dispatch(getAllPublicOverlaysByProjectId(PROJECT_ID)), - dispatch(getModels()), - ]); - + 'map/initMapSizeAndModelId', + async ({ queryData }, { getState }): Promise<InitMapSizeAndModelIdActionPayload> => { const state = getState(); - const mapDataPayload = getInitMapDataPayload(state, queryData); - const openedMapsPayload = getUpdatedOpenedMapWithMainMap(state); - return { data: mapDataPayload, openedMaps: openedMapsPayload }; + + return getInitMapSizeAndModelId(state, queryData); }, ); + export const initMapPosition = createAsyncThunk< - SetMapPositionDataActionPayload, - InitMapDataActionParams, + InitMapPositionActionPayload, + InitMapPositionParams, { dispatch: AppDispatch; state: RootState } >( 'map/initMapPosition', - async ({ queryData }, { getState }): Promise<GetUpdatedMapDataResult | object> => { + async ({ queryData }, { getState }): Promise<InitMapPositionActionPayload> => { const state = getState(); - const mapDataPayload = getInitMapDataPayload(state, queryData); - return mapDataPayload; + return getInitMapPosition(state, queryData); }, ); + +export const initMapBackground = createAsyncThunk< + InitMapBackgroundActionPayload, + InitMapBackgroundParams, + { dispatch: AppDispatch; state: RootState } +>( + 'map/initMapBackground', + async ({ queryData }, { getState }): Promise<InitMapBackgroundActionPayload> => { + const state = getState(); + return getBackgroundId(state, queryData); + }, +); + +export const initOpenedMaps = createAsyncThunk< + InitOpenedMapsActionPayload, + InitOpenedMapsProps, + { dispatch: AppDispatch; state: RootState } +>('appInit/initOpenedMaps', async ({ queryData }, { getState }): Promise<OppenedMap[]> => { + const state = getState(); + + return getOpenedMaps(state, queryData); +}); diff --git a/src/redux/map/map.types.ts b/src/redux/map/map.types.ts index c08d05a466a649dc7c22bca06e2c71ad6030ec15..bd641cd7708a0116fe2a8ba12741733b4a9ac1f8 100644 --- a/src/redux/map/map.types.ts +++ b/src/redux/map/map.types.ts @@ -17,6 +17,11 @@ export type OppenedMap = { lastPosition: Point; }; +export type Position = { + initial: Point; + last: Point; +}; + export type MapData = { projectId: string; meshId: string; @@ -24,10 +29,7 @@ export type MapData = { backgroundId: number; overlaysIds: number[]; size: MapSize; - position: { - initial: Point; - last: Point; - }; + position: Position; show: { legend: boolean; comments: boolean; @@ -89,3 +91,25 @@ export type InitMapDataActionPayload = { export type MiddlewareAllowedAction = PayloadAction< SetMapDataActionPayload | InitMapDataActionPayload >; + +export type InitOpenedMapsActionPayload = OppenedMap[]; + +export type InitOpenedMapsProps = { + queryData: QueryData; +}; + +export type MapSizeAndModelId = Pick<MapData, 'modelId' | 'size'>; +export type InitMapSizeAndModelIdActionPayload = MapSizeAndModelId; +export type InitMapSizeAndModelIdParams = { + queryData: QueryData; +}; + +export type InitMapPositionActionPayload = Position; +export type InitMapPositionParams = { + queryData: QueryData; +}; + +export type InitMapBackgroundActionPayload = number; +export type InitMapBackgroundParams = { + queryData: QueryData; +}; diff --git a/src/redux/mirnas/mirnas.mock.ts b/src/redux/mirnas/mirnas.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..233c3558ef20fe6eb8a1e607c64b8940ad8aaac7 --- /dev/null +++ b/src/redux/mirnas/mirnas.mock.ts @@ -0,0 +1,8 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { MirnasState } from './mirnas.types'; + +export const MIRNAS_INITIAL_STATE_MOCK: MirnasState = { + data: undefined, + loading: 'idle', + error: DEFAULT_ERROR, +}; diff --git a/src/redux/models/models.mock.ts b/src/redux/models/models.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..e45763d82516d7ff30c12ee7512a8da5f2378cdf --- /dev/null +++ b/src/redux/models/models.mock.ts @@ -0,0 +1,8 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { ModelsState } from './models.types'; + +export const MODELS_INITIAL_STATE_MOCK: ModelsState = { + data: [], + loading: 'idle', + error: DEFAULT_ERROR, +}; diff --git a/src/redux/models/models.selectors.ts b/src/redux/models/models.selectors.ts index 8a9478b171d44dfc00afb4a23f34517b14ab6d33..6044064a8795beb57992daee1de9a9650013af9e 100644 --- a/src/redux/models/models.selectors.ts +++ b/src/redux/models/models.selectors.ts @@ -11,3 +11,11 @@ export const currentModelSelector = createSelector( mapDataSelector, (models, mapData) => models.find(model => model.idObject === mapData.modelId), ); + +export const modelByIdSelector = createSelector( + [modelsSelector, (_state, modelId: number): number => modelId], + (models, modelId) => (models?.data || []).find(({ idObject }) => idObject === modelId), +); + +const MAIN_MAP = 0; +export const mainMapModelSelector = createSelector(modelsDataSelector, models => models[MAIN_MAP]); diff --git a/src/redux/overlays/overlays.mock.ts b/src/redux/overlays/overlays.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a8037b6ba6e1ca5a7d096a71ae79a6f3c388fd5 --- /dev/null +++ b/src/redux/overlays/overlays.mock.ts @@ -0,0 +1,8 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { OverlaysState } from './overlays.types'; + +export const OVERLAYS_INITIAL_STATE_MOCK: OverlaysState = { + data: [], + loading: 'idle', + error: DEFAULT_ERROR, +}; diff --git a/src/redux/project/project.mock.ts b/src/redux/project/project.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..036d26346ca92cb0a8108cb83b7c7584db660381 --- /dev/null +++ b/src/redux/project/project.mock.ts @@ -0,0 +1,8 @@ +import { DEFAULT_ERROR } from '@/constants/errors'; +import { ProjectState } from './project.types'; + +export const PROJECT_STATE_INITIAL_MOCK: ProjectState = { + data: undefined, + loading: 'idle', + error: DEFAULT_ERROR, +}; diff --git a/src/redux/root/init.selectors.ts b/src/redux/root/init.selectors.ts index 5095956a5633679600d67a86d458ecffb7227ac2..67cdfa08df4a16ab64f183a41c1c741ccc62bb74 100644 --- a/src/redux/root/init.selectors.ts +++ b/src/redux/root/init.selectors.ts @@ -13,7 +13,15 @@ export const initDataLoadingInitialized = createSelector( (...selectors) => selectors.every(selector => selector.loading !== 'idle'), ); -export const initDataLoadingFinished = createSelector( +export const initDataLoadingFinishedSelector = createSelector( + projectSelector, + backgroundsSelector, + modelsSelector, + overlaysSelector, + (...selectors) => selectors.every(selector => selector.loading === 'succeeded'), +); + +export const initDataAndMapLoadingFinished = createSelector( projectSelector, backgroundsSelector, modelsSelector, diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts new file mode 100644 index 0000000000000000000000000000000000000000..c91e96efa82dbbd6ab9d2dd628fb4ff5f8770728 --- /dev/null +++ b/src/redux/root/init.thunks.ts @@ -0,0 +1,40 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { PROJECT_ID } from '@/constants'; +import { AppDispatch } from '@/redux/store'; +import { QueryData } from '@/types/query'; +import { getAllBackgroundsByProjectId } from '../backgrounds/backgrounds.thunks'; +import { getAllPublicOverlaysByProjectId } from '../overlays/overlays.thunks'; +import { getModels } from '../models/models.thunks'; +import { getProjectById } from '../project/project.thunks'; +import { + initMapBackground, + initMapPosition, + initMapSizeAndModelId, + initOpenedMaps, +} from '../map/map.thunks'; + +interface InitializeAppParams { + queryData: QueryData; +} + +export const fetchInitialAppData = createAsyncThunk< + void, + InitializeAppParams, + { dispatch: AppDispatch } +>('appInit/fetchInitialAppData', async ({ queryData }, { dispatch }): Promise<void> => { + /** Fetch all data required for renderin map */ + await Promise.all([ + dispatch(getProjectById(PROJECT_ID)), + dispatch(getAllBackgroundsByProjectId(PROJECT_ID)), + dispatch(getAllPublicOverlaysByProjectId(PROJECT_ID)), + dispatch(getModels()), + ]); + /** Set map properties to allow rendering. If map params (modelId,backgroundId,position) are not provided in query -> it will be set to map default */ + await Promise.all([ + dispatch(initMapSizeAndModelId({ queryData })), + dispatch(initMapPosition({ queryData })), + dispatch(initMapBackground({ queryData })), + ]); + /** Create tabs for maps / submaps */ + dispatch(initOpenedMaps({ queryData })); +}); diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bd6e11e4351cdd423943a69e309c61fe632aa1e --- /dev/null +++ b/src/redux/root/root.fixtures.ts @@ -0,0 +1,26 @@ +import { BACKGROUND_INITIAL_STATE_MOCK } from '../backgrounds/background.mock'; +import { BIOENTITY_INITIAL_STATE_MOCK } from '../bioEntity/bioEntity.mock'; +import { CHEMICALS_INITIAL_STATE_MOCK } from '../chemicals/chemicals.mock'; +import { initialStateFixture as drawerInitialStateMock } from '../drawer/drawerFixture'; +import { DRUGS_INITIAL_STATE_MOCK } from '../drugs/drugs.mock'; +import { initialMapStateFixture } from '../map/map.fixtures'; +import { MIRNAS_INITIAL_STATE_MOCK } from '../mirnas/mirnas.mock'; +import { MODELS_INITIAL_STATE_MOCK } from '../models/models.mock'; +import { OVERLAYS_INITIAL_STATE_MOCK } from '../overlays/overlays.mock'; +import { PROJECT_STATE_INITIAL_MOCK } from '../project/project.mock'; +import { SEARCH_STATE_INITIAL_MOCK } from '../search/search.mock'; +import { RootState } from '../store'; + +export const INITIAL_STORE_STATE_MOCK: RootState = { + search: SEARCH_STATE_INITIAL_MOCK, + project: PROJECT_STATE_INITIAL_MOCK, + drugs: DRUGS_INITIAL_STATE_MOCK, + mirnas: MIRNAS_INITIAL_STATE_MOCK, + chemicals: CHEMICALS_INITIAL_STATE_MOCK, + models: MODELS_INITIAL_STATE_MOCK, + bioEntity: BIOENTITY_INITIAL_STATE_MOCK, + backgrounds: BACKGROUND_INITIAL_STATE_MOCK, + drawer: drawerInitialStateMock, + map: initialMapStateFixture, + overlays: OVERLAYS_INITIAL_STATE_MOCK, +}; diff --git a/src/redux/search/search.mock.ts b/src/redux/search/search.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..83aa9e6ce6b0602f9027f54ae9c29c36dfa1d40a --- /dev/null +++ b/src/redux/search/search.mock.ts @@ -0,0 +1,6 @@ +import { SearchState } from './search.types'; + +export const SEARCH_STATE_INITIAL_MOCK: SearchState = { + searchValue: '', + loading: 'idle', +}; diff --git a/src/types/query.ts b/src/types/query.ts index a715a34a3397f9f4bb7b2a3eaa7e82657d7d1463..bd9cb48cdd9983d0bde3e12c4b0bb63e2cceb6f3 100644 --- a/src/types/query.ts +++ b/src/types/query.ts @@ -13,3 +13,11 @@ export interface QueryDataParams { y?: number; z?: number; } + +export interface QueryDataRouterParams { + modelId?: string; + backgroundId?: string; + x?: string; + y?: string; + z?: string; +} diff --git a/src/utils/initialize/useInitializeStore.ts b/src/utils/initialize/useInitializeStore.ts index 6c620809c1eb16f4892a3da5695e8983ffbb01ae..9722dd6173ea1fe82164f422f0b7d6dc106a22de 100644 --- a/src/utils/initialize/useInitializeStore.ts +++ b/src/utils/initialize/useInitializeStore.ts @@ -1,44 +1,30 @@ -import { PROJECT_ID } from '@/constants'; -import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -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'; -import { QueryData } from '@/types/query'; import { useRouter } from 'next/router'; -import { useEffect } from 'react'; -import { useSelector } from 'react-redux'; -import { getQueryData } from '../query-manager/getQueryData'; - -interface GetInitStoreDataArgs { - queryData: QueryData; -} +import { useEffect, useMemo } from 'react'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { + initDataLoadingFinishedSelector, + initDataLoadingInitialized, +} from '@/redux/root/init.selectors'; +import { fetchInitialAppData } from '@/redux/root/init.thunks'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { parseQueryToTypes } from '../parseQueryToTypes'; -/* prettier-ignore */ -export const getInitStoreData = - ({ queryData }: GetInitStoreDataArgs) => - (dispatch: AppDispatch): void => { - dispatch(getProjectById(PROJECT_ID)); - // when app loads - dispatch(initMapData({ queryData })); - dispatch(initMapPosition({ queryData })); - }; +/** + * 1. Initialise all required data before app starts: Project info, available Backgrounds, available Overlays, available Models (maps,submaps) + * 2. Based on that set required map data to correctly display view. If query params are available -> use them to set map data + */ export const useInitializeStore = (): void => { const dispatch = useAppDispatch(); - const isInitialized = useSelector(initDataLoadingInitialized); + const isInitialized = useAppSelector(initDataLoadingInitialized); + const isInitDataLoadingFinished = useAppSelector(initDataLoadingFinishedSelector); const { query, isReady: isRouterReady } = useRouter(); + const isQueryReady = useMemo(() => query && isRouterReady, [query, isRouterReady]); useEffect(() => { - const isQueryReady = query && isRouterReady; if (isInitialized || !isQueryReady) { return; } - - dispatch( - getInitStoreData({ - queryData: getQueryData(query), - }), - ); - }, [dispatch, query, isInitialized, isRouterReady]); + dispatch(fetchInitialAppData({ queryData: parseQueryToTypes(query) })); + }, [dispatch, isInitialized, query, isQueryReady, isInitDataLoadingFinished]); }; diff --git a/src/utils/parseQueryToTypes.test.ts b/src/utils/parseQueryToTypes.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..8151e0c92e2ff939d747db75b3bc68d62f2ec2e0 --- /dev/null +++ b/src/utils/parseQueryToTypes.test.ts @@ -0,0 +1,28 @@ +import { parseQueryToTypes } from './parseQueryToTypes'; + +describe('parseQueryToTypes', () => { + it('should return valid data', () => { + expect({}).toEqual({}); + + expect(parseQueryToTypes({ modelId: '666' })).toEqual({ + modelId: 666, + backgroundId: undefined, + initialPosition: { x: undefined, y: undefined, z: undefined }, + }); + expect(parseQueryToTypes({ x: '2137' })).toEqual({ + modelId: undefined, + backgroundId: undefined, + initialPosition: { x: 2137, y: undefined, z: undefined }, + }); + expect(parseQueryToTypes({ y: '1372' })).toEqual({ + modelId: undefined, + backgroundId: undefined, + initialPosition: { x: undefined, y: 1372, z: undefined }, + }); + expect(parseQueryToTypes({ z: '3721' })).toEqual({ + modelId: undefined, + backgroundId: undefined, + initialPosition: { x: undefined, y: undefined, z: 3721 }, + }); + }); +}); diff --git a/src/utils/parseQueryToTypes.ts b/src/utils/parseQueryToTypes.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1b3f297cb084fdc71c10e98798df7edbc5c084e --- /dev/null +++ b/src/utils/parseQueryToTypes.ts @@ -0,0 +1,11 @@ +import { QueryData, QueryDataRouterParams } from '@/types/query'; + +export const parseQueryToTypes = (query: QueryDataRouterParams): QueryData => ({ + modelId: Number(query.modelId) || undefined, + backgroundId: Number(query.backgroundId) || undefined, + initialPosition: { + x: Number(query.x) || undefined, + y: Number(query.y) || undefined, + z: Number(query.z) || undefined, + }, +}); diff --git a/src/utils/query-manager/useReduxBusQueryManager.test.ts b/src/utils/query-manager/useReduxBusQueryManager.test.ts index c79d8bc9201ac9e75e1520ef98139ace4a79e959..dc4bc4b6595a1966de2cabb7c3a54143e033ef42 100644 --- a/src/utils/query-manager/useReduxBusQueryManager.test.ts +++ b/src/utils/query-manager/useReduxBusQueryManager.test.ts @@ -10,7 +10,7 @@ describe('useReduxBusQueryManager - util', () => { const { Wrapper } = getReduxWrapperWithStore(); jest.mock('./../../redux/root/init.selectors', () => ({ - initDataLoadingFinished: jest.fn().mockImplementation(() => false), + initDataAndMapLoadingFinished: jest.fn().mockImplementation(() => false), })); it('should not update query', () => { diff --git a/src/utils/query-manager/useReduxBusQueryManager.ts b/src/utils/query-manager/useReduxBusQueryManager.ts index 4ad04c417f2044f420782544930cffada60388b9..80d277dd03a6954af2085dbc98fe7c75cf169663 100644 --- a/src/utils/query-manager/useReduxBusQueryManager.ts +++ b/src/utils/query-manager/useReduxBusQueryManager.ts @@ -2,12 +2,12 @@ import { queryDataParamsSelector } from '@/redux/root/query.selectors'; import { useRouter } from 'next/router'; import { useCallback, useEffect } from 'react'; import { useSelector } from 'react-redux'; -import { initDataLoadingFinished } from '../../redux/root/init.selectors'; +import { initDataAndMapLoadingFinished } from '../../redux/root/init.selectors'; export const useReduxBusQueryManager = (): void => { const router = useRouter(); const queryData = useSelector(queryDataParamsSelector); - const isDataLoaded = useSelector(initDataLoadingFinished); + const isDataLoaded = useSelector(initDataAndMapLoadingFinished); const handleChangeQuery = useCallback( () =>