From 793ecb9ba343dd4746a2db80913b9a66d6c634a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 21 Nov 2024 12:14:20 +0100 Subject: [PATCH] feat(vector-map): add spinner when diagram is loading --- src/components/Map/Map.component.tsx | 2 + .../MapLoader/MapLoader.component.test.tsx | 107 ++++++++++++++++++ .../Map/MapLoader/MapLoader.component.tsx | 50 ++++++++ .../Map/MapLoader/MapLoader.styles.css | 9 ++ .../reactionsLayer/useOlMapReactionsLayer.ts | 6 +- src/models/mocks/modelsMock.ts | 1 + src/redux/layers/layers.selectors.ts | 2 + .../modelElements/modelElements.selector.ts | 5 + .../newReactions/newReactions.selectors.ts | 5 + src/redux/shapes/shapes.selectors.ts | 15 +++ 10 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 src/components/Map/MapLoader/MapLoader.component.test.tsx create mode 100644 src/components/Map/MapLoader/MapLoader.component.tsx create mode 100644 src/components/Map/MapLoader/MapLoader.styles.css diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx index 67d4d216..67b7187c 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 00000000..eb35db6b --- /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 00000000..fc457e0b --- /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 00000000..750e4cb6 --- /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 87f31fcf..43fabc21 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 1684bf93..53dac6b9 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 987ec4ac..d29698e0 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 54b4a75b..3a9d8b54 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 4dc2babe..146bbc85 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 cdd4fa16..d23c9a97 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, +); -- GitLab