Skip to content
Snippets Groups Projects
Commit 5f7f6092 authored by Adrian Orłów's avatar Adrian Orłów :fire:
Browse files

Merge branch 'MIN-178-modal-render-overview-image' into 'development'

feat: add overview image render layer (MIN-178)

Closes MIN-178

See merge request !75
parents 0567dff2 51c0cbf6
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...,!75feat: add overview image render layer (MIN-178)
Pipeline #83293 failed
Showing
with 593 additions and 7 deletions
export interface OverviewImageSize {
width: number;
height: number;
}
export interface ImageContainerSize {
width: number;
height: number;
}
import { BASE_MAP_IMAGES_URL } from '@/constants';
import { projectFixture } from '@/models/fixtures/projectFixture';
import { MODAL_INITIAL_STATE_MOCK } from '@/redux/modal/modal.mock';
import { PROJECT_OVERVIEW_IMAGE_MOCK } from '@/redux/project/project.mock';
import { StoreType } from '@/redux/store';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { OverviewImagesModal } from './OverviewImagesModal.component';
jest.mock('./utils/useOverviewImageSize', () => ({
__esModule: true,
useOverviewImageSize: jest.fn().mockImplementation(() => ({
width: 200,
height: 300,
})),
}));
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<OverviewImagesModal />
</Wrapper>,
),
{
store,
}
);
};
describe('OverviewImagesModal - component', () => {
describe('when currentImage is NOT valid', () => {
beforeEach(() => {
renderComponent({
project: {
data: {
...projectFixture,
overviewImageViews: [],
topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK,
},
loading: 'succeeded',
error: { message: '', name: '' },
},
modal: {
...MODAL_INITIAL_STATE_MOCK,
overviewImagesState: {
imageId: 0,
},
},
});
});
it('should not render component', () => {
const element = screen.queryByTestId('overview-images-modal');
expect(element).toBeNull();
});
});
describe('when currentImage is valid', () => {
beforeEach(() => {
renderComponent({
project: {
data: {
...projectFixture,
overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK],
topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK,
},
loading: 'succeeded',
error: { message: '', name: '' },
},
modal: {
...MODAL_INITIAL_STATE_MOCK,
overviewImagesState: {
imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject,
},
},
});
});
it('should render component', () => {
const element = screen.queryByTestId('overview-images-modal');
expect(element).not.toBeNull();
});
it('should render image with valid src', () => {
const imageElement = screen.getByAltText('overview');
const result = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`;
expect(imageElement.getAttribute('src')).toBe(result);
});
it('should render image wrapper with valid size', () => {
const imageElement = screen.getByAltText('overview');
const wrapperElement = imageElement.closest('div');
const wrapperStyle = wrapperElement?.getAttribute('style');
expect(wrapperStyle).toBe('width: 200px; height: 300px;');
});
});
});
/* eslint-disable @next/next/no-img-element */
import * as React from 'react';
import { useCallback, useState } from 'react';
import { useOverviewImage } from './utils/useOverviewImage';
export const OverviewImagesModal: React.FC = () => {
return <div className="h-[200px] w-[500px] bg-white " />;
const [containerRect, setContainerRect] = useState<DOMRect>();
const { imageUrl, size } = useOverviewImage({ containerRect });
const { width, height } = size;
const handleRect = useCallback((node: HTMLDivElement | null) => {
if (!node) {
return;
}
setContainerRect(node.getBoundingClientRect());
}, []);
if (!imageUrl) {
return null;
}
return (
<div
data-testid="overview-images-modal"
className="flex h-full w-full items-center justify-center bg-white"
ref={handleRect}
>
<div className="relative" style={{ width, height }}>
<img alt="overview" className="block h-full w-full" src={imageUrl} />
{/* TODO: interactions - clickable elements (in next task) */}
</div>
</div>
);
};
import { getFinalImageSize } from './getFinalImageSize';
describe('getFinalImageSize - util', () => {
const cases = [
[
{ width: 0, height: 0 },
{ width: 0, height: 0 },
{ width: 0, height: 0, sizeFactor: 0 },
],
[
{ width: 100, height: 100 },
{ width: 100, height: 100 },
{ width: 100, height: 100, sizeFactor: 1 },
],
[
{ width: 100, height: 100 },
{ width: 200, height: 250 },
{ width: 80, height: 100, sizeFactor: 0.4 },
],
[
{ width: 10, height: 40 },
{ width: 40, height: 60 },
{ width: 10, height: 15, sizeFactor: 0.25 },
],
];
it.each(cases)(
'should return valid size and size factor',
(containerSize, maxImageSize, finalSize) => {
expect(getFinalImageSize(containerSize, maxImageSize)).toStrictEqual(finalSize);
},
);
});
import { ZERO } from '@/constants/common';
import { ImageContainerSize, OverviewImageSize } from '../OverviewImageModal.types';
interface GetFinalImageSizeResult extends OverviewImageSize {
sizeFactor: number;
}
export const getFinalImageSize = (
containerSize: ImageContainerSize,
maxImageSize: OverviewImageSize,
): GetFinalImageSizeResult => {
const maxHeight = Math.min(containerSize.height, maxImageSize.height);
const maxWidth = Math.min(containerSize.width, maxImageSize.width);
const heightSizeFactor = maxHeight / maxImageSize.height;
const widthSizeFactor = maxWidth / maxImageSize.width;
const sizeFactor = Math.min(heightSizeFactor, widthSizeFactor);
const width = maxImageSize.width * sizeFactor;
const height = maxImageSize.height * sizeFactor;
return {
height: height || ZERO,
width: width || ZERO,
sizeFactor: sizeFactor || ZERO,
};
};
import { BASE_MAP_IMAGES_URL } from '@/constants';
import { DEFAULT_OVERVIEW_IMAGE_SIZE } from '@/constants/project';
import { projectFixture } from '@/models/fixtures/projectFixture';
import { MODAL_INITIAL_STATE_MOCK } from '@/redux/modal/modal.mock';
import { PROJECT_OVERVIEW_IMAGE_MOCK } from '@/redux/project/project.mock';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { renderHook } from '@testing-library/react';
import { useOverviewImage } from './useOverviewImage';
describe('useOverviewImage - hook', () => {
describe('when image data is invalid', () => {
const { Wrapper } = getReduxWrapperWithStore({
project: {
data: {
...projectFixture,
overviewImageViews: [],
topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK,
},
loading: 'succeeded',
error: { message: '', name: '' },
},
modal: {
...MODAL_INITIAL_STATE_MOCK,
overviewImagesState: {
imageId: 0,
},
},
});
const { result } = renderHook(() => useOverviewImage({ containerRect: undefined }), {
wrapper: Wrapper,
});
it('should return default size of image and empty imageUrl', () => {
expect(result.current).toStrictEqual({
imageUrl: '',
size: DEFAULT_OVERVIEW_IMAGE_SIZE,
});
});
});
describe('when containerReact is undefined', () => {
const { Wrapper } = getReduxWrapperWithStore({
project: {
data: {
...projectFixture,
overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK],
topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK,
},
loading: 'succeeded',
error: { message: '', name: '' },
},
modal: {
...MODAL_INITIAL_STATE_MOCK,
overviewImagesState: {
imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject,
},
},
});
const { result } = renderHook(() => useOverviewImage({ containerRect: undefined }), {
wrapper: Wrapper,
});
it('should return default size of image and valid imageUrl', () => {
const imageUrl = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`;
expect(result.current).toStrictEqual({
imageUrl,
size: DEFAULT_OVERVIEW_IMAGE_SIZE,
});
});
});
describe('when containerReact is valid', () => {
const { Wrapper } = getReduxWrapperWithStore({
project: {
data: {
...projectFixture,
overviewImageViews: [
{
...PROJECT_OVERVIEW_IMAGE_MOCK,
height: 500,
width: 500,
},
],
topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK,
},
loading: 'succeeded',
error: { message: '', name: '' },
},
modal: {
...MODAL_INITIAL_STATE_MOCK,
overviewImagesState: {
imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject,
},
},
});
const { result } = renderHook(
() => useOverviewImage({ containerRect: { width: 100, height: 200 } as DOMRect }),
{
wrapper: Wrapper,
},
);
it('should return size of image and valid imageUrl', () => {
const imageUrl = `${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`;
expect(result.current).toStrictEqual({
imageUrl,
size: { height: 100, width: 100, sizeFactor: 0.2 },
});
});
});
});
import { OverviewImageSize } from '../OverviewImageModal.types';
import { useOverviewImageSize } from './useOverviewImageSize';
import { useOverviewImageUrl } from './useOverviewImageUrl';
interface UseOverviewImageArgs {
containerRect?: DOMRect;
}
interface UseOverviewImageResults {
imageUrl: string;
size: OverviewImageSize;
}
export const useOverviewImage = ({
containerRect,
}: UseOverviewImageArgs): UseOverviewImageResults => {
const imageUrl = useOverviewImageUrl();
const size = useOverviewImageSize({ containerRect });
return {
size,
imageUrl,
};
};
/* eslint-disable no-magic-numbers */
import { DEFAULT_OVERVIEW_IMAGE_SIZE } from '@/constants/project';
import { projectFixture } from '@/models/fixtures/projectFixture';
import { MODAL_INITIAL_STATE_MOCK } from '@/redux/modal/modal.mock';
import { PROJECT_OVERVIEW_IMAGE_MOCK } from '@/redux/project/project.mock';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { renderHook } from '@testing-library/react';
import { useOverviewImageSize } from './useOverviewImageSize';
describe('useOverviewImageSize - hook', () => {
describe('when currentImage is not valid', () => {
const { Wrapper } = getReduxWrapperWithStore({
project: {
data: {
...projectFixture,
overviewImageViews: [],
topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK,
},
loading: 'succeeded',
error: { message: '', name: '' },
},
modal: {
...MODAL_INITIAL_STATE_MOCK,
overviewImagesState: {
imageId: 0,
},
},
});
const { result } = renderHook(
() => useOverviewImageSize({ containerRect: { width: 800, height: 600 } as DOMRect }),
{
wrapper: Wrapper,
},
);
it('should return default value', () => {
expect(result.current).toStrictEqual(DEFAULT_OVERVIEW_IMAGE_SIZE);
});
});
describe('when containerRect is not valid', () => {
const { Wrapper } = getReduxWrapperWithStore({
project: {
data: {
...projectFixture,
overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK],
topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK,
},
loading: 'succeeded',
error: { message: '', name: '' },
},
modal: {
...MODAL_INITIAL_STATE_MOCK,
overviewImagesState: {
imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject,
},
},
});
const { result } = renderHook(() => useOverviewImageSize({ containerRect: undefined }), {
wrapper: Wrapper,
});
it('should return default value', () => {
expect(result.current).toStrictEqual(DEFAULT_OVERVIEW_IMAGE_SIZE);
});
});
describe('when data is valid', () => {
const { Wrapper } = getReduxWrapperWithStore({
project: {
data: {
...projectFixture,
overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK],
topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK,
},
loading: 'succeeded',
error: { message: '', name: '' },
},
modal: {
...MODAL_INITIAL_STATE_MOCK,
overviewImagesState: {
imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject,
},
},
});
const { result } = renderHook(
() => useOverviewImageSize({ containerRect: { width: 1600, height: 1000 } as DOMRect }),
{
wrapper: Wrapper,
},
);
const { height, width, sizeFactor } = result.current;
it('should return calculated height, width, sizeFactor', () => {
expect(height).toBeCloseTo(1000);
expect(width).toBeCloseTo(1429.7);
expect(sizeFactor).toBeCloseTo(0.247);
});
});
});
import { DEFAULT_OVERVIEW_IMAGE_SIZE } from '@/constants/project';
import { currentOverviewImageSelector } from '@/redux/project/project.selectors';
import { useSelector } from 'react-redux';
import { OverviewImageSize } from '../OverviewImageModal.types';
import { getFinalImageSize } from './getFinalImageSize';
interface UseOverviewImageArgs {
containerRect?: DOMRect;
}
interface UseOverviewImageResult extends OverviewImageSize {
sizeFactor: number;
}
export const useOverviewImageSize = ({
containerRect,
}: UseOverviewImageArgs): UseOverviewImageResult => {
const currentImage = useSelector(currentOverviewImageSelector);
if (!currentImage || !containerRect) return DEFAULT_OVERVIEW_IMAGE_SIZE;
const maxImageSize = {
width: currentImage.width,
height: currentImage.height,
};
return getFinalImageSize(containerRect, maxImageSize);
};
import { BASE_MAP_IMAGES_URL } from '@/constants';
import { projectFixture } from '@/models/fixtures/projectFixture';
import { MODAL_INITIAL_STATE_MOCK } from '@/redux/modal/modal.mock';
import { PROJECT_OVERVIEW_IMAGE_MOCK } from '@/redux/project/project.mock';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { renderHook } from '@testing-library/react';
import { useOverviewImageUrl } from './useOverviewImageUrl';
describe('useOverviewImageUrl - hook', () => {
describe('when currentImage data is valid', () => {
const { Wrapper } = getReduxWrapperWithStore({
project: {
data: {
...projectFixture,
overviewImageViews: [],
topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK,
},
loading: 'succeeded',
error: { message: '', name: '' },
},
modal: {
...MODAL_INITIAL_STATE_MOCK,
overviewImagesState: {
imageId: 0,
},
},
});
it('should return valid url', () => {
const { result } = renderHook(() => useOverviewImageUrl(), { wrapper: Wrapper });
expect(result.current).toBe('');
});
});
describe('when currentImage data is valid', () => {
const { Wrapper } = getReduxWrapperWithStore({
project: {
data: {
...projectFixture,
overviewImageViews: [PROJECT_OVERVIEW_IMAGE_MOCK],
topOverviewImage: PROJECT_OVERVIEW_IMAGE_MOCK,
},
loading: 'succeeded',
error: { message: '', name: '' },
},
modal: {
...MODAL_INITIAL_STATE_MOCK,
overviewImagesState: {
imageId: PROJECT_OVERVIEW_IMAGE_MOCK.idObject,
},
},
});
it('should return valid url', () => {
const { result } = renderHook(() => useOverviewImageUrl(), { wrapper: Wrapper });
expect(result.current).toBe(
`${BASE_MAP_IMAGES_URL}/map_images/${PROJECT_OVERVIEW_IMAGE_MOCK.filename}`,
);
});
});
});
import { BASE_MAP_IMAGES_URL } from '@/constants';
import { currentOverviewImageSelector } from '@/redux/project/project.selectors';
import { useSelector } from 'react-redux';
export const useOverviewImageUrl = (): string => {
const currentImage = useSelector(currentOverviewImageSelector);
if (!currentImage) {
return '';
}
return `${BASE_MAP_IMAGES_URL}/map_images/${currentImage.filename}`;
};
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { openOverviewImagesModal } from '@/redux/modal/modal.slice';
import { openOverviewImagesModalById } from '@/redux/modal/modal.slice';
import { projectDefaultOverviewImageIdSelector } from '@/redux/project/project.selectors';
import { Button } from '@/shared/Button';
import { useSelector } from 'react-redux';
import { twMerge } from 'tailwind-merge';
import { BackgroundSelector } from './BackgroundsSelector';
......@@ -8,9 +10,10 @@ import { BackgroundSelector } from './BackgroundsSelector';
export const MapAdditionalOptions = (): JSX.Element => {
const dispatch = useAppDispatch();
const defaultOverviewImageId = useSelector(projectDefaultOverviewImageIdSelector);
const handleBrowseOverviewImagesClick = (): void => {
dispatch(openOverviewImagesModal());
dispatch(openOverviewImagesModalById(defaultOverviewImageId));
};
return (
......
export const DEFAULT_OVERVIEW_IMAGE_WIDTH = 800;
export const DEFAULT_OVERVIEW_IMAGE_HEIGHT = 500;
export const DEFAULT_OVERVIEW_IMAGE_SIZE = {
width: DEFAULT_OVERVIEW_IMAGE_WIDTH,
height: DEFAULT_OVERVIEW_IMAGE_HEIGHT,
sizeFactor: 1,
};
......@@ -20,4 +20,5 @@ export const projectSchema = z.object({
creationDate: z.string(),
mapCanvasType: z.string(),
overviewImageViews: z.array(overviewImageView),
topOverviewImage: overviewImageView,
});
......@@ -14,6 +14,8 @@ export const MODEL_ID_DEFAULT: number = 0;
export const BACKGROUND_ID_DEFAULT: number = 0;
export const OVERVIEW_IMAGE_ID_DEFAULT: number = 0;
export const MAP_DATA_INITIAL_STATE: MapData = {
projectId: PROJECT_ID,
meshId: '',
......
import { OVERVIEW_IMAGE_ID_DEFAULT } from '../map/map.constants';
import { ModalState } from './modal.types';
export const MODAL_INITIAL_STATE: ModalState = {
isOpen: false,
modalName: 'none',
modalTitle: '',
overviewImagesState: {
imageId: OVERVIEW_IMAGE_ID_DEFAULT,
},
};
import { OVERVIEW_IMAGE_ID_DEFAULT } from '../map/map.constants';
import { ModalState } from './modal.types';
export const MODAL_INITIAL_STATE_MOCK: ModalState = {
isOpen: false,
modalName: 'none',
modalTitle: '',
overviewImagesState: {
imageId: OVERVIEW_IMAGE_ID_DEFAULT,
},
};
......@@ -12,8 +12,14 @@ export const closeModalReducer = (state: ModalState): void => {
state.modalName = 'none';
};
export const openOverviewImagesModalReducer = (state: ModalState): void => {
export const openOverviewImagesModalByIdReducer = (
state: ModalState,
action: PayloadAction<number>,
): void => {
state.isOpen = true;
state.modalName = 'overview-images';
state.modalTitle = 'Overview images';
state.overviewImagesState = {
imageId: action.payload,
};
};
import { createSelector } from '@reduxjs/toolkit';
import { OVERVIEW_IMAGE_ID_DEFAULT } from '../map/map.constants';
import { rootSelector } from '../root/root.selectors';
export const modalSelector = createSelector(rootSelector, state => state.modal);
export const isModalOpenSelector = createSelector(modalSelector, state => state.isOpen);
export const currentOverviewImageId = createSelector(
modalSelector,
modal => modal?.overviewImagesState.imageId || OVERVIEW_IMAGE_ID_DEFAULT,
);
......@@ -3,7 +3,7 @@ import { MODAL_INITIAL_STATE } from './modal.constants';
import {
closeModalReducer,
openModalReducer,
openOverviewImagesModalReducer,
openOverviewImagesModalByIdReducer,
} from './modal.reducers';
const modalSlice = createSlice({
......@@ -12,10 +12,10 @@ const modalSlice = createSlice({
reducers: {
openModal: openModalReducer,
closeModal: closeModalReducer,
openOverviewImagesModal: openOverviewImagesModalReducer,
openOverviewImagesModalById: openOverviewImagesModalByIdReducer,
},
});
export const { openModal, closeModal, openOverviewImagesModal } = modalSlice.actions;
export const { openModal, closeModal, openOverviewImagesModalById } = modalSlice.actions;
export default modalSlice.reducer;
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