From 56bb144c8b321a6f7846e563488582a2e90ba5fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20Miesi=C4=85c?= <tadeusz.miesiac@gmail.com> Date: Thu, 21 Dec 2023 21:30:41 +0100 Subject: [PATCH] feat(overlays): add/remove overlay from store --- .../OverlayListItem.component.test.tsx | 64 +++++++++++++------ .../OverlayListItem.component.tsx | 16 ++--- .../OverlayListItem/hooks/useOverlay.ts | 28 ++++++++ .../getColorByAvailableProperties.ts | 4 +- .../overlayBioEntity.reducers.test.ts | 42 ++++++++++++ .../overlayBioEntity.reducers.ts | 31 +++++++-- .../overlayBioEntity.selector.ts | 16 ++++- .../overlayBioEntity.slice.ts | 6 +- .../overlayBioEntity.types.ts | 11 +++- 9 files changed, 177 insertions(+), 41 deletions(-) create mode 100644 src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/hooks/useOverlay.ts create mode 100644 src/redux/overlayBioEntity/overlayBioEntity.reducers.test.ts diff --git a/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.test.tsx b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.test.tsx index dd3cc6ea..1c2e5658 100644 --- a/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.test.tsx +++ b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.test.tsx @@ -47,30 +47,56 @@ describe('OverlayListItem - component', () => { expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument(); }); - it('should trigger view overlays on view button click and switch background to Empty if available', async () => { - const OVERLAY_ID = 21; - const { store } = renderComponent({ - map: initialMapStateFixture, - backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK }, - overlayBioEntity: OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, - models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] }, - }); - mockedAxiosNewClient - .onGet(apiPath.getOverlayBioEntity({ overlayId: OVERLAY_ID, modelId: 5053 })) - .reply(HttpStatusCode.Ok, overlayBioEntityFixture); + describe('view overlays', () => { + it('should trigger view overlays on view button click and switch background to Empty if available', async () => { + const OVERLAY_ID = 21; + const MODEL_ID = 5053; + const { store } = renderComponent({ + map: initialMapStateFixture, + backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK }, + overlayBioEntity: OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, + models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] }, + }); + mockedAxiosNewClient + .onGet(apiPath.getOverlayBioEntity({ overlayId: OVERLAY_ID, modelId: MODEL_ID })) + .reply(HttpStatusCode.Ok, overlayBioEntityFixture); + + expect(store.getState().map.data.backgroundId).toBe(DEFAULT_BACKGROUND_ID); - expect(store.getState().map.data.backgroundId).toBe(DEFAULT_BACKGROUND_ID); + const ViewButton = screen.getByRole('button', { name: 'View' }); + await act(() => { + ViewButton.click(); + }); - const ViewButton = screen.getByRole('button', { name: 'View' }); - await act(() => { - ViewButton.click(); + expect(store.getState().map.data.backgroundId).toBe(EMPTY_BACKGROUND_ID); + expect(store.getState().overlayBioEntity.data).toEqual({ + [OVERLAY_ID]: { + [MODEL_ID]: parseOverlayBioEntityToOlRenderingFormat(overlayBioEntityFixture, OVERLAY_ID), + }, + }); }); + it('should disable overlay on view button click if overlay is active', async () => { + const OVERLAY_ID = 21; + const { store } = renderComponent({ + map: { + ...initialMapStateFixture, + data: { ...initialMapStateFixture.data, backgroundId: EMPTY_BACKGROUND_ID }, + }, + backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK }, + overlayBioEntity: { ...OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, overlaysId: [OVERLAY_ID] }, + models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] }, + }); + + const ViewButton = screen.getByRole('button', { name: 'Disable' }); + await act(() => { + ViewButton.click(); + }); - expect(store.getState().map.data.backgroundId).toBe(EMPTY_BACKGROUND_ID); - expect(store.getState().overlayBioEntity.data).toEqual( - parseOverlayBioEntityToOlRenderingFormat(overlayBioEntityFixture, OVERLAY_ID), - ); + expect(store.getState().overlayBioEntity.data).toEqual([]); + expect(store.getState().overlayBioEntity.overlaysId).toEqual([]); + }); }); + // TODO implement when connecting logic to component it.skip('should trigger download overlay to PC on download button click', () => {}); }); diff --git a/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.tsx b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.tsx index 20f173fe..ab40e7cf 100644 --- a/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.tsx +++ b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/OverlayListItem.component.tsx @@ -1,7 +1,5 @@ -import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { getOverlayBioEntityForAllModels } from '@/redux/overlayBioEntity/overlayBioEntity.thunk'; import { Button } from '@/shared/Button'; -import { useEmptyBackground } from './hooks/useEmptyBackground'; +import { useOverlay } from './hooks/useOverlay'; interface OverlayListItemProps { name: string; @@ -10,20 +8,14 @@ interface OverlayListItemProps { export const OverlayListItem = ({ name, overlayId }: OverlayListItemProps): JSX.Element => { const onDownloadOverlay = (): void => {}; - const dispatch = useAppDispatch(); - const { setBackgroundtoEmptyIfAvailable } = useEmptyBackground(); - - const onViewOverlay = (): void => { - setBackgroundtoEmptyIfAvailable(); - dispatch(getOverlayBioEntityForAllModels({ overlayId })); - }; + const { toggleOverlay, isOverlayActive } = useOverlay(overlayId); return ( <li className="flex flex-row flex-nowrap justify-between pl-5 [&:not(:last-of-type)]:mb-4"> <span>{name}</span> <div className="flex flex-row flex-nowrap"> - <Button variantStyles="ghost" className="mr-4 max-h-8" onClick={onViewOverlay}> - View + <Button variantStyles="ghost" className="mr-4 max-h-8" onClick={toggleOverlay}> + {isOverlayActive ? 'Disable' : 'View'} </Button> <Button className="max-h-8" variantStyles="ghost" onClick={onDownloadOverlay}> Download diff --git a/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/hooks/useOverlay.ts b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/hooks/useOverlay.ts new file mode 100644 index 00000000..89f65ee8 --- /dev/null +++ b/src/components/Map/Drawer/OverlaysDrawer/GeneralOverlays/OverlayListItem/hooks/useOverlay.ts @@ -0,0 +1,28 @@ +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { isOverlayActiveSelector } from '@/redux/overlayBioEntity/overlayBioEntity.selector'; +import { removeOverlayBioEntityForGivenOverlay } from '@/redux/overlayBioEntity/overlayBioEntity.slice'; +import { getOverlayBioEntityForAllModels } from '@/redux/overlayBioEntity/overlayBioEntity.thunk'; +import { useEmptyBackground } from './useEmptyBackground'; + +type UseOverlay = { + toggleOverlay: () => void; + isOverlayActive: boolean; +}; + +export const useOverlay = (overlayId: number): UseOverlay => { + const dispatch = useAppDispatch(); + const isOverlayActive = useAppSelector(state => isOverlayActiveSelector(state, overlayId)); + const { setBackgroundtoEmptyIfAvailable } = useEmptyBackground(); + + const toggleOverlay = (): void => { + if (isOverlayActive) { + dispatch(removeOverlayBioEntityForGivenOverlay({ overlayId })); + } else { + setBackgroundtoEmptyIfAvailable(); + dispatch(getOverlayBioEntityForAllModels({ overlayId })); + } + }; + + return { toggleOverlay, isOverlayActive }; +}; diff --git a/src/components/Map/MapViewer/utils/config/overlaysLayer/getColorByAvailableProperties.ts b/src/components/Map/MapViewer/utils/config/overlaysLayer/getColorByAvailableProperties.ts index b7ac985f..6cbc9883 100644 --- a/src/components/Map/MapViewer/utils/config/overlaysLayer/getColorByAvailableProperties.ts +++ b/src/components/Map/MapViewer/utils/config/overlaysLayer/getColorByAvailableProperties.ts @@ -1,7 +1,7 @@ import { ZERO } from '@/constants/common'; import type { GetHex3ColorGradientColorWithAlpha } from '@/hooks/useTriColorLerp'; import { OverlayBioEntityRender } from '@/types/OLrendering'; -import { convertDecimalToHex } from '@/utils/convert/convertDecimalToHex'; +import { convertDecimalToHexColor } from '@/utils/convert/convertDecimalToHex'; export const getColorByAvailableProperties = ( entity: OverlayBioEntityRender, @@ -12,7 +12,7 @@ export const getColorByAvailableProperties = ( return getHexTricolorGradientColorWithAlpha(entity.value || ZERO); } if (entity.color) { - return convertDecimalToHex(entity.color.rgb); + return convertDecimalToHexColor(entity.color.rgb); } return defaultColor; }; diff --git a/src/redux/overlayBioEntity/overlayBioEntity.reducers.test.ts b/src/redux/overlayBioEntity/overlayBioEntity.reducers.test.ts new file mode 100644 index 00000000..ba1740c3 --- /dev/null +++ b/src/redux/overlayBioEntity/overlayBioEntity.reducers.test.ts @@ -0,0 +1,42 @@ +import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { overlayBioEntityFixture } from '@/models/fixtures/overlayBioEntityFixture'; +import overlayBioEntityReducer from './overlayBioEntity.slice'; +import { getOverlayBioEntity } from './overlayBioEntity.thunk'; +import { parseOverlayBioEntityToOlRenderingFormat } from './overlayBioEntity.utils'; +import { OverlaysBioEntityState } from './overlayBioEntity.types'; +import { apiPath } from '../apiPath'; + +const mockedNewAxiosClient = mockNetworkNewAPIResponse(); + +describe('Overlay Bio Entity Reducers', () => { + const OVERLAY_ID = 21; + const MODEL_ID = 27; + + let store = {} as ToolkitStoreWithSingleSlice<OverlaysBioEntityState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('overlayBioEntity', overlayBioEntityReducer); + }); + + describe('getOverlayBioEntityReducer', () => { + it('should update the state correctly when getOverlayBioEntity action is dispatched', async () => { + mockedNewAxiosClient + .onGet(apiPath.getOverlayBioEntity({ overlayId: OVERLAY_ID, modelId: MODEL_ID })) + .reply(HttpStatusCode.Ok, overlayBioEntityFixture); + + const { type } = await store.dispatch( + getOverlayBioEntity({ overlayId: OVERLAY_ID, modelId: MODEL_ID }), + ); + const { data } = store.getState().overlayBioEntity; + + expect(type).toBe('overlayBioEntity/getOverlayBioEntity/fulfilled'); + expect(data[OVERLAY_ID][MODEL_ID]).toEqual( + parseOverlayBioEntityToOlRenderingFormat(overlayBioEntityFixture, OVERLAY_ID), + ); + }); + }); +}); diff --git a/src/redux/overlayBioEntity/overlayBioEntity.reducers.ts b/src/redux/overlayBioEntity/overlayBioEntity.reducers.ts index da76054b..797bacd7 100644 --- a/src/redux/overlayBioEntity/overlayBioEntity.reducers.ts +++ b/src/redux/overlayBioEntity/overlayBioEntity.reducers.ts @@ -1,14 +1,21 @@ import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import { getOverlayBioEntity, getOverlayBioEntityForAllModels } from './overlayBioEntity.thunk'; -import { OverlaysBioEntityState } from './overlayBioEntity.types'; +import { + OverlaysBioEntityState, + RemoveOverlayBioEntityForGivenOverlayAction, +} from './overlayBioEntity.types'; export const getOverlayBioEntityReducer = ( builder: ActionReducerMapBuilder<OverlaysBioEntityState>, ): void => { builder.addCase(getOverlayBioEntity.fulfilled, (state, action) => { if (action.payload) { - state.overlaysId = [action.meta.arg.overlayId]; - state.data.push(...action.payload); + const { overlayId, modelId } = action.meta.arg; + if (!state.data[action.meta.arg.overlayId]) { + state.data[overlayId] = {}; + } + + state.data[overlayId][modelId] = action.payload; } }); }; @@ -16,7 +23,21 @@ export const getOverlayBioEntityReducer = ( export const getOverlayBioEntityForAllModelsReducer = ( builder: ActionReducerMapBuilder<OverlaysBioEntityState>, ): void => { - builder.addCase(getOverlayBioEntityForAllModels.pending, state => { - state.data = []; + builder.addCase(getOverlayBioEntityForAllModels.pending, (state, action) => { + const { overlayId } = action.meta.arg; + state.overlaysId.push(overlayId); + state.data = { + ...state.data, // this is expection to the rule of immutability from redux-toolkit. state.data[overlayId] = {} would add null values up to overlayId value witch leads to mess in the store + [overlayId]: {}, + }; }); }; + +export const removeOverlayBioEntityForGivenOverlayReducer = ( + state: OverlaysBioEntityState, + action: RemoveOverlayBioEntityForGivenOverlayAction, +): void => { + const { overlayId } = action.payload; + state.overlaysId = state.overlaysId.filter(id => id !== overlayId); + delete state.data[overlayId]; +}; diff --git a/src/redux/overlayBioEntity/overlayBioEntity.selector.ts b/src/redux/overlayBioEntity/overlayBioEntity.selector.ts index 72c3b359..00803c03 100644 --- a/src/redux/overlayBioEntity/overlayBioEntity.selector.ts +++ b/src/redux/overlayBioEntity/overlayBioEntity.selector.ts @@ -12,8 +12,22 @@ export const overlayBioEntityDataSelector = createSelector( overlayBioEntity => overlayBioEntity.data, ); +export const activeOverlaysIdSelector = createSelector( + overlayBioEntitySelector, + state => state.overlaysId, +); + +const FIRST_ENTITY_INDEX = 0; +// TODO, improve selector when multioverlay algorithm comes in place export const overlayBioEntitiesForCurrentModelSelector = createSelector( overlayBioEntityDataSelector, currentModelIdSelector, - (data, currentModelId) => data.filter(entity => entity.modelId === currentModelId), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (data, currentModelId) => data[Object.keys(data)[FIRST_ENTITY_INDEX]]?.[currentModelId] ?? [], // temporary solution untill multioverlay algorithm comes in place +); + +export const isOverlayActiveSelector = createSelector( + [activeOverlaysIdSelector, (_, overlayId: number): number => overlayId], + (overlaysId, overlayId) => overlaysId.includes(overlayId), ); diff --git a/src/redux/overlayBioEntity/overlayBioEntity.slice.ts b/src/redux/overlayBioEntity/overlayBioEntity.slice.ts index f25d3ed6..ae21322c 100644 --- a/src/redux/overlayBioEntity/overlayBioEntity.slice.ts +++ b/src/redux/overlayBioEntity/overlayBioEntity.slice.ts @@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { getOverlayBioEntityForAllModelsReducer, getOverlayBioEntityReducer, + removeOverlayBioEntityForGivenOverlayReducer, } from './overlayBioEntity.reducers'; import { OverlaysBioEntityState } from './overlayBioEntity.types'; @@ -13,11 +14,14 @@ const initialState: OverlaysBioEntityState = { export const overlayBioEntitySlice = createSlice({ name: 'overlayBioEntity', initialState, - reducers: {}, + reducers: { + removeOverlayBioEntityForGivenOverlay: removeOverlayBioEntityForGivenOverlayReducer, + }, extraReducers: builder => { getOverlayBioEntityReducer(builder); getOverlayBioEntityForAllModelsReducer(builder); }, }); +export const { removeOverlayBioEntityForGivenOverlay } = overlayBioEntitySlice.actions; export default overlayBioEntitySlice.reducer; diff --git a/src/redux/overlayBioEntity/overlayBioEntity.types.ts b/src/redux/overlayBioEntity/overlayBioEntity.types.ts index 43eeb895..4074058b 100644 --- a/src/redux/overlayBioEntity/overlayBioEntity.types.ts +++ b/src/redux/overlayBioEntity/overlayBioEntity.types.ts @@ -1,6 +1,15 @@ import { OverlayBioEntityRender } from '@/types/OLrendering'; +import { PayloadAction } from '@reduxjs/toolkit'; export type OverlaysBioEntityState = { overlaysId: number[]; - data: OverlayBioEntityRender[]; + data: { + [overlayId: number]: { + [modelId: number]: OverlayBioEntityRender[]; + }; + }; }; + +export type RemoveOverlayBioEntityForGivenOverlayPayload = { overlayId: number }; +export type RemoveOverlayBioEntityForGivenOverlayAction = + PayloadAction<RemoveOverlayBioEntityForGivenOverlayPayload>; -- GitLab