diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx index 7e6142984a4a9887d16fa281dec26a75695f8f41..6ee65f6b44eeb7c078b7d7fc0250928f933db92f 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx @@ -1,26 +1,37 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { DrawerHeading } from '@/shared/DrawerHeading'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { layersSelector, layersVisibilitySelector } from '@/redux/layers/layers.selectors'; +import { + layersForCurrentModelSelector, + layersVisibilityForCurrentModelSelector, +} from '@/redux/layers/layers.selectors'; import { Switch } from '@/shared/Switch'; import { setLayerVisibility } from '@/redux/layers/layers.slice'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; export const LayersDrawer = (): JSX.Element => { - const layers = useAppSelector(layersSelector); - const layersVisibility = useAppSelector(layersVisibilitySelector); + const layersForCurrentModel = useAppSelector(layersForCurrentModelSelector); + const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector); + const currentModelId = useAppSelector(currentModelIdSelector); const dispatch = useAppDispatch(); return ( <div data-testid="layers-drawer" className="h-full max-h-full"> <DrawerHeading title="Layers" /> <div className="flex h-[calc(100%-93px)] max-h-[calc(100%-93px)] flex-col overflow-y-auto px-6"> - {layers.map(layer => ( + {layersForCurrentModel.map(layer => ( <div key={layer.details.id} className="flex items-center justify-between border-b p-4"> <h1>{layer.details.name}</h1> <Switch - isChecked={layersVisibility[layer.details.layerId]} + isChecked={layersVisibilityForCurrentModel[layer.details.layerId]} onToggle={value => - dispatch(setLayerVisibility({ visible: value, layerId: layer.details.layerId })) + dispatch( + setLayerVisibility({ + modelId: currentModelId, + visible: value, + layerId: layer.details.layerId, + }), + ) } /> </div> diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts index 05993a8106fe4592d10b1175ac04feab5979c751..444cf47bc8ed2d4bfccf05076b7536f9a1a1b7ac 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/useOlMapVectorListeners.ts @@ -14,8 +14,8 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { Coordinate } from 'ol/coordinate'; import { Pixel } from 'ol/pixel'; import { onMapRightClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseRightClick/onMapRightClick'; -import { modelElementsSelector } from '@/redux/modelElements/modelElements.selector'; -import { newReactionsDataSelector } from '@/redux/newReactions/newReactions.selectors'; +import { modelElementsForCurrentModelSelector } from '@/redux/modelElements/modelElements.selector'; +import { newReactionsForCurrentModelSelector } from '@/redux/newReactions/newReactions.selectors'; interface UseOlMapVectorListenersInput { mapInstance: MapInstance; @@ -25,8 +25,8 @@ export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapVectorListeners const mapSize = useSelector(mapDataSizeSelector); const modelId = useSelector(currentModelIdSelector); const isResultDrawerOpen = useSelector(resultDrawerOpen); - const modelElements = useSelector(modelElementsSelector); - const reactions = useSelector(newReactionsDataSelector); + const modelElementsForCurrentModel = useSelector(modelElementsForCurrentModelSelector); + const newReactionsForCurrentModel = useSelector(newReactionsForCurrentModelSelector); const dispatch = useAppDispatch(); const coordinate = useRef<Coordinate>([]); const pixel = useRef<Pixel>([]); @@ -41,15 +41,21 @@ export const useOlMapVectorListeners = ({ mapInstance }: UseOlMapVectorListeners dispatch, isResultDrawerOpen, comments, - modelElements?.content || [], - reactions, + modelElementsForCurrentModel || [], + newReactionsForCurrentModel, ), OPTIONS.clickPersistTime, { leading: false }, ); const handleRightClick = useDebouncedCallback( - onMapRightClick(mapSize, modelId, dispatch, modelElements?.content || [], reactions), + onMapRightClick( + mapSize, + modelId, + dispatch, + modelElementsForCurrentModel || [], + newReactionsForCurrentModel, + ), OPTIONS.clickPersistTime, { leading: false, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index 98a4276aea101f2916ace06afc65d05e5c1c356f..b37cffdaf794171804473a8a0dc3c438c5350b3a 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -6,14 +6,19 @@ import { useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { getLayers } from '@/redux/layers/layers.thunks'; -import { layersSelector, layersVisibilitySelector } from '@/redux/layers/layers.selectors'; +import { getLayersForModel } from '@/redux/layers/layers.thunks'; +import { + layersForCurrentModelSelector, + layersLoadingSelector, + layersVisibilityForCurrentModelSelector, +} from '@/redux/layers/layers.selectors'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { MapInstance } from '@/types/map'; import { LineString, MultiPolygon, Point } from 'ol/geom'; import Polygon from 'ol/geom/Polygon'; import Layer from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer'; import { arrowTypesSelector, lineTypesSelector } from '@/redux/shapes/shapes.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; export const useOlMapAdditionalLayers = ( mapInstance: MapInstance, @@ -24,18 +29,26 @@ export const useOlMapAdditionalLayers = ( > => { const dispatch = useAppDispatch(); const currentModelId = useSelector(currentModelIdSelector); - const mapLayers = useSelector(layersSelector); - const layersVisibility = useSelector(layersVisibilitySelector); + + const layersForCurrentModel = useAppSelector(layersForCurrentModelSelector); + const layersLoading = useAppSelector(layersLoadingSelector); + const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector); + const lineTypes = useSelector(lineTypesSelector); const arrowTypes = useSelector(arrowTypesSelector); const pointToProjection = usePointToProjection(); useEffect(() => { - dispatch(getLayers(currentModelId)); - }, [currentModelId, dispatch]); + if (!currentModelId) { + return; + } + if (!['succeeded', 'pending'].includes(layersLoading)) { + dispatch(getLayersForModel(currentModelId)); + } + }, [currentModelId, dispatch, layersLoading]); const vectorLayers = useMemo(() => { - return mapLayers.map(layer => { + return layersForCurrentModel.map(layer => { const additionalLayer = new Layer({ texts: layer.texts, rects: layer.rects, @@ -50,16 +63,16 @@ export const useOlMapAdditionalLayers = ( }); return additionalLayer.vectorLayer; }); - }, [arrowTypes, lineTypes, mapInstance, mapLayers, pointToProjection]); + }, [arrowTypes, lineTypes, mapInstance, layersForCurrentModel, pointToProjection]); useEffect(() => { vectorLayers.forEach(layer => { const layerId = layer.get('id'); - if (layerId && layersVisibility[layerId] !== undefined) { - layer.setVisible(layersVisibility[layerId]); + if (layerId && layersVisibilityForCurrentModel[layerId] !== undefined) { + layer.setVisible(layersVisibilityForCurrentModel[layerId]); } }); - }, [layersVisibility, vectorLayers]); + }, [layersVisibilityForCurrentModel, vectorLayers]); return vectorLayers; }; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts index c92820ed42c913c231fa006e2177da80b9f06490..2e5bbf9969a67b00336a3c48105ec32636cdcc00 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts @@ -1,4 +1,4 @@ -import { ModelElement, ModelElements } from '@/types/models'; +import { ModelElement } from '@/types/models'; import MapElement from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/MapElement'; import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle'; import CompartmentSquare from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare'; @@ -18,7 +18,7 @@ import VectorSource from 'ol/source/Vector'; import { MapSize } from '@/redux/map/map.types'; export default function processModelElements( - modelElements: ModelElements, + modelElements: Array<ModelElement>, shapes: BioShapesDict, lineTypes: LineTypeDict, groupedElementsOverlays: Record<string, Array<OverlayBioEntityRender>>, @@ -34,7 +34,7 @@ export default function processModelElements( const validElements: Array< MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph > = []; - modelElements.content.forEach((element: ModelElement) => { + modelElements.forEach((element: ModelElement) => { if (element.glyph) { const glyph = new Glyph({ elementId: element.id, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index f33900f80e071eff12c9f7c91ea574709fc7dffa..6f2aafa1406a548c7b9e4a8199b30e783c552015 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -12,17 +12,23 @@ import { lineTypesSelector, } from '@/redux/shapes/shapes.selectors'; import { MapInstance } from '@/types/map'; -import { modelElementsSelector } from '@/redux/modelElements/modelElements.selector'; +import { + modelElementsForCurrentModelSelector, + modelElementsLoadingSelector, +} from '@/redux/modelElements/modelElements.selector'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; -import { getModelElements } from '@/redux/modelElements/modelElements.thunks'; +import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import CompartmentSquare from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentSquare'; import CompartmentCircle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentCircle'; import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph'; import CompartmentPathway from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/CompartmentPathway'; import Reaction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/reaction/Reaction'; -import { newReactionsDataSelector } from '@/redux/newReactions/newReactions.selectors'; -import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; +import { + newReactionsForCurrentModelSelector, + newReactionsLoadingSelector, +} from '@/redux/newReactions/newReactions.selectors'; +import { getNewReactionsForModel } from '@/redux/newReactions/newReactions.thunks'; import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import { getOverlayOrderSelector, @@ -41,6 +47,7 @@ import useDebouncedValue from '@/utils/useDebouncedValue'; import { mapBackgroundTypeSelector, mapDataSizeSelector } from '@/redux/map/map.selectors'; import MapBackgroundsEnum from '@/redux/map/map.enums'; import { setMapBackgroundType } from '@/redux/map/map.slice'; +import { ZOOM_RESCALING_FACTOR } from '@/constants/map'; export const useOlMapReactionsLayer = ({ mapInstance, @@ -50,8 +57,6 @@ export const useOlMapReactionsLayer = ({ const dispatch = useAppDispatch(); const currentModelId = useSelector(currentModelIdSelector); - const modelElements = useSelector(modelElementsSelector); - const modelReactions = useSelector(newReactionsDataSelector); const shapes = useSelector(bioShapesSelector); const mapSize = useSelector(mapDataSizeSelector); const lineTypes = useSelector(lineTypesSelector); @@ -61,6 +66,11 @@ export const useOlMapReactionsLayer = ({ const currentMarkers = useAppSelector(markersSufraceOfCurrentMapDataSelector); const markersRender = parseSurfaceMarkersToBioEntityRender(currentMarkers); const bioEntities = useAppSelector(overlayBioEntitiesForCurrentModelSelector); + const reactionsForCurrentModel = useAppSelector(newReactionsForCurrentModelSelector); + const modelElementsLoading = useAppSelector(modelElementsLoadingSelector); + const reactionsLoading = useAppSelector(newReactionsLoadingSelector); + + const modelElementsForCurrentModel = useAppSelector(modelElementsForCurrentModelSelector); const debouncedBioEntities = useDebouncedValue(bioEntities, 1000); const { getOverlayBioEntityColorByAvailableProperties } = useGetOverlayColor(); @@ -68,12 +78,23 @@ export const useOlMapReactionsLayer = ({ const vectorSource = useMemo(() => new VectorSource(), []); + const mapModelOriginalMaxZoom = mapInstance?.getView().get('originalMaxZoom'); + + const isCorrectMapInstanceViewScale = useMemo(() => { + return mapSize.maxZoom * ZOOM_RESCALING_FACTOR === mapModelOriginalMaxZoom; + }, [mapModelOriginalMaxZoom, mapSize.maxZoom]); + useEffect(() => { - if (currentModelId) { - dispatch(getModelElements(currentModelId)); - dispatch(getNewReactions(currentModelId)); + if (!currentModelId) { + return; } - }, [currentModelId, dispatch]); + if (!['succeeded', 'pending'].includes(modelElementsLoading)) { + dispatch(getModelElementsForModel(currentModelId)); + } + if (!['succeeded', 'pending'].includes(reactionsLoading)) { + dispatch(getNewReactionsForModel(currentModelId)); + } + }, [currentModelId, dispatch, reactionsLoading, modelElementsLoading]); useEffect(() => { if (overlaysOrder.length) { @@ -94,6 +115,9 @@ export const useOlMapReactionsLayer = ({ }, [bioEntities]); const linesOverlaysFeatures = useMemo(() => { + if (!isCorrectMapInstanceViewScale) { + return []; + } return linesOverlays.map(lineOverlay => { return new LineOverlay({ lineOverlay, @@ -104,12 +128,16 @@ export const useOlMapReactionsLayer = ({ }); }, [ getOverlayBioEntityColorByAvailableProperties, + isCorrectMapInstanceViewScale, linesOverlays, mapInstance, pointToProjection, ]); const markerOverlaysFeatures = useMemo(() => { + if (!isCorrectMapInstanceViewScale) { + return []; + } return markersRender.map(marker => { return new MarkerOverlay({ markerOverlay: marker, @@ -120,13 +148,17 @@ export const useOlMapReactionsLayer = ({ }); }, [ getOverlayBioEntityColorByAvailableProperties, + isCorrectMapInstanceViewScale, mapInstance, markersRender, pointToProjection, ]); const reactions = useMemo(() => { - return modelReactions.map(reaction => { + if (!reactionsForCurrentModel || !isCorrectMapInstanceViewScale) { + return []; + } + return reactionsForCurrentModel.map(reaction => { const reactionShapes = shapes && shapes[reaction.sboTerm]; if (!reactionShapes) { return []; @@ -148,16 +180,25 @@ export const useOlMapReactionsLayer = ({ }); return reactionObject.features; }); - }, [arrowTypes, lineTypes, mapInstance, modelReactions, pointToProjection, shapes, vectorSource]); + }, [ + reactionsForCurrentModel, + isCorrectMapInstanceViewScale, + shapes, + lineTypes, + arrowTypes, + pointToProjection, + vectorSource, + mapInstance, + ]); const elements: Array< MapElement | CompartmentCircle | CompartmentSquare | CompartmentPathway | Glyph > = useMemo(() => { - if (!modelElements || !shapes) { + if (!modelElementsForCurrentModel || !shapes || !isCorrectMapInstanceViewScale) { return []; } return processModelElements( - modelElements, + modelElementsForCurrentModel, shapes, lineTypes, groupedElementsOverlays, @@ -170,8 +211,9 @@ export const useOlMapReactionsLayer = ({ mapSize, ); }, [ - modelElements, + modelElementsForCurrentModel, shapes, + isCorrectMapInstanceViewScale, lineTypes, groupedElementsOverlays, overlaysOrder, diff --git a/src/components/Map/MapViewer/utils/config/useOlMapView.ts b/src/components/Map/MapViewer/utils/config/useOlMapView.ts index 77d8be74b8f7cf8df6af8885821b0678fb5bcfb6..a639ebd1dfe90648dfab7edc89855b406a6d565b 100644 --- a/src/components/Map/MapViewer/utils/config/useOlMapView.ts +++ b/src/components/Map/MapViewer/utils/config/useOlMapView.ts @@ -65,16 +65,7 @@ export const useOlMapView = ({ mapInstance }: UseOlMapViewInput): MapConfig['vie minZoom: mapSize.minZoom * ZOOM_RESCALING_FACTOR, extent, }), - [ - center, - mapInitialPosition.z, - mapSize.width, - mapSize.tileSize, - mapSize.height, - mapSize.maxZoom, - mapSize.minZoom, - extent, - ], + [center, mapInitialPosition.z, mapSize, extent], ); const view = useMemo(() => new View(viewConfig), [viewConfig]); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 5346ef4d1bb1aa5ae919214414dfc9cdd22b2891..c0686ae35bf32cb34b17c4071dee0d10e71663e0 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -44,7 +44,7 @@ export const apiPath = { getChemicalsStringWithColumnsTarget: (columns: string, target: string): string => `projects/${PROJECT_ID}/chemicals:search?columns=${columns}&target=${target}`, getModelsString: (): string => `projects/${PROJECT_ID}/models/`, - getModelElements: (modelId: number): string => + getModelElementsForModel: (modelId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/bioEntities/elements/?size=10000`, getShapes: (): string => `projects/${PROJECT_ID}/shapes/`, getLineTypes: (): string => `projects/${PROJECT_ID}/lineTypes/`, @@ -60,8 +60,8 @@ export const apiPath = { `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/lines/`, getGlyphImage: (glyphId: number): string => `projects/${PROJECT_ID}/glyphs/${glyphId}/fileContent`, - getNewReactions: (modelId: number): string => - `projects/${PROJECT_ID}/maps/${modelId}/bioEntities/reactions/?size=2000`, + getNewReactionsForModel: (modelId: number): string => + `projects/${PROJECT_ID}/maps/${modelId}/bioEntities/reactions/?size=10000`, getNewReaction: (modelId: number, reactionId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/bioEntities/reactions/${reactionId}`, getChemicalsStringWithQuery: (searchQuery: string): string => diff --git a/src/redux/layers/layers.mock.ts b/src/redux/layers/layers.mock.ts index 9ec2ce4c48696d6c1b75c008bfcc2f1624475ebd..38e72675c3f471a4cc7033d2013eced09d87b0f9 100644 --- a/src/redux/layers/layers.mock.ts +++ b/src/redux/layers/layers.mock.ts @@ -1,7 +1,10 @@ -import { LayersState } from '@/redux/layers/layers.types'; +import { LayersState, LayersVisibilitiesState } from '@/redux/layers/layers.types'; import { DEFAULT_ERROR } from '@/constants/errors'; +import { FetchDataState } from '@/types/fetchDataState'; -export const LAYERS_STATE_INITIAL_MOCK: LayersState = { +export const LAYERS_STATE_INITIAL_MOCK: LayersState = {}; + +export const LAYERS_STATE_INITIAL_LAYER_MOCK: FetchDataState<LayersVisibilitiesState> = { data: { layers: [], layersVisibility: {}, @@ -9,3 +12,8 @@ export const LAYERS_STATE_INITIAL_MOCK: LayersState = { loading: 'idle', error: DEFAULT_ERROR, }; + +export const LAYER_STATE_DEFAULT_DATA = { + layers: [], + layersVisibility: {}, +}; diff --git a/src/redux/layers/layers.reducers.test.ts b/src/redux/layers/layers.reducers.test.ts index 827c1dff5f1b500b5d59fd70c2cf14581c4dec40..ffc0d92384518dde4ef3e126133e30568e74c220 100644 --- a/src/redux/layers/layers.reducers.test.ts +++ b/src/redux/layers/layers.reducers.test.ts @@ -7,8 +7,8 @@ import { import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; import { HttpStatusCode } from 'axios'; import { unwrapResult } from '@reduxjs/toolkit'; -import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; -import { getLayers } from '@/redux/layers/layers.thunks'; +import { LAYER_STATE_DEFAULT_DATA, LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; +import { getLayersForModel } from '@/redux/layers/layers.thunks'; import { layersFixture } from '@/models/fixtures/layersFixture'; import { layerTextsFixture } from '@/models/fixtures/layerTextsFixture'; import { layerRectsFixture } from '@/models/fixtures/layerRectsFixture'; @@ -48,8 +48,8 @@ describe('layers reducer', () => { .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id)) .reply(HttpStatusCode.Ok, layerLinesFixture); - const { type } = await store.dispatch(getLayers(1)); - const { data, loading, error } = store.getState().layers; + const { type } = await store.dispatch(getLayersForModel(1)); + const { data, loading, error } = store.getState().layers[1]; expect(type).toBe('vectorMap/getLayers/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); @@ -72,8 +72,8 @@ describe('layers reducer', () => { it('should update store after failed getLayers query', async () => { mockedAxiosClient.onGet(apiPath.getLayers(1)).reply(HttpStatusCode.NotFound, []); - const action = await store.dispatch(getLayers(1)); - const { data, loading, error } = store.getState().layers; + const action = await store.dispatch(getLayersForModel(1)); + const { data, loading, error } = store.getState().layers[1]; expect(action.type).toBe('vectorMap/getLayers/rejected'); expect(() => unwrapResult(action)).toThrow( @@ -99,14 +99,14 @@ describe('layers reducer', () => { .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id)) .reply(HttpStatusCode.Ok, layerLinesFixture); - const layersPromise = store.dispatch(getLayers(1)); + const layersPromise = store.dispatch(getLayersForModel(1)); - const { data, loading } = store.getState().layers; - expect(data).toEqual({ layers: [], layersVisibility: {} }); + const { data, loading } = store.getState().layers[1]; + expect(data).toEqual(LAYER_STATE_DEFAULT_DATA); expect(loading).toEqual('pending'); layersPromise.then(() => { - const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().layers; + const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().layers[1]; expect(dataPromiseFulfilled).toEqual({ layers: [ diff --git a/src/redux/layers/layers.reducers.ts b/src/redux/layers/layers.reducers.ts index ed75a68735150b29e6a2fa96f61360853f5f7160..0cc1ad73bf6c4e7625d657791389ef5befe48fc5 100644 --- a/src/redux/layers/layers.reducers.ts +++ b/src/redux/layers/layers.reducers.ts @@ -1,30 +1,52 @@ /* eslint-disable no-magic-numbers */ import { ActionReducerMapBuilder, PayloadAction } from '@reduxjs/toolkit'; -import { getLayers } from '@/redux/layers/layers.thunks'; +import { getLayersForModel } from '@/redux/layers/layers.thunks'; import { LayersState } from '@/redux/layers/layers.types'; +import { + LAYER_STATE_DEFAULT_DATA, + LAYERS_STATE_INITIAL_LAYER_MOCK, +} from '@/redux/layers/layers.mock'; +import { DEFAULT_ERROR } from '@/constants/errors'; -export const getLayersReducer = (builder: ActionReducerMapBuilder<LayersState>): void => { - builder.addCase(getLayers.pending, state => { - state.loading = 'pending'; +export const getLayersForModelReducer = (builder: ActionReducerMapBuilder<LayersState>): void => { + builder.addCase(getLayersForModel.pending, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'pending'; + } else { + state[modelId] = { ...LAYERS_STATE_INITIAL_LAYER_MOCK, loading: 'pending' }; + } }); - builder.addCase(getLayers.fulfilled, (state, action) => { - state.data = action.payload || { - layers: [], - layersVisibility: {}, - }; - state.loading = 'succeeded'; + builder.addCase(getLayersForModel.fulfilled, (state, action) => { + const modelId = action.meta.arg; + const data = action.payload || { ...LAYER_STATE_DEFAULT_DATA }; + if (state[modelId]) { + state[modelId].data = data; + state[modelId].loading = 'succeeded'; + } else { + state[modelId] = { data, loading: 'pending', error: DEFAULT_ERROR }; + } }); - builder.addCase(getLayers.rejected, state => { - state.loading = 'failed'; + builder.addCase(getLayersForModel.rejected, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'failed'; + } else { + state[modelId] = { ...LAYERS_STATE_INITIAL_LAYER_MOCK, loading: 'failed' }; + } }); }; export const setLayerVisibilityReducer = ( state: LayersState, - action: PayloadAction<{ visible: boolean; layerId: string }>, + action: PayloadAction<{ modelId: number; visible: boolean; layerId: string }>, ): void => { - const { payload } = action; - if (state.data && state.data.layersVisibility[payload.layerId] !== undefined) { - state.data.layersVisibility[payload.layerId] = payload.visible; + const { modelId, visible, layerId } = action.payload; + const { data } = state[modelId]; + if (!data) { + return; + } + if (data.layersVisibility[layerId] !== undefined) { + data.layersVisibility[layerId] = visible; } }; diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts index d29698e0254a341147c7bbc16fe83456bb8ca7be..4d9e3f6d534b6891c8f9bc3eefbb18ecc6aedd03 100644 --- a/src/redux/layers/layers.selectors.ts +++ b/src/redux/layers/layers.selectors.ts @@ -1,14 +1,26 @@ import { createSelector } from '@reduxjs/toolkit'; import { rootSelector } from '@/redux/root/root.selectors'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; -export const layersSelector = createSelector( - rootSelector, - state => state.layers?.data?.layers || [], +export const layersSelector = createSelector(rootSelector, state => state.layers); + +export const layersStateForCurrentModelSelector = createSelector( + layersSelector, + currentModelIdSelector, + (state, currentModelId) => state[currentModelId], +); + +export const layersLoadingSelector = createSelector( + layersStateForCurrentModelSelector, + state => state?.loading, ); -export const layersLoadingSelector = createSelector(rootSelector, state => state.layers.loading); +export const layersVisibilityForCurrentModelSelector = createSelector( + layersStateForCurrentModelSelector, + state => state?.data?.layersVisibility || {}, +); -export const layersVisibilitySelector = createSelector( - rootSelector, - state => state.layers?.data?.layersVisibility || {}, +export const layersForCurrentModelSelector = createSelector( + layersStateForCurrentModelSelector, + state => state?.data?.layers || [], ); diff --git a/src/redux/layers/layers.slice.ts b/src/redux/layers/layers.slice.ts index 47da06b01b1205dcdf422265bcf36e660f311fd3..7c07cdc0a77d1b7cd9a912a0bccee08970f1e790 100644 --- a/src/redux/layers/layers.slice.ts +++ b/src/redux/layers/layers.slice.ts @@ -1,6 +1,9 @@ import { createSlice } from '@reduxjs/toolkit'; import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; -import { getLayersReducer, setLayerVisibilityReducer } from '@/redux/layers/layers.reducers'; +import { + getLayersForModelReducer, + setLayerVisibilityReducer, +} from '@/redux/layers/layers.reducers'; export const layersSlice = createSlice({ name: 'layers', @@ -9,7 +12,7 @@ export const layersSlice = createSlice({ setLayerVisibility: setLayerVisibilityReducer, }, extraReducers: builder => { - getLayersReducer(builder); + getLayersForModelReducer(builder); }, }); diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts index a6c0018672be6bcdad62f83e1464ea8f3fbe2d60..bdc626d56da950187f9c9d5f57970e12e8a9597b 100644 --- a/src/redux/layers/layers.thunks.test.ts +++ b/src/redux/layers/layers.thunks.test.ts @@ -7,7 +7,7 @@ import { import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; import { HttpStatusCode } from 'axios'; import { LayersState } from '@/redux/layers/layers.types'; -import { getLayers } from '@/redux/layers/layers.thunks'; +import { getLayersForModel } from '@/redux/layers/layers.thunks'; import { layersFixture } from '@/models/fixtures/layersFixture'; import { layerTextsFixture } from '@/models/fixtures/layerTextsFixture'; import { layerRectsFixture } from '@/models/fixtures/layerRectsFixture'; @@ -23,7 +23,7 @@ describe('layers thunks', () => { store = createStoreInstanceUsingSliceReducer('layers', layersReducer); }); - describe('getLayers', () => { + describe('getLayersForModel', () => { it('should return data when data response from API is valid', async () => { mockedAxiosClient.onGet(apiPath.getLayers(1)).reply(HttpStatusCode.Ok, layersFixture); mockedAxiosClient @@ -39,7 +39,7 @@ describe('layers thunks', () => { .onGet(apiPath.getLayerLines(1, layersFixture.content[0].id)) .reply(HttpStatusCode.Ok, layerLinesFixture); - const { payload } = await store.dispatch(getLayers(1)); + const { payload } = await store.dispatch(getLayersForModel(1)); expect(payload).toEqual({ layers: [ { @@ -61,7 +61,7 @@ describe('layers thunks', () => { .onGet(apiPath.getLayers(1)) .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); - const { payload } = await store.dispatch(getLayers(1)); + const { payload } = await store.dispatch(getLayersForModel(1)); expect(payload).toEqual(undefined); }); }); diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts index 9aa71e8301dcc714a7e52922d71656275e412887..0f38e3d1256a8c2d0ec34581cee2fb129cddfc74 100644 --- a/src/redux/layers/layers.thunks.ts +++ b/src/redux/layers/layers.thunks.ts @@ -15,51 +15,52 @@ import { pageableSchema } from '@/models/pageableSchema'; import { layerOvalSchema } from '@/models/layerOvalSchema'; import { layerLineSchema } from '@/models/layerLineSchema'; -export const getLayers = createAsyncThunk<LayersVisibilitiesState | undefined, number, ThunkConfig>( - 'vectorMap/getLayers', - async (modelId: number) => { - try { - const { data } = await axiosInstanceNewAPI.get<Layers>(apiPath.getLayers(modelId)); - const isDataValid = validateDataUsingZodSchema(data, pageableSchema(layerSchema)); - if (!isDataValid) { - return undefined; - } - let layers = await Promise.all( - data.content.map(async (layer: Layer) => { - const [textsResponse, rectsResponse, ovalsResponse, linesResponse] = await Promise.all([ - axiosInstanceNewAPI.get(apiPath.getLayerTexts(modelId, layer.id)), - axiosInstanceNewAPI.get(apiPath.getLayerRects(modelId, layer.id)), - axiosInstanceNewAPI.get(apiPath.getLayerOvals(modelId, layer.id)), - axiosInstanceNewAPI.get(apiPath.getLayerLines(modelId, layer.id)), - ]); +export const getLayersForModel = createAsyncThunk< + LayersVisibilitiesState | undefined, + number, + ThunkConfig +>('vectorMap/getLayers', async (modelId: number) => { + try { + const { data } = await axiosInstanceNewAPI.get<Layers>(apiPath.getLayers(modelId)); + const isDataValid = validateDataUsingZodSchema(data, pageableSchema(layerSchema)); + if (!isDataValid) { + return undefined; + } + let layers = await Promise.all( + data.content.map(async (layer: Layer) => { + const [textsResponse, rectsResponse, ovalsResponse, linesResponse] = await Promise.all([ + axiosInstanceNewAPI.get(apiPath.getLayerTexts(modelId, layer.id)), + axiosInstanceNewAPI.get(apiPath.getLayerRects(modelId, layer.id)), + axiosInstanceNewAPI.get(apiPath.getLayerOvals(modelId, layer.id)), + axiosInstanceNewAPI.get(apiPath.getLayerLines(modelId, layer.id)), + ]); - return { - details: layer, - texts: textsResponse.data.content, - rects: rectsResponse.data.content, - ovals: ovalsResponse.data.content, - lines: linesResponse.data.content, - }; - }), + return { + details: layer, + texts: textsResponse.data.content, + rects: rectsResponse.data.content, + ovals: ovalsResponse.data.content, + lines: linesResponse.data.content, + }; + }), + ); + layers = layers.filter(layer => { + return ( + z.array(layerTextSchema).safeParse(layer.texts).success && + z.array(layerRectSchema).safeParse(layer.rects).success && + z.array(layerOvalSchema).safeParse(layer.ovals).success && + z.array(layerLineSchema).safeParse(layer.lines).success ); - layers = layers.filter(layer => { - return ( - z.array(layerTextSchema).safeParse(layer.texts).success && - z.array(layerRectSchema).safeParse(layer.rects).success && - z.array(layerOvalSchema).safeParse(layer.ovals).success && - z.array(layerLineSchema).safeParse(layer.lines).success - ); - }); - const layersVisibility = layers.reduce((acc: { [key: string]: boolean }, layer) => { - acc[layer.details.layerId] = layer.details.visible; - return acc; - }, {}); - return { - layers, - layersVisibility, - }; - } catch (error) { - return Promise.reject(getError({ error, prefix: LAYERS_FETCHING_ERROR_PREFIX })); - } - }, -); + }); + const layersVisibility = layers.reduce((acc: { [key: string]: boolean }, layer) => { + acc[layer.details.layerId] = layer.details.visible; + return acc; + }, {}); + return { + layers, + layersVisibility, + }; + } catch (error) { + return Promise.reject(getError({ error, prefix: LAYERS_FETCHING_ERROR_PREFIX })); + } +}); diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts index 636376906b991185f851a8ec1b1550b7dcdab440..c00d6a7e63889b392919154ee1be634e5ed6f90e 100644 --- a/src/redux/layers/layers.types.ts +++ b/src/redux/layers/layers.types.ts @@ -1,4 +1,4 @@ -import { FetchDataState } from '@/types/fetchDataState'; +import { KeyedFetchDataState } from '@/types/fetchDataState'; import { Layer, LayerLine, LayerOval, LayerRect, LayerText } from '@/types/models'; export type LayerState = { @@ -18,4 +18,4 @@ export type LayersVisibilitiesState = { layers: LayerState[]; }; -export type LayersState = FetchDataState<LayersVisibilitiesState>; +export type LayersState = KeyedFetchDataState<LayersVisibilitiesState>; diff --git a/src/redux/modelElements/modelElements.mock.ts b/src/redux/modelElements/modelElements.mock.ts index 5ad63a0231db2dc6257cfa4fede7e11fa8ec65b8..583f03f397d76cd5f9e857f1f46cb64d8e2d6614 100644 --- a/src/redux/modelElements/modelElements.mock.ts +++ b/src/redux/modelElements/modelElements.mock.ts @@ -1,8 +1,12 @@ import { DEFAULT_ERROR } from '@/constants/errors'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; +import { FetchDataState } from '@/types/fetchDataState'; +import { ModelElement } from '@/types/models'; -export const MODEL_ELEMENTS_INITIAL_STATE_MOCK: ModelElementsState = { - data: null, +export const MODEL_ELEMENTS_INITIAL_STATE_MOCK: ModelElementsState = {}; + +export const MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK: FetchDataState<Array<ModelElement>> = { + data: [], loading: 'idle', error: DEFAULT_ERROR, }; diff --git a/src/redux/modelElements/modelElements.reducers.test.ts b/src/redux/modelElements/modelElements.reducers.test.ts index d896072208c1657f6d40c7b5fdf352944bbd03a2..fc5c5a6fe505f8861e49553e2ace08268f6791eb 100644 --- a/src/redux/modelElements/modelElements.reducers.test.ts +++ b/src/redux/modelElements/modelElements.reducers.test.ts @@ -9,17 +9,11 @@ import { HttpStatusCode } from 'axios'; import { unwrapResult } from '@reduxjs/toolkit'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import modelElementsReducer from '@/redux/modelElements/modelElements.slice'; -import { getModelElements } from '@/redux/modelElements/modelElements.thunks'; +import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; import { modelElementsFixture } from '@/models/fixtures/modelElementsFixture'; const mockedAxiosClient = mockNetworkNewAPIResponse(); -const INITIAL_STATE: ModelElementsState = { - data: null, - loading: 'idle', - error: { name: '', message: '' }, -}; - describe('model elements reducer', () => { let store = {} as ToolkitStoreWithSingleSlice<ModelElementsState>; beforeEach(() => { @@ -29,54 +23,54 @@ describe('model elements reducer', () => { it('should match initial state', () => { const action = { type: 'unknown' }; - expect(modelElementsReducer(undefined, action)).toEqual(INITIAL_STATE); + expect(modelElementsReducer(undefined, action)).toEqual({}); }); - it('should update store after successful getModelElements query', async () => { + it('should update store after successful getModelElementsForModel query', async () => { mockedAxiosClient - .onGet(apiPath.getModelElements(0)) + .onGet(apiPath.getModelElementsForModel(0)) .reply(HttpStatusCode.Ok, modelElementsFixture); - const { type } = await store.dispatch(getModelElements(0)); - const { data, loading, error } = store.getState().modelElements; + const { type } = await store.dispatch(getModelElementsForModel(0)); + const { data, loading, error } = store.getState().modelElements[0]; - expect(type).toBe('vectorMap/getModelElements/fulfilled'); + expect(type).toBe('vectorMap/getModelElementsForModel/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual(modelElementsFixture); + expect(data).toEqual(modelElementsFixture.content); }); - it('should update store after failed getModelElements query', async () => { - mockedAxiosClient.onGet(apiPath.getModelElements(0)).reply(HttpStatusCode.NotFound, []); + it('should update store after failed getModelElementsForModel query', async () => { + mockedAxiosClient.onGet(apiPath.getModelElementsForModel(0)).reply(HttpStatusCode.NotFound, []); - const action = await store.dispatch(getModelElements(0)); - const { data, loading, error } = store.getState().modelElements; + const action = await store.dispatch(getModelElementsForModel(0)); + const { data, loading, error } = store.getState().modelElements[0]; - expect(action.type).toBe('vectorMap/getModelElements/rejected'); + expect(action.type).toBe('vectorMap/getModelElementsForModel/rejected'); expect(() => unwrapResult(action)).toThrow( "Failed to fetch model elements: The page you're looking for doesn't exist. Please verify the URL and try again.", ); expect(loading).toEqual('failed'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual(null); + expect(data).toEqual([]); }); - it('should update store on loading getModelElements query', async () => { + it('should update store on loading getModelElementsForModel query', async () => { mockedAxiosClient - .onGet(apiPath.getModelElements(0)) + .onGet(apiPath.getModelElementsForModel(0)) .reply(HttpStatusCode.Ok, modelElementsFixture); - const modelElementsPromise = store.dispatch(getModelElements(0)); + const modelElementsPromise = store.dispatch(getModelElementsForModel(0)); - const { data, loading } = store.getState().modelElements; - expect(data).toEqual(null); + const { data, loading } = store.getState().modelElements[0]; + expect(data).toEqual([]); expect(loading).toEqual('pending'); modelElementsPromise.then(() => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = - store.getState().modelElements; + store.getState().modelElements[0]; - expect(dataPromiseFulfilled).toEqual(modelElementsFixture); + expect(dataPromiseFulfilled).toEqual(modelElementsFixture.content); expect(promiseFulfilled).toEqual('succeeded'); }); }); diff --git a/src/redux/modelElements/modelElements.reducers.ts b/src/redux/modelElements/modelElements.reducers.ts index fda618dfa3442ea5c0029e2db6bc2a3b6855edaa..777295cf15053679c1d33127f58f2fbc8babc2b2 100644 --- a/src/redux/modelElements/modelElements.reducers.ts +++ b/src/redux/modelElements/modelElements.reducers.ts @@ -1,18 +1,36 @@ import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; -import { getModelElements } from '@/redux/modelElements/modelElements.thunks'; +import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; +import { MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK } from '@/redux/modelElements/modelElements.mock'; +import { DEFAULT_ERROR } from '@/constants/errors'; export const getModelElementsReducer = ( builder: ActionReducerMapBuilder<ModelElementsState>, ): void => { - builder.addCase(getModelElements.pending, state => { - state.loading = 'pending'; + builder.addCase(getModelElementsForModel.pending, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'pending'; + } else { + state[modelId] = { ...MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK, loading: 'pending' }; + } }); - builder.addCase(getModelElements.fulfilled, (state, action) => { - state.data = action.payload || null; - state.loading = 'succeeded'; + builder.addCase(getModelElementsForModel.fulfilled, (state, action) => { + const modelId = action.meta.arg; + const data = action.payload || []; + if (state[modelId]) { + state[modelId].data = data; + state[modelId].loading = 'succeeded'; + } else { + state[modelId] = { data, loading: 'pending', error: DEFAULT_ERROR }; + } }); - builder.addCase(getModelElements.rejected, state => { - state.loading = 'failed'; + builder.addCase(getModelElementsForModel.rejected, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'failed'; + } else { + state[modelId] = { ...MODEL_ELEMENTS_STATE_INITIAL_MODEL_MOCK, loading: 'failed' }; + } }); }; diff --git a/src/redux/modelElements/modelElements.selector.ts b/src/redux/modelElements/modelElements.selector.ts index 3a9d8b54a29cabdda042e2ec63a3eb0c7465472d..4be70ab40f652e4c887e0e97714818be179df35d 100644 --- a/src/redux/modelElements/modelElements.selector.ts +++ b/src/redux/modelElements/modelElements.selector.ts @@ -1,12 +1,21 @@ import { createSelector } from '@reduxjs/toolkit'; import { rootSelector } from '@/redux/root/root.selectors'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; -export const modelElementsSelector = createSelector( - rootSelector, - state => state.modelElements.data, +export const modelElementsSelector = createSelector(rootSelector, state => state.modelElements); + +export const modelElementsStateForCurrentModelSelector = createSelector( + modelElementsSelector, + currentModelIdSelector, + (state, currentModelId) => state[currentModelId], ); export const modelElementsLoadingSelector = createSelector( - rootSelector, - state => state.modelElements.loading, + modelElementsStateForCurrentModelSelector, + state => state?.loading, +); + +export const modelElementsForCurrentModelSelector = createSelector( + modelElementsStateForCurrentModelSelector, + state => state?.data || [], ); diff --git a/src/redux/modelElements/modelElements.slice.ts b/src/redux/modelElements/modelElements.slice.ts index b9e7f4119ab512ee9327b6f82854d42b2cd308f1..cbf16d002fe05a7ef90c2334d844106da209d4a5 100644 --- a/src/redux/modelElements/modelElements.slice.ts +++ b/src/redux/modelElements/modelElements.slice.ts @@ -1,16 +1,10 @@ import { createSlice } from '@reduxjs/toolkit'; import { getModelElementsReducer } from '@/redux/modelElements/modelElements.reducers'; -import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; - -const initialState: ModelElementsState = { - data: null, - loading: 'idle', - error: { name: '', message: '' }, -}; +import { MODEL_ELEMENTS_INITIAL_STATE_MOCK } from '@/redux/modelElements/modelElements.mock'; export const modelElements = createSlice({ name: 'modelElements', - initialState, + initialState: MODEL_ELEMENTS_INITIAL_STATE_MOCK, reducers: {}, extraReducers: builder => { getModelElementsReducer(builder); diff --git a/src/redux/modelElements/modelElements.thunks.test.ts b/src/redux/modelElements/modelElements.thunks.test.ts index c5611de7faa4a7de5420a58d5450111a9f0e373d..0ba825c9f3d9a31f2d6f1d1e852d57fddc79464c 100644 --- a/src/redux/modelElements/modelElements.thunks.test.ts +++ b/src/redux/modelElements/modelElements.thunks.test.ts @@ -8,7 +8,7 @@ import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; import { HttpStatusCode } from 'axios'; import { ModelElementsState } from '@/redux/modelElements/modelElements.types'; import modelElementsReducer from '@/redux/modelElements/modelElements.slice'; -import { getModelElements } from '@/redux/modelElements/modelElements.thunks'; +import { getModelElementsForModel } from '@/redux/modelElements/modelElements.thunks'; import { modelElementsFixture } from '@/models/fixtures/modelElementsFixture'; const mockedAxiosClient = mockNetworkNewAPIResponse(); @@ -19,22 +19,22 @@ describe('model elements thunks', () => { store = createStoreInstanceUsingSliceReducer('modelElements', modelElementsReducer); }); - describe('getModelElements', () => { + describe('getModelElementsForModel', () => { it('should return data when data response from API is valid', async () => { mockedAxiosClient - .onGet(apiPath.getModelElements(0)) + .onGet(apiPath.getModelElementsForModel(0)) .reply(HttpStatusCode.Ok, modelElementsFixture); - const { payload } = await store.dispatch(getModelElements(0)); - expect(payload).toEqual(modelElementsFixture); + const { payload } = await store.dispatch(getModelElementsForModel(0)); + expect(payload).toEqual(modelElementsFixture.content); }); it('should return undefined when data response from API is not valid ', async () => { mockedAxiosClient - .onGet(apiPath.getModelElements(0)) + .onGet(apiPath.getModelElementsForModel(0)) .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); - const { payload } = await store.dispatch(getModelElements(0)); + const { payload } = await store.dispatch(getModelElementsForModel(0)); expect(payload).toEqual(undefined); }); }); diff --git a/src/redux/modelElements/modelElements.thunks.ts b/src/redux/modelElements/modelElements.thunks.ts index 2c7e2f25a2b850d845d923c99e191a1f29b60b1c..210b65600cac6aeddb439298bb82534696302d55 100644 --- a/src/redux/modelElements/modelElements.thunks.ts +++ b/src/redux/modelElements/modelElements.thunks.ts @@ -4,25 +4,23 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { ThunkConfig } from '@/types/store'; import { getError } from '@/utils/error-report/getError'; import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; -import { ModelElements } from '@/types/models'; +import { ModelElement, ModelElements } from '@/types/models'; import { MODEL_ELEMENTS_FETCHING_ERROR_PREFIX } from '@/redux/modelElements/modelElements.constants'; import { modelElementSchema } from '@/models/modelElementSchema'; import { pageableSchema } from '@/models/pageableSchema'; -export const getModelElements = createAsyncThunk<ModelElements | undefined, number, ThunkConfig>( - 'vectorMap/getModelElements', - async (modelId: number) => { - try { - const response = await axiosInstanceNewAPI.get<ModelElements>( - apiPath.getModelElements(modelId), - ); - const isDataValid = validateDataUsingZodSchema( - response.data, - pageableSchema(modelElementSchema), - ); - return isDataValid ? response.data : undefined; - } catch (error) { - return Promise.reject(getError({ error, prefix: MODEL_ELEMENTS_FETCHING_ERROR_PREFIX })); - } - }, -); +export const getModelElementsForModel = createAsyncThunk< + Array<ModelElement> | undefined, + number, + ThunkConfig +>('vectorMap/getModelElementsForModel', async (modelId: number) => { + try { + const { data } = await axiosInstanceNewAPI.get<ModelElements>( + apiPath.getModelElementsForModel(modelId), + ); + const isDataValid = validateDataUsingZodSchema(data, pageableSchema(modelElementSchema)); + return isDataValid ? data.content : undefined; + } catch (error) { + return Promise.reject(getError({ error, prefix: MODEL_ELEMENTS_FETCHING_ERROR_PREFIX })); + } +}); diff --git a/src/redux/modelElements/modelElements.types.ts b/src/redux/modelElements/modelElements.types.ts index 0dfdb426840f0a55ec4429ca78f06587d23f0f5f..b2d4ee6861e60602de84d7b45a021a36d11b93d5 100644 --- a/src/redux/modelElements/modelElements.types.ts +++ b/src/redux/modelElements/modelElements.types.ts @@ -1,4 +1,4 @@ -import { FetchDataState } from '@/types/fetchDataState'; -import { ModelElements } from '@/types/models'; +import { KeyedFetchDataState } from '@/types/fetchDataState'; +import { ModelElement } from '@/types/models'; -export type ModelElementsState = FetchDataState<ModelElements, null>; +export type ModelElementsState = KeyedFetchDataState<Array<ModelElement>>; diff --git a/src/redux/newReactions/newReactions.constants.ts b/src/redux/newReactions/newReactions.constants.ts index 80ea28ccf9f366c876a3dff5d6582db073fabe48..bb4677eb255820c83741904b21ddbf4d48cc074b 100644 --- a/src/redux/newReactions/newReactions.constants.ts +++ b/src/redux/newReactions/newReactions.constants.ts @@ -1,10 +1 @@ -import { DEFAULT_ERROR } from '@/constants/errors'; -import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; - -export const NEW_REACTIONS_INITIAL_STATE: NewReactionsState = { - data: [], - loading: 'idle', - error: DEFAULT_ERROR, -}; - export const NEW_REACTIONS_FETCHING_ERROR_PREFIX = 'Failed to fetch new reactions'; diff --git a/src/redux/newReactions/newReactions.mock.ts b/src/redux/newReactions/newReactions.mock.ts index 3847aed65b5a817df00b05ba02e4a5a5c7e63133..5b3c51a8a2aef056eaf4d5faba703ff2130c187c 100644 --- a/src/redux/newReactions/newReactions.mock.ts +++ b/src/redux/newReactions/newReactions.mock.ts @@ -1,7 +1,11 @@ import { DEFAULT_ERROR } from '@/constants/errors'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; +import { FetchDataState } from '@/types/fetchDataState'; +import { NewReaction } from '@/types/models'; -export const NEW_REACTIONS_INITIAL_STATE_MOCK: NewReactionsState = { +export const NEW_REACTIONS_INITIAL_STATE_MOCK: NewReactionsState = {}; + +export const NEW_REACTIONS_STATE_INITIAL_REACTIONS_MOCK: FetchDataState<Array<NewReaction>> = { data: [], loading: 'idle', error: DEFAULT_ERROR, diff --git a/src/redux/newReactions/newReactions.reducers.test.ts b/src/redux/newReactions/newReactions.reducers.test.ts index 21f8d6686e63ce18e3b924d3fa25f6f57e4c6687..0ceaa5b486cb6021c1c9227e78136d4db1d40f29 100644 --- a/src/redux/newReactions/newReactions.reducers.test.ts +++ b/src/redux/newReactions/newReactions.reducers.test.ts @@ -10,7 +10,7 @@ import { unwrapResult } from '@reduxjs/toolkit'; import newReactionsReducer from '@/redux/newReactions/newReactions.slice'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; import { NEW_REACTIONS_INITIAL_STATE_MOCK } from '@/redux/newReactions/newReactions.mock'; -import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; +import { getNewReactionsForModel } from '@/redux/newReactions/newReactions.thunks'; import { newReactionsFixture } from '@/models/fixtures/newReactionsFixture'; const mockedAxiosClient = mockNetworkNewAPIResponse(); @@ -29,26 +29,26 @@ describe('newReactions reducer', () => { expect(newReactionsReducer(undefined, action)).toEqual(INITIAL_STATE); }); - it('should update store after successful getNewReactions query', async () => { + it('should update store after successful getNewReactionsForModel query', async () => { mockedAxiosClient - .onGet(apiPath.getNewReactions(1)) + .onGet(apiPath.getNewReactionsForModel(1)) .reply(HttpStatusCode.Ok, newReactionsFixture); - const { type } = await store.dispatch(getNewReactions(1)); - const { data, loading, error } = store.getState().newReactions; - expect(type).toBe('newReactions/getNewReactions/fulfilled'); + const { type } = await store.dispatch(getNewReactionsForModel(1)); + const { data, loading, error } = store.getState().newReactions[1]; + expect(type).toBe('newReactions/getNewReactionsForModel/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); expect(data).toEqual(newReactionsFixture.content); }); - it('should update store after failed getNewReactions query', async () => { - mockedAxiosClient.onGet(apiPath.getNewReactions(1)).reply(HttpStatusCode.NotFound, []); + it('should update store after failed getNewReactionsForModel query', async () => { + mockedAxiosClient.onGet(apiPath.getNewReactionsForModel(1)).reply(HttpStatusCode.NotFound, []); - const action = await store.dispatch(getNewReactions(1)); - const { data, loading, error } = store.getState().newReactions; + const action = await store.dispatch(getNewReactionsForModel(1)); + const { data, loading, error } = store.getState().newReactions[1]; - expect(action.type).toBe('newReactions/getNewReactions/rejected'); + expect(action.type).toBe('newReactions/getNewReactionsForModel/rejected'); expect(() => unwrapResult(action)).toThrow( "Failed to fetch new reactions: The page you're looking for doesn't exist. Please verify the URL and try again.", ); @@ -57,20 +57,20 @@ describe('newReactions reducer', () => { expect(data).toEqual([]); }); - it('should update store on loading getNewReactions query', async () => { + it('should update store on loading getNewReactionsForModel query', async () => { mockedAxiosClient - .onGet(apiPath.getNewReactions(1)) + .onGet(apiPath.getNewReactionsForModel(1)) .reply(HttpStatusCode.Ok, newReactionsFixture); - const newReactionsPromise = store.dispatch(getNewReactions(1)); + const newReactionsPromise = store.dispatch(getNewReactionsForModel(1)); - const { data, loading } = store.getState().newReactions; + const { data, loading } = store.getState().newReactions[1]; expect(data).toEqual([]); expect(loading).toEqual('pending'); newReactionsPromise.then(() => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = - store.getState().newReactions; + store.getState().newReactions[1]; expect(dataPromiseFulfilled).toEqual(newReactionsFixture.content); expect(promiseFulfilled).toEqual('succeeded'); diff --git a/src/redux/newReactions/newReactions.reducers.ts b/src/redux/newReactions/newReactions.reducers.ts index 306b306e770cfb0097e256e1aed05fa8017d2990..c8963df9c58fbee4cd4675e8b5041f97f07ebd00 100644 --- a/src/redux/newReactions/newReactions.reducers.ts +++ b/src/redux/newReactions/newReactions.reducers.ts @@ -1,18 +1,36 @@ import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; -import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; +import { getNewReactionsForModel } from '@/redux/newReactions/newReactions.thunks'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; +import { NEW_REACTIONS_STATE_INITIAL_REACTIONS_MOCK } from '@/redux/newReactions/newReactions.mock'; +import { DEFAULT_ERROR } from '@/constants/errors'; export const getNewReactionsReducer = ( builder: ActionReducerMapBuilder<NewReactionsState>, ): void => { - builder.addCase(getNewReactions.pending, state => { - state.loading = 'pending'; + builder.addCase(getNewReactionsForModel.pending, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'pending'; + } else { + state[modelId] = { ...NEW_REACTIONS_STATE_INITIAL_REACTIONS_MOCK, loading: 'pending' }; + } }); - builder.addCase(getNewReactions.fulfilled, (state, action) => { - state.data = action.payload || []; - state.loading = 'succeeded'; + builder.addCase(getNewReactionsForModel.fulfilled, (state, action) => { + const modelId = action.meta.arg; + const data = action.payload || []; + if (state[modelId]) { + state[modelId].data = data; + state[modelId].loading = 'succeeded'; + } else { + state[modelId] = { data, loading: 'pending', error: DEFAULT_ERROR }; + } }); - builder.addCase(getNewReactions.rejected, state => { - state.loading = 'failed'; + builder.addCase(getNewReactionsForModel.rejected, (state, action) => { + const modelId = action.meta.arg; + if (state[modelId]) { + state[modelId].loading = 'failed'; + } else { + state[modelId] = { ...NEW_REACTIONS_STATE_INITIAL_REACTIONS_MOCK, loading: 'failed' }; + } }); }; diff --git a/src/redux/newReactions/newReactions.selectors.ts b/src/redux/newReactions/newReactions.selectors.ts index 146bbc85efe9c374b3c4f7d5118e6651cd5d3a57..6bf11f0e122132c75ef757a362234d0305408bb0 100644 --- a/src/redux/newReactions/newReactions.selectors.ts +++ b/src/redux/newReactions/newReactions.selectors.ts @@ -1,14 +1,21 @@ import { createSelector } from '@reduxjs/toolkit'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { rootSelector } from '../root/root.selectors'; export const newReactionsSelector = createSelector(rootSelector, state => state.newReactions); -export const newReactionsLoadingSelector = createSelector( +export const newReactionsStateForCurrentModelSelector = createSelector( newReactionsSelector, - state => state.loading, + currentModelIdSelector, + (state, currentModelId) => state[currentModelId], ); -export const newReactionsDataSelector = createSelector( - newReactionsSelector, - reactions => reactions.data || [], +export const newReactionsLoadingSelector = createSelector( + newReactionsStateForCurrentModelSelector, + state => state?.loading, +); + +export const newReactionsForCurrentModelSelector = createSelector( + newReactionsStateForCurrentModelSelector, + state => state?.data || [], ); diff --git a/src/redux/newReactions/newReactions.slice.ts b/src/redux/newReactions/newReactions.slice.ts index 5bb5198a1f58d89036338741124f555a64c9b71a..d603e5797e3c3fe292794e086e661b7ce3984091 100644 --- a/src/redux/newReactions/newReactions.slice.ts +++ b/src/redux/newReactions/newReactions.slice.ts @@ -1,10 +1,10 @@ import { createSlice } from '@reduxjs/toolkit'; -import { NEW_REACTIONS_INITIAL_STATE } from '@/redux/newReactions/newReactions.constants'; import { getNewReactionsReducer } from '@/redux/newReactions/newReactions.reducers'; +import { NEW_REACTIONS_INITIAL_STATE_MOCK } from '@/redux/newReactions/newReactions.mock'; export const newReactionsSlice = createSlice({ name: 'reactions', - initialState: NEW_REACTIONS_INITIAL_STATE, + initialState: NEW_REACTIONS_INITIAL_STATE_MOCK, reducers: {}, extraReducers: builder => { getNewReactionsReducer(builder); diff --git a/src/redux/newReactions/newReactions.thunks.test.ts b/src/redux/newReactions/newReactions.thunks.test.ts index afd14d77104b12dcbfaa00d460ca2221244947b9..3bcc5a9bb43cb0059fffec7586c8d6241249188d 100644 --- a/src/redux/newReactions/newReactions.thunks.test.ts +++ b/src/redux/newReactions/newReactions.thunks.test.ts @@ -9,7 +9,7 @@ import { HttpStatusCode } from 'axios'; import newReactionsReducer from '@/redux/newReactions/newReactions.slice'; import { NewReactionsState } from '@/redux/newReactions/newReactions.types'; import { newReactionsFixture } from '@/models/fixtures/newReactionsFixture'; -import { getNewReactions } from '@/redux/newReactions/newReactions.thunks'; +import { getNewReactionsForModel } from '@/redux/newReactions/newReactions.thunks'; const mockedAxiosClient = mockNetworkNewAPIResponse(); @@ -22,19 +22,19 @@ describe('newReactions thunks', () => { describe('getReactions', () => { it('should return data when data response from API is valid', async () => { mockedAxiosClient - .onGet(apiPath.getNewReactions(1)) + .onGet(apiPath.getNewReactionsForModel(1)) .reply(HttpStatusCode.Ok, newReactionsFixture); - const { payload } = await store.dispatch(getNewReactions(1)); + const { payload } = await store.dispatch(getNewReactionsForModel(1)); expect(payload).toEqual(newReactionsFixture.content); }); it('should return undefined when data response from API is not valid ', async () => { mockedAxiosClient - .onGet(apiPath.getNewReactions(1)) + .onGet(apiPath.getNewReactionsForModel(1)) .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); - const { payload } = await store.dispatch(getNewReactions(1)); + const { payload } = await store.dispatch(getNewReactionsForModel(1)); expect(payload).toEqual(undefined); }); }); diff --git a/src/redux/newReactions/newReactions.thunks.ts b/src/redux/newReactions/newReactions.thunks.ts index 7e7081a85787f62592bce8fc9095714a45eef4b3..2efd3f061fe3695950822ff5f4cdfe535ad7b856 100644 --- a/src/redux/newReactions/newReactions.thunks.ts +++ b/src/redux/newReactions/newReactions.thunks.ts @@ -9,13 +9,15 @@ import { newReactionSchema } from '@/models/newReactionSchema'; import { pageableSchema } from '@/models/pageableSchema'; import { NEW_REACTIONS_FETCHING_ERROR_PREFIX } from '@/redux/newReactions/newReactions.constants'; -export const getNewReactions = createAsyncThunk< +export const getNewReactionsForModel = createAsyncThunk< Array<NewReaction> | undefined, number, ThunkConfig ->('newReactions/getNewReactions', async (modelId: number) => { +>('newReactions/getNewReactionsForModel', async (modelId: number) => { try { - const { data } = await axiosInstanceNewAPI.get<NewReactions>(apiPath.getNewReactions(modelId)); + const { data } = await axiosInstanceNewAPI.get<NewReactions>( + apiPath.getNewReactionsForModel(modelId), + ); const isDataValid = validateDataUsingZodSchema(data, pageableSchema(newReactionSchema)); return isDataValid ? data.content : undefined; } catch (error) { diff --git a/src/redux/newReactions/newReactions.types.ts b/src/redux/newReactions/newReactions.types.ts index 142906d7f46037ae9789eee1e74504ea2408d55f..aff62402cdf7b3d7a15beee7399ec143fec21cb6 100644 --- a/src/redux/newReactions/newReactions.types.ts +++ b/src/redux/newReactions/newReactions.types.ts @@ -1,4 +1,4 @@ -import { FetchDataState } from '@/types/fetchDataState'; +import { KeyedFetchDataState } from '@/types/fetchDataState'; import { NewReaction } from '@/types/models'; -export type NewReactionsState = FetchDataState<NewReaction[]>; +export type NewReactionsState = KeyedFetchDataState<Array<NewReaction>>;