Skip to content
Snippets Groups Projects
Commit 6b498119 authored by Tadeusz Miesiąc's avatar Tadeusz Miesiąc Committed by Adrian Orłów
Browse files

feat: add legend for active overlays

parents fc11105a 56bb144c
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!91feat: add legend for active overlays
Pipeline #83842 passed
Showing
with 323 additions and 41 deletions
...@@ -47,30 +47,56 @@ describe('OverlayListItem - component', () => { ...@@ -47,30 +47,56 @@ describe('OverlayListItem - component', () => {
expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument();
}); });
it('should trigger view overlays on view button click and switch background to Empty if available', async () => { describe('view overlays', () => {
const OVERLAY_ID = 21; it('should trigger view overlays on view button click and switch background to Empty if available', async () => {
const { store } = renderComponent({ const OVERLAY_ID = 21;
map: initialMapStateFixture, const MODEL_ID = 5053;
backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK }, const { store } = renderComponent({
overlayBioEntity: OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK, map: initialMapStateFixture,
models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] }, backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK },
}); overlayBioEntity: OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK,
mockedAxiosNewClient models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] },
.onGet(apiPath.getOverlayBioEntity({ overlayId: OVERLAY_ID, modelId: 5053 })) });
.reply(HttpStatusCode.Ok, overlayBioEntityFixture); 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' }); expect(store.getState().map.data.backgroundId).toBe(EMPTY_BACKGROUND_ID);
await act(() => { expect(store.getState().overlayBioEntity.data).toEqual({
ViewButton.click(); [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([]);
expect(store.getState().overlayBioEntity.data).toEqual( expect(store.getState().overlayBioEntity.overlaysId).toEqual([]);
parseOverlayBioEntityToOlRenderingFormat(overlayBioEntityFixture, OVERLAY_ID), });
);
}); });
// TODO implement when connecting logic to component // TODO implement when connecting logic to component
it.skip('should trigger download overlay to PC on download button click', () => {}); it.skip('should trigger download overlay to PC on download button click', () => {});
}); });
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { getOverlayBioEntityForAllModels } from '@/redux/overlayBioEntity/overlayBioEntity.thunk';
import { Button } from '@/shared/Button'; import { Button } from '@/shared/Button';
import { useEmptyBackground } from './hooks/useEmptyBackground'; import { useOverlay } from './hooks/useOverlay';
interface OverlayListItemProps { interface OverlayListItemProps {
name: string; name: string;
...@@ -10,20 +8,14 @@ interface OverlayListItemProps { ...@@ -10,20 +8,14 @@ interface OverlayListItemProps {
export const OverlayListItem = ({ name, overlayId }: OverlayListItemProps): JSX.Element => { export const OverlayListItem = ({ name, overlayId }: OverlayListItemProps): JSX.Element => {
const onDownloadOverlay = (): void => {}; const onDownloadOverlay = (): void => {};
const dispatch = useAppDispatch(); const { toggleOverlay, isOverlayActive } = useOverlay(overlayId);
const { setBackgroundtoEmptyIfAvailable } = useEmptyBackground();
const onViewOverlay = (): void => {
setBackgroundtoEmptyIfAvailable();
dispatch(getOverlayBioEntityForAllModels({ overlayId }));
};
return ( return (
<li className="flex flex-row flex-nowrap justify-between pl-5 [&:not(:last-of-type)]:mb-4"> <li className="flex flex-row flex-nowrap justify-between pl-5 [&:not(:last-of-type)]:mb-4">
<span>{name}</span> <span>{name}</span>
<div className="flex flex-row flex-nowrap"> <div className="flex flex-row flex-nowrap">
<Button variantStyles="ghost" className="mr-4 max-h-8" onClick={onViewOverlay}> <Button variantStyles="ghost" className="mr-4 max-h-8" onClick={toggleOverlay}>
View {isOverlayActive ? 'Disable' : 'View'}
</Button> </Button>
<Button className="max-h-8" variantStyles="ghost" onClick={onDownloadOverlay}> <Button className="max-h-8" variantStyles="ghost" onClick={onDownloadOverlay}>
Download Download
......
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 };
};
import { DrawerHeading } from '@/shared/DrawerHeading'; import { DrawerHeading } from '@/shared/DrawerHeading';
import { GeneralOverlays } from './GeneralOverlays'; import { GeneralOverlays } from './GeneralOverlays';
import { OverlaysLegends } from './OverlaysLegends';
import { UserOverlays } from './UserOverlays'; import { UserOverlays } from './UserOverlays';
export const OverlaysDrawer = (): JSX.Element => { export const OverlaysDrawer = (): JSX.Element => {
...@@ -9,6 +10,7 @@ export const OverlaysDrawer = (): JSX.Element => { ...@@ -9,6 +10,7 @@ export const OverlaysDrawer = (): JSX.Element => {
<div className="h-[calc(100%-93px)] max-h-[calc(100%-93px)] overflow-y-auto"> <div className="h-[calc(100%-93px)] max-h-[calc(100%-93px)] overflow-y-auto">
<GeneralOverlays /> <GeneralOverlays />
<UserOverlays /> <UserOverlays />
<OverlaysLegends />
</div> </div>
</div> </div>
); );
......
import { BASE_API_URL, PROJECT_ID } from '@/constants';
import { overlayFixture } from '@/models/fixtures/overlaysFixture';
import { MapOverlay } from '@/types/models';
import { render, screen } from '@testing-library/react';
import { OverlaySingleLegend } from './OverlaySingleLegend.component';
const renderComponent = ({ overlay }: { overlay: MapOverlay }): void => {
render(<OverlaySingleLegend overlay={overlay} />);
};
describe('OverlaySingleLegend - component', () => {
beforeEach(() => {
renderComponent({
overlay: {
...overlayFixture,
name: 'overlay name',
idObject: 1234,
},
});
});
it('should render title with overlay name', () => {
expect(screen.getByText('overlay name')).toBeInTheDocument();
});
it('should render image with valid src and alt', () => {
const image = screen.getByAltText('overlay name legend');
expect(image).toBeInTheDocument();
expect(image.getAttribute('src')).toBe(
`${BASE_API_URL}/projects/${PROJECT_ID}/overlays/1234:downloadLegend`,
);
});
});
/* eslint-disable @next/next/no-img-element */
import { BASE_API_URL, PROJECT_ID } from '@/constants';
import { MapOverlay } from '@/types/models';
interface Props {
overlay: MapOverlay;
}
export const OverlaySingleLegend = ({ overlay }: Props): JSX.Element => {
const overlayName = overlay.name;
const overlayImageSrc = `${BASE_API_URL}/projects/${PROJECT_ID}/overlays/${overlay.idObject}:downloadLegend`;
return (
<div>
<p className="mb-5 text-sm font-semibold">{overlayName}</p>
<img src={overlayImageSrc} alt={`${overlayName} legend`} />
</div>
);
};
export { OverlaySingleLegend } from './OverlaySingleLegend.component';
import { BASE_API_URL, PROJECT_ID } from '@/constants';
import { overlaysFixture } from '@/models/fixtures/overlaysFixture';
import { StoreType } from '@/redux/store';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { activeOverlaysSelector } from '../../../../../redux/overlayBioEntity/overlayBioEntity.selector';
import { OverlaysLegends } from './OverlaysLegends.component';
jest.mock('../../../../../redux/overlayBioEntity/overlayBioEntity.selector', () => ({
activeOverlaysSelector: jest.fn(),
}));
const activeOverlaysSelectorMock = activeOverlaysSelector as unknown as jest.Mock;
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<OverlaysLegends />
</Wrapper>,
),
{
store,
}
);
};
describe('OverlaysLegends - component', () => {
describe('when active overlays are empty', () => {
beforeEach(() => {
activeOverlaysSelectorMock.mockImplementation(() => []);
renderComponent();
});
it('should not render list of overlays legends', () => {
expect(screen.getByTestId('overlays-legends')).toBeEmptyDOMElement();
});
});
describe('when active overlays are present', () => {
beforeEach(() => {
activeOverlaysSelectorMock.mockImplementation(() => overlaysFixture);
renderComponent();
});
it.each(overlaysFixture)('should render overlay legend', overlay => {
const image = screen.getByAltText(`${overlay.name} legend`);
expect(screen.getByText(overlay.name)).toBeInTheDocument();
expect(image).toBeInTheDocument();
expect(image.getAttribute('src')).toBe(
`${BASE_API_URL}/projects/${PROJECT_ID}/overlays/${overlay.idObject}:downloadLegend`,
);
});
});
});
import { activeOverlaysSelector } from '@/redux/overlayBioEntity/overlayBioEntity.selector';
import { useSelector } from 'react-redux';
import { OverlaySingleLegend } from './OverlaySingleLegend';
export const OverlaysLegends = (): JSX.Element => {
const overlays = useSelector(activeOverlaysSelector);
return (
<div className="p-6" data-testid="overlays-legends">
{overlays.map(overlay => (
<OverlaySingleLegend key={overlay.idObject} overlay={overlay} />
))}
</div>
);
};
export { OverlaysLegends } from './OverlaysLegends.component';
...@@ -14,7 +14,7 @@ export const UserOverlays = (): JSX.Element => { ...@@ -14,7 +14,7 @@ export const UserOverlays = (): JSX.Element => {
}; };
return ( return (
<div className="p-6"> <div className="border-b border-b-divide p-6">
{loadingUser === 'pending' && <h1>Loading</h1>} {loadingUser === 'pending' && <h1>Loading</h1>}
{loadingUser !== 'pending' && !authenticatedUser && ( {loadingUser !== 'pending' && !authenticatedUser && (
......
...@@ -8,3 +8,8 @@ export const overlaysFixture = createFixture(z.array(mapOverlay), { ...@@ -8,3 +8,8 @@ export const overlaysFixture = createFixture(z.array(mapOverlay), {
seed: ZOD_SEED, seed: ZOD_SEED,
array: { min: 2, max: 2 }, array: { min: 2, max: 2 },
}); });
export const overlayFixture = createFixture(mapOverlay, {
seed: ZOD_SEED,
array: { min: 1, max: 1 },
});
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),
);
});
});
});
import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
import { getOverlayBioEntity, getOverlayBioEntityForAllModels } from './overlayBioEntity.thunk'; import { getOverlayBioEntity, getOverlayBioEntityForAllModels } from './overlayBioEntity.thunk';
import { OverlaysBioEntityState } from './overlayBioEntity.types'; import {
OverlaysBioEntityState,
RemoveOverlayBioEntityForGivenOverlayAction,
} from './overlayBioEntity.types';
export const getOverlayBioEntityReducer = ( export const getOverlayBioEntityReducer = (
builder: ActionReducerMapBuilder<OverlaysBioEntityState>, builder: ActionReducerMapBuilder<OverlaysBioEntityState>,
): void => { ): void => {
builder.addCase(getOverlayBioEntity.fulfilled, (state, action) => { builder.addCase(getOverlayBioEntity.fulfilled, (state, action) => {
if (action.payload) { if (action.payload) {
state.overlaysId = [action.meta.arg.overlayId]; const { overlayId, modelId } = action.meta.arg;
state.data.push(...action.payload); if (!state.data[action.meta.arg.overlayId]) {
state.data[overlayId] = {};
}
state.data[overlayId][modelId] = action.payload;
} }
}); });
}; };
...@@ -16,7 +23,21 @@ export const getOverlayBioEntityReducer = ( ...@@ -16,7 +23,21 @@ export const getOverlayBioEntityReducer = (
export const getOverlayBioEntityForAllModelsReducer = ( export const getOverlayBioEntityForAllModelsReducer = (
builder: ActionReducerMapBuilder<OverlaysBioEntityState>, builder: ActionReducerMapBuilder<OverlaysBioEntityState>,
): void => { ): void => {
builder.addCase(getOverlayBioEntityForAllModels.pending, state => { builder.addCase(getOverlayBioEntityForAllModels.pending, (state, action) => {
state.data = []; 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];
};
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { rootSelector } from '../root/root.selectors';
import { currentModelIdSelector } from '../models/models.selectors'; import { currentModelIdSelector } from '../models/models.selectors';
import { overlaysDataSelector } from '../overlays/overlays.selectors';
import { rootSelector } from '../root/root.selectors';
export const overlayBioEntitySelector = createSelector( export const overlayBioEntitySelector = createSelector(
rootSelector, rootSelector,
...@@ -12,8 +13,29 @@ export const overlayBioEntityDataSelector = createSelector( ...@@ -12,8 +13,29 @@ export const overlayBioEntityDataSelector = createSelector(
overlayBioEntity => overlayBioEntity.data, 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( export const overlayBioEntitiesForCurrentModelSelector = createSelector(
overlayBioEntityDataSelector, overlayBioEntityDataSelector,
currentModelIdSelector, 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),
);
export const activeOverlaysSelector = createSelector(
rootSelector,
overlaysDataSelector,
(state, overlaysData) =>
overlaysData.filter(overlay => isOverlayActiveSelector(state, overlay.idObject)),
); );
...@@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; ...@@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit';
import { import {
getOverlayBioEntityForAllModelsReducer, getOverlayBioEntityForAllModelsReducer,
getOverlayBioEntityReducer, getOverlayBioEntityReducer,
removeOverlayBioEntityForGivenOverlayReducer,
} from './overlayBioEntity.reducers'; } from './overlayBioEntity.reducers';
import { OverlaysBioEntityState } from './overlayBioEntity.types'; import { OverlaysBioEntityState } from './overlayBioEntity.types';
...@@ -13,11 +14,14 @@ const initialState: OverlaysBioEntityState = { ...@@ -13,11 +14,14 @@ const initialState: OverlaysBioEntityState = {
export const overlayBioEntitySlice = createSlice({ export const overlayBioEntitySlice = createSlice({
name: 'overlayBioEntity', name: 'overlayBioEntity',
initialState, initialState,
reducers: {}, reducers: {
removeOverlayBioEntityForGivenOverlay: removeOverlayBioEntityForGivenOverlayReducer,
},
extraReducers: builder => { extraReducers: builder => {
getOverlayBioEntityReducer(builder); getOverlayBioEntityReducer(builder);
getOverlayBioEntityForAllModelsReducer(builder); getOverlayBioEntityForAllModelsReducer(builder);
}, },
}); });
export const { removeOverlayBioEntityForGivenOverlay } = overlayBioEntitySlice.actions;
export default overlayBioEntitySlice.reducer; export default overlayBioEntitySlice.reducer;
import { OverlayBioEntityRender } from '@/types/OLrendering'; import { OverlayBioEntityRender } from '@/types/OLrendering';
import { PayloadAction } from '@reduxjs/toolkit';
export type OverlaysBioEntityState = { export type OverlaysBioEntityState = {
overlaysId: number[]; overlaysId: number[];
data: OverlayBioEntityRender[]; data: {
[overlayId: number]: {
[modelId: number]: OverlayBioEntityRender[];
};
};
}; };
export type RemoveOverlayBioEntityForGivenOverlayPayload = { overlayId: number };
export type RemoveOverlayBioEntityForGivenOverlayAction =
PayloadAction<RemoveOverlayBioEntityForGivenOverlayPayload>;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment