diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx index 67d4d216c793458cefe6cd83af70a72989112876..67b7187c64b3d5a96784e8e713ed4ba6747d02c0 100644 --- a/src/components/Map/Map.component.tsx +++ b/src/components/Map/Map.component.tsx @@ -2,6 +2,7 @@ import { Drawer } from '@/components/Map/Drawer'; import { Legend } from '@/components/Map/Legend'; import { MapViewer } from '@/components/Map/MapViewer'; +import { MapLoader } from '@/components/Map/MapLoader/MapLoader.component'; import { MapAdditionalActions } from './MapAdditionalActions'; import { MapAdditionalOptions } from './MapAdditionalOptions'; import { PluginsDrawer } from './PluginsDrawer'; @@ -18,6 +19,7 @@ export const Map = (): JSX.Element => { <PluginsDrawer /> <Legend /> <MapAdditionalActions /> + <MapLoader /> </div> ); }; diff --git a/src/components/Map/MapLoader/MapLoader.component.test.tsx b/src/components/Map/MapLoader/MapLoader.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..eb35db6b6bfbafe153929763e5b5dffbe9e5069a --- /dev/null +++ b/src/components/Map/MapLoader/MapLoader.component.test.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { + getReduxWrapperWithStore, + InitialStoreState, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { StoreType } from '@/redux/store'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { newReactionsLoadingSelector } from '@/redux/newReactions/newReactions.selectors'; +import { modelElementsLoadingSelector } from '@/redux/modelElements/modelElements.selector'; +import { isDrawerOpenSelector } from '@/redux/drawer/drawer.selectors'; +import { vectorRenderingSelector } from '@/redux/models/models.selectors'; +import { + arrowTypesLoadingSelector, + bioShapesLoadingSelector, + lineTypesLoadingSelector, +} from '@/redux/shapes/shapes.selectors'; +import { layersLoadingSelector } from '@/redux/layers/layers.selectors'; +import { MapLoader } from './MapLoader.component'; + +jest.mock('../../../redux/hooks/useAppSelector', () => ({ + useAppSelector: jest.fn(), +})); +type SelectorFunction = (state: never) => string | boolean; + +const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStore); + + return ( + render( + <Wrapper> + <MapLoader /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('MapLoader', () => { + const mockUseAppSelector = useAppSelector as jest.Mock; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should not render the LoadingIndicator when no data is loading', () => { + mockUseAppSelector.mockImplementation(selector => { + const selectorMap = new Map<SelectorFunction, string | boolean>([ + [newReactionsLoadingSelector, 'succeeded'], + [modelElementsLoadingSelector, 'succeeded'], + [vectorRenderingSelector, true], + [bioShapesLoadingSelector, 'succeeded'], + [lineTypesLoadingSelector, 'succeeded'], + [arrowTypesLoadingSelector, 'succeeded'], + [layersLoadingSelector, 'succeeded'], + [isDrawerOpenSelector, false], + ]); + + return selectorMap.get(selector) ?? false; + }); + renderComponent(); + + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); + }); + + it('should render the LoadingIndicator when vectorRendering is true and data is loading', () => { + mockUseAppSelector.mockImplementation(selector => { + const selectorMap = new Map<SelectorFunction, string | boolean>([ + [newReactionsLoadingSelector, 'pending'], + [modelElementsLoadingSelector, 'succeeded'], + [vectorRenderingSelector, true], + [bioShapesLoadingSelector, 'succeeded'], + [lineTypesLoadingSelector, 'succeeded'], + [arrowTypesLoadingSelector, 'succeeded'], + [layersLoadingSelector, 'succeeded'], + [isDrawerOpenSelector, false], + ]); + + return selectorMap.get(selector) ?? false; + }); + renderComponent(); + + expect(screen.queryByTestId('loading-indicator')).toBeInTheDocument(); + }); + + it('should not render the LoadingIndicator when vectorRendering is false even when data is loading', () => { + mockUseAppSelector.mockImplementation(selector => { + const selectorMap = new Map<SelectorFunction, string | boolean>([ + [newReactionsLoadingSelector, 'pending'], + [modelElementsLoadingSelector, 'succeeded'], + [vectorRenderingSelector, false], + [bioShapesLoadingSelector, 'succeeded'], + [lineTypesLoadingSelector, 'succeeded'], + [arrowTypesLoadingSelector, 'succeeded'], + [layersLoadingSelector, 'succeeded'], + [isDrawerOpenSelector, false], + ]); + + return selectorMap.get(selector) ?? false; + }); + renderComponent(); + + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/Map/MapLoader/MapLoader.component.tsx b/src/components/Map/MapLoader/MapLoader.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fc457e0b62d735e95569b41fdbe41b598638f15c --- /dev/null +++ b/src/components/Map/MapLoader/MapLoader.component.tsx @@ -0,0 +1,50 @@ +import { LoadingIndicator } from '@/shared/LoadingIndicator'; +import { useMemo } from 'react'; +import { newReactionsLoadingSelector } from '@/redux/newReactions/newReactions.selectors'; +import { modelElementsLoadingSelector } from '@/redux/modelElements/modelElements.selector'; +import { vectorRenderingSelector } from '@/redux/models/models.selectors'; +import { + arrowTypesLoadingSelector, + bioShapesLoadingSelector, + lineTypesLoadingSelector, +} from '@/redux/shapes/shapes.selectors'; +import { layersLoadingSelector } from '@/redux/layers/layers.selectors'; +import './MapLoader.styles.css'; +import { isDrawerOpenSelector } from '@/redux/drawer/drawer.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; + +export const MapLoader = (): JSX.Element => { + const reactionsFetching = useAppSelector(newReactionsLoadingSelector); + const modelElementsFetching = useAppSelector(modelElementsLoadingSelector); + const vectorRendering = useAppSelector(vectorRenderingSelector); + const bioShapesFetching = useAppSelector(bioShapesLoadingSelector); + const lineTypesFetching = useAppSelector(lineTypesLoadingSelector); + const arrowTypesFetching = useAppSelector(arrowTypesLoadingSelector); + const layersLoading = useAppSelector(layersLoadingSelector); + + const isDrawerOpen = useAppSelector(isDrawerOpenSelector); + + const showLoader = useMemo(() => { + return [ + reactionsFetching, + modelElementsFetching, + bioShapesFetching, + lineTypesFetching, + arrowTypesFetching, + layersLoading, + ].includes('pending'); + }, [ + reactionsFetching, + modelElementsFetching, + bioShapesFetching, + lineTypesFetching, + arrowTypesFetching, + layersLoading, + ]); + + return ( + <div className={`map-loader transition-all duration-500 ${isDrawerOpen ? 'move-right' : ''}`}> + {vectorRendering && showLoader && <LoadingIndicator width={48} height={48} />} + </div> + ); +}; diff --git a/src/components/Map/MapLoader/MapLoader.styles.css b/src/components/Map/MapLoader/MapLoader.styles.css new file mode 100644 index 0000000000000000000000000000000000000000..750e4cb6fdd5ca1f3567fd02979f6ade9f566991 --- /dev/null +++ b/src/components/Map/MapLoader/MapLoader.styles.css @@ -0,0 +1,9 @@ +.map-loader { + position: absolute; + left: 120px; + top: 128px; +} + +.map-loader.move-right { + left: 550px; +} 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 87f31fcf34f378176ec4fc91d070135875204e5f..43fabc2100dccdca69456cecd7cf1cba462b04cb 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -62,8 +62,10 @@ export const useOlMapReactionsLayer = ({ const pointToProjection = usePointToProjection(); useEffect(() => { - dispatch(getModelElements(currentModelId)); - dispatch(getNewReactions(currentModelId)); + if (currentModelId) { + dispatch(getModelElements(currentModelId)); + dispatch(getNewReactions(currentModelId)); + } }, [currentModelId, dispatch]); const groupedElementsOverlays = useMemo(() => { diff --git a/src/models/mocks/modelsMock.ts b/src/models/mocks/modelsMock.ts index 1684bf934c71c5ecf6fc05912aa57e41b7fbc1a2..53dac6b905d13af4459fa88d7a1f9e3fde5f8012 100644 --- a/src/models/mocks/modelsMock.ts +++ b/src/models/mocks/modelsMock.ts @@ -474,6 +474,7 @@ export const CORE_PD_MODEL_MOCK: MapModel = { modificationDates: [], minZoom: 2, maxZoom: 9, + vectorRendering: true, }; export const MODEL_WITH_DESCRIPTION: MapModel = { diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts index 987ec4ac2855d7e3c1a30857301034ef4dec7daf..d29698e0254a341147c7bbc16fe83456bb8ca7be 100644 --- a/src/redux/layers/layers.selectors.ts +++ b/src/redux/layers/layers.selectors.ts @@ -6,6 +6,8 @@ export const layersSelector = createSelector( state => state.layers?.data?.layers || [], ); +export const layersLoadingSelector = createSelector(rootSelector, state => state.layers.loading); + export const layersVisibilitySelector = createSelector( rootSelector, state => state.layers?.data?.layersVisibility || {}, diff --git a/src/redux/modelElements/modelElements.selector.ts b/src/redux/modelElements/modelElements.selector.ts index 54b4a75b00d98355f601dfa2ed2c49c1911f4258..3a9d8b54a29cabdda042e2ec63a3eb0c7465472d 100644 --- a/src/redux/modelElements/modelElements.selector.ts +++ b/src/redux/modelElements/modelElements.selector.ts @@ -5,3 +5,8 @@ export const modelElementsSelector = createSelector( rootSelector, state => state.modelElements.data, ); + +export const modelElementsLoadingSelector = createSelector( + rootSelector, + state => state.modelElements.loading, +); diff --git a/src/redux/newReactions/newReactions.selectors.ts b/src/redux/newReactions/newReactions.selectors.ts index 4dc2babe517c9850b03cf090644ebe2389550dcd..146bbc85efe9c374b3c4f7d5118e6651cd5d3a57 100644 --- a/src/redux/newReactions/newReactions.selectors.ts +++ b/src/redux/newReactions/newReactions.selectors.ts @@ -3,6 +3,11 @@ import { rootSelector } from '../root/root.selectors'; export const newReactionsSelector = createSelector(rootSelector, state => state.newReactions); +export const newReactionsLoadingSelector = createSelector( + newReactionsSelector, + state => state.loading, +); + export const newReactionsDataSelector = createSelector( newReactionsSelector, reactions => reactions.data || [], diff --git a/src/redux/shapes/shapes.selectors.ts b/src/redux/shapes/shapes.selectors.ts index cdd4fa160ae29d1a0197c847a0d20127c394365a..d23c9a97d97a744f6150d30757d2706dbeaa6a96 100644 --- a/src/redux/shapes/shapes.selectors.ts +++ b/src/redux/shapes/shapes.selectors.ts @@ -8,12 +8,27 @@ export const bioShapesSelector = createSelector( shapes => shapes.bioShapesState.data, ); +export const bioShapesLoadingSelector = createSelector( + shapesSelector, + shapes => shapes.bioShapesState.loading, +); + export const lineTypesSelector = createSelector( shapesSelector, shapes => shapes.lineTypesState.data || {}, ); +export const lineTypesLoadingSelector = createSelector( + shapesSelector, + shapes => shapes.lineTypesState.loading, +); + export const arrowTypesSelector = createSelector( shapesSelector, shapes => shapes.arrowTypesState.data || {}, ); + +export const arrowTypesLoadingSelector = createSelector( + shapesSelector, + shapes => shapes.arrowTypesState.loading, +);