From 4a9d3eb1e717fcb78bd61cc59f53219b6b77b5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Mon, 17 Feb 2025 12:16:14 +0100 Subject: [PATCH 1/8] feat(layer-text): add transform interaction for layer text and delete functionality --- ...eObjectEditFactoryModal.component.test.tsx | 38 ++- ...rImageObjectEditFactoryModal.component.tsx | 21 +- .../LayerDrawerTextItem.component.tsx | 70 +++- .../LayersDrawer/LayersDrawer.component.tsx | 4 +- .../LayersDrawerImageItem.component.tsx | 4 +- .../LayersDrawerObjectsList.component.tsx | 147 +++++--- .../Map/MapViewer/MapViewer.component.tsx | 8 +- .../useOlMapAdditionalLayers.ts | 34 +- .../utils/shapes/elements/Glyph/Glyph.ts | 10 +- .../Map/MapViewer/utils/shapes/layer/Layer.ts | 56 ++-- .../Glyph => layer/elements}/LayerImage.ts | 33 +- .../elements/LayerText.test.ts} | 28 +- .../utils/shapes/layer/elements/LayerText.ts | 315 ++++++++++++++++++ ...est.ts => getTransformInteraction.test.ts} | 6 +- ...eraction.ts => getTransformInteraction.ts} | 40 +-- .../utils/updateElement.ts} | 12 +- .../Map/MapViewer/utils/shapes/text/Text.ts | 145 -------- .../utils/shapes/text/getTextCoords.ts | 9 +- .../utils/websocket/processLayerImage.ts | 4 +- .../utils/websocket/processLayerText.ts | 33 +- src/models/layerTextSchema.ts | 4 +- src/redux/apiPath.ts | 4 + src/redux/layers/layers.reducers.ts | 32 ++ src/redux/layers/layers.slice.ts | 6 + src/redux/layers/layers.thunks.ts | 86 ++++- src/redux/mapEditTools/mapEditTools.mock.ts | 2 +- .../mapEditTools/mapEditTools.reducers.ts | 6 +- .../mapEditTools/mapEditTools.selectors.ts | 4 +- src/redux/mapEditTools/mapEditTools.types.ts | 4 +- 29 files changed, 818 insertions(+), 347 deletions(-) rename src/components/Map/MapViewer/utils/shapes/{elements/Glyph => layer/elements}/LayerImage.ts (54%) rename src/components/Map/MapViewer/utils/shapes/{text/Text.test.ts => layer/elements/LayerText.test.ts} (75%) create mode 100644 src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts rename src/components/Map/MapViewer/utils/shapes/layer/interaction/{getTransformImageInteraction.test.ts => getTransformInteraction.test.ts} (85%) rename src/components/Map/MapViewer/utils/shapes/layer/interaction/{getTransformImageInteraction.ts => getTransformInteraction.ts} (76%) rename src/components/Map/MapViewer/utils/shapes/{elements/Glyph/updateGlyph.ts => layer/utils/updateElement.ts} (60%) delete mode 100644 src/components/Map/MapViewer/utils/shapes/text/Text.ts diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx index 0c0973f2..29a26c8b 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx @@ -81,7 +81,13 @@ const renderComponent = ( describe('LayerImageObjectEditFactoryModal - component', () => { it('should render LayerImageObjectEditFactoryModal component with initial state', () => { - renderComponent(); + renderComponent({ + activeAction: null, + layerObject: { + ...layerImageFixture, + glyph: null, + }, + }); expect(screen.getByText(/Glyph:/i)).toBeInTheDocument(); expect(screen.getByText(/File:/i)).toBeInTheDocument(); @@ -90,7 +96,13 @@ describe('LayerImageObjectEditFactoryModal - component', () => { }); it('should display a list of glyphs in the dropdown', async () => { - renderComponent(); + renderComponent({ + activeAction: null, + layerObject: { + ...layerImageFixture, + glyph: null, + }, + }); const dropdown = screen.getByTestId('autocomplete'); if (!dropdown.firstChild) { @@ -102,7 +114,13 @@ describe('LayerImageObjectEditFactoryModal - component', () => { }); it('should update the selected glyph on dropdown change', async () => { - renderComponent(); + renderComponent({ + activeAction: null, + layerObject: { + ...layerImageFixture, + glyph: null, + }, + }); const dropdown = screen.getByTestId('autocomplete'); if (!dropdown.firstChild) { @@ -142,13 +160,13 @@ describe('LayerImageObjectEditFactoryModal - component', () => { }; const getGlyphDataMock = jest.fn(() => glyphData); jest.spyOn(layerObjectFeature, 'get').mockImplementation(key => { - if (key === 'update') return (): void => {}; - if (key === 'getGlyphData') return getGlyphDataMock; + if (key === 'updateElement') return (): void => {}; + if (key === 'getObjectData') return getGlyphDataMock; return undefined; }); renderComponent({ activeAction: MAP_EDIT_ACTIONS.TRANSFORM_IMAGE, - layerImageObject: glyphData, + layerObject: glyphData, }); const submitButton = screen.getByText(/Submit/i); @@ -164,7 +182,13 @@ describe('LayerImageObjectEditFactoryModal - component', () => { }); it('should display "No Image" when there is no image file', () => { - const { store } = renderComponent(); + const { store } = renderComponent({ + activeAction: null, + layerObject: { + ...layerImageFixture, + glyph: null, + }, + }); store.dispatch({ type: 'glyphs/clearGlyphData', diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx index e4dfcdd0..dc280845 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; import { LayerImageObjectForm } from '@/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { addGlyph } from '@/redux/glyphs/glyphs.thunks'; @@ -13,24 +13,25 @@ import { showToast } from '@/utils/showToast'; import { closeModal } from '@/redux/modal/modal.slice'; import { SerializedError } from '@reduxjs/toolkit'; import { useMapInstance } from '@/utils/context/mapInstanceContext'; -import updateGlyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph'; +import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; export const LayerImageObjectEditFactoryModal: React.FC = () => { - const layerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); + const layerObject = useAppSelector(mapEditToolsLayerObjectSelector); const { mapInstance } = useMapInstance(); + if (!layerObject || !('glyph' in layerObject)) { + throw new Error('Invalid layer image object'); + } const currentModelId = useAppSelector(currentModelIdSelector); const dispatch = useAppDispatch(); - const [selectedGlyph, setSelectedGlyph] = useState<number | null>( - layerImageObject?.glyph || null, - ); + const [selectedGlyph, setSelectedGlyph] = useState<number | null>(layerObject?.glyph || null); const [file, setFile] = useState<File | null>(null); const [isSending, setIsSending] = useState<boolean>(false); const handleSubmit = async (): Promise<void> => { - if (!layerImageObject) { + if (!layerObject) { return; } setIsSending(true); @@ -47,8 +48,8 @@ export const LayerImageObjectEditFactoryModal: React.FC = () => { const layerImage = await dispatch( updateLayerImageObject({ modelId: currentModelId, - layerId: layerImageObject.layer, - ...layerImageObject, + layerId: layerObject.layer, + ...layerObject, glyph: glyphId, }), ).unwrap(); @@ -57,7 +58,7 @@ export const LayerImageObjectEditFactoryModal: React.FC = () => { layerUpdateImage({ modelId: currentModelId, layerId: layerImage.layer, layerImage }), ); dispatch(mapEditToolsSetLayerObject(layerImage)); - updateGlyph(mapInstance, layerImage.layer, layerImage); + updateElement(mapInstance, layerImage.layer, layerImage); } showToast({ type: 'success', diff --git a/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx index 12a7959c..6f57c762 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx @@ -1,18 +1,80 @@ -import { JSX } from 'react'; +import React, { JSX, useMemo } from 'react'; import { LayerText } from '@/types/models'; import { Icon } from '@/shared/Icon'; +import { LayersDrawerObjectActions } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; +import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; interface LayersDrawerTextItemProps { layerText: LayerText; + bringToFront: () => void; + bringToBack: () => void; + removeObject: () => void; + centerObject: () => void; + editObject: () => void; + isLayerVisible: boolean; + isLayerActive: boolean; } export const LayersDrawerTextItem = ({ layerText, + bringToFront, + bringToBack, + removeObject, + centerObject, + editObject, + isLayerVisible, + isLayerActive, }: LayersDrawerTextItemProps): JSX.Element | null => { + const dispatch = useAppDispatch(); + const activeLayerObject = useAppSelector(mapEditToolsLayerObjectSelector); + const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); + + const showActions = useMemo(() => { + return activeLayerObject?.id === layerText.id; + }, [activeLayerObject?.id, layerText.id]); + + const canSelectItem = useMemo(() => { + return isLayerVisible && isLayerActive && hasPrivilegeToWriteProject; + }, [isLayerVisible, isLayerActive, hasPrivilegeToWriteProject]); + + const selectItem = useMemo(() => { + return (): void => { + if (canSelectItem) { + dispatch(mapEditToolsSetLayerObject(layerText)); + } + }; + }, [canSelectItem, dispatch, layerText]); + + const handleKeyPress = (): void => {}; + return ( - <div className="flex min-h-[24px] gap-2"> - <Icon name="text" className="shrink-0" /> - <span className="truncate">{layerText.notes}</span> + <div + className="flex min-h-[24px] items-center justify-between gap-2" + id={`layer-text-item-${layerText.id}`} + > + <div + className={`flex gap-2 ${canSelectItem ? 'cursor-pointer' : 'cursor-default'}`} + onClick={selectItem} + tabIndex={0} + onKeyDown={handleKeyPress} + role="button" + > + <Icon name="text" className="shrink-0" /> + <span className="truncate">{layerText.notes}</span> + </div> + {showActions && ( + <LayersDrawerObjectActions + bringToFront={bringToFront} + bringToBack={bringToBack} + removeObject={removeObject} + centerObject={centerObject} + editObject={editObject} + /> + )} </div> ); }; diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx index df941aca..12bef081 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx @@ -8,14 +8,14 @@ import { JSX, useEffect, useRef } from 'react'; import { openLayerFactoryModal } from '@/redux/modal/modal.slice'; import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; import { LayersDrawerLayer } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component'; -import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; export const LayersDrawer = (): JSX.Element => { const layersForCurrentModel = useAppSelector(layersForCurrentModelSelector); const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); const dispatch = useAppDispatch(); const layersDrawerRef = useRef<HTMLDivElement>(null); - const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); + const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerObjectSelector); const addNewLayer = (): void => { dispatch(openLayerFactoryModal()); diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx index d2884a5b..0622f141 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx @@ -4,7 +4,7 @@ import { Icon } from '@/shared/Icon'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { glyphFileNameByIdSelector } from '@/redux/glyphs/glyphs.selectors'; import { LayersDrawerObjectActions } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component'; -import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; @@ -31,7 +31,7 @@ export const LayersDrawerImageItem = ({ isLayerActive, }: LayersDrawerImageItemProps): JSX.Element | null => { const dispatch = useAppDispatch(); - const activeLayerImage = useAppSelector(mapEditToolsLayerImageObjectSelector); + const activeLayerImage = useAppSelector(mapEditToolsLayerObjectSelector); const fileName = useAppSelector(state => glyphFileNameByIdSelector(state, layerImage.glyph)); const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx index fca7a858..fa98e051 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx @@ -9,19 +9,23 @@ import { JSX, useState } from 'react'; import { LayersDrawerImageItem } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component'; import { LayersDrawerTextItem } from '@/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component'; import QuestionModal from '@/components/FunctionalArea/Modal/QuestionModal/QustionModal.component'; -import { removeLayerImage, updateLayerImageObject } from '@/redux/layers/layers.thunks'; -import { layerDeleteImage, layerUpdateImage } from '@/redux/layers/layers.slice'; +import { + removeLayerImage, + removeLayerText, + updateLayerImageObject, +} from '@/redux/layers/layers.thunks'; +import { layerDeleteImage, layerDeleteText, layerUpdateImage } from '@/redux/layers/layers.slice'; import removeElementFromLayer from '@/components/Map/MapViewer/utils/shapes/elements/removeElementFromLayer'; import { showToast } from '@/utils/showToast'; import { SerializedError } from '@reduxjs/toolkit'; -import { LayerImage } from '@/types/models'; +import { LayerImage, LayerText } from '@/types/models'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useMapInstance } from '@/utils/context/mapInstanceContext'; import { mapModelIdSelector } from '@/redux/map/map.selectors'; import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; -import updateGlyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph'; +import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; import { useSetBounds } from '@/utils/map/useSetBounds'; -import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { Coordinate } from 'ol/coordinate'; import { openLayerImageObjectEditFactoryModal } from '@/redux/modal/modal.slice'; @@ -32,6 +36,19 @@ interface LayersDrawerObjectsListProps { isLayerActive: boolean; } +const removeObjectConfig = { + image: { + question: 'Are you sure you want to remove the image?', + successMessage: 'The layer image has been successfully removed', + errorMessage: 'An error occurred while removing the layer text', + }, + text: { + question: 'Are you sure you want to remove the text?', + successMessage: 'The layer text has been successfully removed', + errorMessage: 'An error occurred while removing the layer text', + }, +}; + export const LayersDrawerObjectsList = ({ layerId, isLayerVisible, @@ -41,57 +58,81 @@ export const LayersDrawerObjectsList = ({ const highestZIndex = useAppSelector(highestZIndexSelector); const lowestZIndex = useAppSelector(lowestZIndexSelector); const layer = useAppSelector(state => layerByIdSelector(state, layerId)); - const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); - const [isImageRemoveModalOpen, setIsImageRemoveModalOpen] = useState(false); - const [layerImageToRemove, setLayerImageToRemove] = useState<LayerImage | null>(null); + const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerObjectSelector); + const [removeModalState, setRemoveModalState] = useState<undefined | 'text' | 'image'>(undefined); + const [layerObjectToRemove, setLayerObjectToRemove] = useState<LayerImage | LayerText | null>( + null, + ); const dispatch = useAppDispatch(); const setBounds = useSetBounds(); const pointToProjection = usePointToProjection(); const { mapInstance } = useMapInstance(); - const removeImage = (layerImage: LayerImage): void => { - setLayerImageToRemove(layerImage); - setIsImageRemoveModalOpen(true); + const removeObject = (layerObject: LayerImage | LayerText): void => { + setLayerObjectToRemove(layerObject); + if ('glyph' in layerObject) { + setRemoveModalState('image'); + } else { + setRemoveModalState('text'); + } }; - const rejectImageRemove = (): void => { - setIsImageRemoveModalOpen(false); + const rejectRemove = (): void => { + setRemoveModalState(undefined); }; - const confirmImageRemove = async (): Promise<void> => { - if (!layerImageToRemove) { + const confirmRemove = async (): Promise<void> => { + if (!layerObjectToRemove || !removeModalState) { return; } + try { - await dispatch( - removeLayerImage({ - modelId: currentModelId, - layerId: layerImageToRemove.layer, - imageId: layerImageToRemove.id, - }), - ).unwrap(); - dispatch( - layerDeleteImage({ - modelId: currentModelId, - layerId: layerImageToRemove.layer, - imageId: layerImageToRemove.id, - }), - ); + if (removeModalState === 'text') { + await dispatch( + removeLayerText({ + modelId: currentModelId, + layerId: layerObjectToRemove.layer, + textId: layerObjectToRemove.id, + }), + ).unwrap(); + dispatch( + layerDeleteText({ + modelId: currentModelId, + layerId: layerObjectToRemove.layer, + textId: layerObjectToRemove.id, + }), + ); + } else { + await dispatch( + removeLayerImage({ + modelId: currentModelId, + layerId: layerObjectToRemove.layer, + imageId: layerObjectToRemove.id, + }), + ).unwrap(); + dispatch( + layerDeleteImage({ + modelId: currentModelId, + layerId: layerObjectToRemove.layer, + imageId: layerObjectToRemove.id, + }), + ); + } removeElementFromLayer({ mapInstance, - layerId: layerImageToRemove.layer, - featureId: layerImageToRemove.id, + layerId: layerObjectToRemove.layer, + featureId: layerObjectToRemove.id, }); showToast({ type: 'success', - message: 'The layer image has been successfully removed', + message: removeObjectConfig[removeModalState].successMessage, }); - setIsImageRemoveModalOpen(false); + setRemoveModalState(undefined); } catch (error) { const typedError = error as SerializedError; showToast({ type: 'error', - message: typedError.message || 'An error occurred while removing the layer image', + message: typedError.message || removeObjectConfig[removeModalState].errorMessage, }); } }; @@ -120,7 +161,7 @@ export const LayersDrawerObjectsList = ({ }), ); dispatch(mapEditToolsSetLayerObject(newLayerImage)); - updateGlyph(mapInstance, newLayerImage.layer, newLayerImage); + updateElement(mapInstance, newLayerImage.layer, newLayerImage); } }; @@ -132,12 +173,12 @@ export const LayersDrawerObjectsList = ({ await updateImageZIndex({ zIndex: lowestZIndex - 1, layerImage }); }; - const centerObject = (layerImage: LayerImage): void => { - if (mapEditToolsLayerImageObject && mapEditToolsLayerImageObject.id === layerImage.id) { - const point1 = pointToProjection({ x: layerImage.x, y: layerImage.y }); + const centerObject = (layerObject: LayerImage | LayerText): void => { + if (mapEditToolsLayerImageObject && mapEditToolsLayerImageObject.id === layerObject.id) { + const point1 = pointToProjection({ x: layerObject.x, y: layerObject.y }); const point2 = pointToProjection({ - x: layerImage.x + layerImage.width, - y: layerImage.y + layerImage.height, + x: layerObject.x + layerObject.width, + y: layerObject.y + layerObject.height, }); setBounds([point1, point2] as Coordinate[]); } @@ -154,13 +195,27 @@ export const LayersDrawerObjectsList = ({ return ( <div className={`${isLayerVisible ? 'opacity-100' : 'opacity-40'} flex flex-col gap-1 ps-3`}> <QuestionModal - isOpen={isImageRemoveModalOpen} - onClose={rejectImageRemove} - onConfirm={confirmImageRemove} - question="Are you sure you want to remove the image?" + isOpen={Boolean(removeModalState)} + onClose={rejectRemove} + onConfirm={confirmRemove} + question={ + removeModalState + ? removeObjectConfig[removeModalState].question + : 'Are you sure you want to remove the object' + } /> {Object.values(layer.texts).map(layerText => ( - <LayersDrawerTextItem layerText={layerText} key={layerText.id} /> + <LayersDrawerTextItem + layerText={layerText} + key={layerText.id} + bringToFront={() => {}} + bringToBack={() => {}} + removeObject={() => removeObject(layerText)} + centerObject={() => centerObject(layerText)} + editObject={() => {}} + isLayerVisible={isLayerVisible} + isLayerActive={isLayerActive} + /> ))} {Object.values(layer.images).map(layerImage => ( <LayersDrawerImageItem @@ -168,7 +223,7 @@ export const LayersDrawerObjectsList = ({ key={layerImage.id} bringToFront={() => bringImageToFront(layerImage)} bringToBack={() => bringImageToBack(layerImage)} - removeObject={() => removeImage(layerImage)} + removeObject={() => removeObject(layerImage)} centerObject={() => centerObject(layerImage)} editObject={() => editImage()} isLayerVisible={isLayerVisible} diff --git a/src/components/Map/MapViewer/MapViewer.component.tsx b/src/components/Map/MapViewer/MapViewer.component.tsx index 0307cf08..faf874c1 100644 --- a/src/components/Map/MapViewer/MapViewer.component.tsx +++ b/src/components/Map/MapViewer/MapViewer.component.tsx @@ -1,13 +1,17 @@ import 'ol/ol.css'; import { twMerge } from 'tailwind-merge'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { isMapEditToolsActiveSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { + isMapEditToolsActiveSelector, + mapEditToolsLayerObjectSelector, +} from '@/redux/mapEditTools/mapEditTools.selectors'; import { useOlMap } from './utils/useOlMap'; import { MAP_VIEWER_ROLE } from './MapViewer.constants'; export const MapViewer = (): JSX.Element => { const { mapRef } = useOlMap(); const isMapEditToolsActive = useAppSelector(isMapEditToolsActiveSelector); + const layerObject = useAppSelector(mapEditToolsLayerObjectSelector); return ( <div @@ -15,7 +19,7 @@ export const MapViewer = (): JSX.Element => { role={MAP_VIEWER_ROLE} className={twMerge( 'absolute left-[88px] top-[104px] h-[calc(100%-104px)] w-[calc(100%-88px)] bg-white', - isMapEditToolsActive ? 'bg-[#e4e2de]' : 'bg-white', + isMapEditToolsActive || layerObject ? 'bg-[#e4e2de]' : 'bg-white', )} /> ); diff --git a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index 162d345c..5e378293 100644 --- a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -16,7 +16,7 @@ import { } from '@/redux/layers/layers.selectors'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { MapInstance } from '@/types/map'; -import { Geometry, LineString, MultiPolygon, Point } from 'ol/geom'; +import { Geometry, LineString, MultiPolygon } from 'ol/geom'; import Polygon from 'ol/geom/Polygon'; import Layer from '@/components/Map/MapViewer/utils/shapes/layer/Layer'; import { arrowTypesSelector, lineTypesSelector } from '@/redux/shapes/shapes.selectors'; @@ -25,7 +25,7 @@ import { mapDataSizeSelector } from '@/redux/map/map.selectors'; import { LayerState } from '@/redux/layers/layers.types'; import { mapEditToolsActiveActionSelector, - mapEditToolsLayerImageObjectSelector, + mapEditToolsLayerObjectSelector, } from '@/redux/mapEditTools/mapEditTools.selectors'; import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; import getDrawBoundingBoxInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getDrawBoundingBoxInteraction'; @@ -38,7 +38,7 @@ import { mapEditToolsSetActiveAction, mapEditToolsSetLayerObject, } from '@/redux/mapEditTools/mapEditTools.slice'; -import getTransformImageInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction'; +import getTransformInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction'; import { useWebSocketEntityUpdatesContext } from '@/utils/websocket-entity-updates/webSocketEntityUpdatesProvider'; import processMessage from '@/components/Map/MapViewer/utils/websocket/processMessage'; import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; @@ -46,9 +46,7 @@ import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors' export const useOlMapAdditionalLayers = ( mapInstance: MapInstance, ): Array< - VectorLayer< - VectorSource<Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>> - > + VectorLayer<VectorSource<Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>>> > => { const activeAction = useAppSelector(mapEditToolsActiveActionSelector); const dispatch = useAppDispatch(); @@ -61,7 +59,7 @@ export const useOlMapAdditionalLayers = ( const activeLayers = useAppSelector(layersActiveLayersSelector); const drawLayer = useAppSelector(layersDrawLayerSelector); const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); - const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); + const mapEditToolsLayerObject = useAppSelector(mapEditToolsLayerObjectSelector); const [layersState, setLayersState] = useState<Array<LayerState>>([]); const [layersLoadingState, setLayersLoadingState] = useState(false); @@ -157,21 +155,23 @@ export const useOlMapAdditionalLayers = ( if (!dispatch || !currentModelId || !activeLayers.length) { return null; } - const imagesFeatures: Array<Feature<Geometry>> = []; + const features: Array<Feature<Geometry>> = []; const activeVectorLayers = vectorLayers.filter(layer => activeLayers.includes(layer.get('id'))); activeVectorLayers.forEach(vectorLayer => { - imagesFeatures.push(...vectorLayer.get('imagesFeatures')); + features.push(...vectorLayer.get('imagesFeatures')); + features.push(...vectorLayer.get('textsFeatures')); }); - const imagesFeaturesCollection = new Collection(imagesFeatures); - return getTransformImageInteraction( + const featuresCollection = new Collection(features); + return getTransformInteraction( dispatch, mapSize, currentModelId, - imagesFeaturesCollection, + featuresCollection, restrictionExtent, ); }, [dispatch, mapSize, currentModelId, restrictionExtent, activeLayers, vectorLayers]); const transformRef = useRef(transformInteraction); + useEffect(() => { transformRef.current = transformInteraction; }, [transformInteraction]); @@ -202,13 +202,13 @@ export const useOlMapAdditionalLayers = ( } const transformFeatures = transformRef.current.getFeatures(); if ( - mapEditToolsLayerImageObject && + mapEditToolsLayerObject && (!transformFeatures.getLength() || - transformFeatures.item(0).getId() !== mapEditToolsLayerImageObject.id) + transformFeatures.item(0).getId() !== mapEditToolsLayerObject.id) ) { const layer = vectorLayers.find(vectorLayer => { const layerId = vectorLayer.get('id'); - return layerId === mapEditToolsLayerImageObject.layer; + return layerId === mapEditToolsLayerObject.layer; }); if (!layer) { return; @@ -217,13 +217,13 @@ export const useOlMapAdditionalLayers = ( if (!source) { return; } - const feature = source.getFeatureById(mapEditToolsLayerImageObject.id); + const feature = source.getFeatureById(mapEditToolsLayerObject.id); if (!feature) { return; } transformRef.current.setSelection(new Collection<Feature>([feature])); } - }, [mapEditToolsLayerImageObject, vectorLayers]); + }, [mapEditToolsLayerObject, vectorLayers]); useEffect(() => { const activeVectorLayers = vectorLayers.filter(layer => { diff --git a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts b/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts index 0dca36a1..342cdfac 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts @@ -17,7 +17,6 @@ import { LayerImage } from '@/types/models'; import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; import getFill from '@/components/Map/MapViewer/utils/shapes/style/getFill'; import getScaledElementStyle from '@/components/Map/MapViewer/utils/shapes/style/getScaledElementStyle'; -import getBoundingBoxFromExtent from '@/components/Map/MapViewer/utils/shapes/coords/getBoundingBoxFromExtent'; export type GlyphProps = { elementId: number; @@ -154,7 +153,7 @@ export default class Glyph { this.feature.set('setCoordinates', this.setCoordinates.bind(this)); this.feature.set('refreshPolygon', this.refreshPolygon.bind(this)); - this.feature.set('update', this.update.bind(this)); + this.feature.set('updateElement', this.updateElement.bind(this)); this.feature.setId(this.elementId); this.feature.setStyle(this.getStyle.bind(this)); @@ -186,7 +185,7 @@ export default class Glyph { this.feature.changed(); } - private update(imageObject: LayerImage): void { + protected updateElement(imageObject: LayerImage): void { this.elementId = imageObject.id; this.x = imageObject.x; this.y = imageObject.y; @@ -219,11 +218,6 @@ export default class Glyph { const geometry = this.polygonStyle.getGeometry(); if (geometry && geometry instanceof Polygon) { geometry.setCoordinates(coords); - const boundingBox = getBoundingBoxFromExtent(geometry.getExtent(), this.mapSize); - this.x = boundingBox.x; - this.y = boundingBox.y; - this.width = boundingBox.width; - this.height = boundingBox.height; } } diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts index 2149e63b..7f697a72 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts @@ -4,12 +4,12 @@ import { LayerLine, LayerOval, LayerRect, - LayerText, + LayerText as LayerTextModel, } from '@/types/models'; import { MapInstance } from '@/types/map'; import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; import { Feature } from 'ol'; -import { LineString, MultiPolygon, Point } from 'ol/geom'; +import { LineString, MultiPolygon } from 'ol/geom'; import Polygon from 'ol/geom/Polygon'; import VectorSource from 'ol/source/Vector'; import VectorLayer from 'ol/layer/Vector'; @@ -30,11 +30,11 @@ import getArrowFeature from '@/components/Map/MapViewer/utils/shapes/elements/ge import getRotation from '@/components/Map/MapViewer/utils/shapes/coords/getRotation'; import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; import getEllipseCoords from '@/components/Map/MapViewer/utils/shapes/coords/getEllipseCoords'; -import Text from '@/components/Map/MapViewer/utils/shapes/text/Text'; -import LayerImage from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage'; +import LayerText from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerText'; +import LayerImage from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage'; export interface LayerProps { - texts: { [key: string]: LayerText }; + texts: { [key: string]: LayerTextModel }; rects: Array<LayerRect>; ovals: Array<LayerOval>; lines: Array<LayerLine>; @@ -51,7 +51,7 @@ export interface LayerProps { export default class Layer { layerId: number; - texts: { [key: string]: LayerText }; + texts: { [key: string]: LayerTextModel }; rects: Array<LayerRect>; @@ -71,12 +71,10 @@ export default class Layer { mapSize: MapSize; - vectorSource: VectorSource< - Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon> - >; + vectorSource: VectorSource<Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>>; vectorLayer: VectorLayer< - VectorSource<Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>> + VectorSource<Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon>> >; constructor({ @@ -107,7 +105,9 @@ export default class Layer { this.mapSize = mapSize; this.layerId = layerId; - this.vectorSource.addFeatures(this.getTextsFeatures()); + const textsFeatures = this.getTextsFeatures(); + this.vectorSource.addFeatures(textsFeatures); + this.vectorSource.addFeatures(this.getRectsFeatures()); this.vectorSource.addFeatures(this.getOvalsFeatures()); const imagesFeatures = this.getImagesFeatures(); @@ -127,53 +127,39 @@ export default class Layer { this.vectorLayer.set('id', layerId); this.vectorLayer.set('imagesFeatures', imagesFeatures); + this.vectorLayer.set('textsFeatures', textsFeatures); this.vectorLayer.set('drawImage', this.drawImage.bind(this)); this.vectorLayer.set('drawText', this.drawText.bind(this)); } - private getTextsFeatures = (): Array<Feature<Point>> => { - const textObjects = Object.values(this.texts).map(text => { - return new Text({ - x: text.x, - y: text.y, - zIndex: text.z, - width: text.width, - height: text.height, - layer: text.layer, - fontColor: text.color, - borderColor: text.borderColor, - fontSize: text.fontSize, - text: text.notes, - verticalAlign: text.verticalAlign as VerticalAlign, - horizontalAlign: text.horizontalAlign as HorizontalAlign, - pointToProjection: this.pointToProjection, - mapInstance: this.mapInstance, - }); - }); - return textObjects.map(text => text.feature); + private getTextsFeatures = (): Array<Feature<Polygon>> => { + return Object.values(this.texts).map(text => this.getTextFeature(text)); }; - private drawText(text: LayerText): void { + private drawText(text: LayerTextModel): void { const textFeature = this.getTextFeature(text); this.vectorSource.addFeature(textFeature); } - private getTextFeature(text: LayerText): Feature<Point> { - const textObject = new Text({ + private getTextFeature(text: LayerTextModel): Feature<Polygon> { + const textObject = new LayerText({ + elementId: text.id, x: text.x, y: text.y, zIndex: text.z, width: text.width, height: text.height, layer: text.layer, - fontColor: text.color, + color: text.color, borderColor: text.borderColor, + backgroundColor: text.backgroundColor, fontSize: text.fontSize, text: text.notes, verticalAlign: text.verticalAlign as VerticalAlign, horizontalAlign: text.horizontalAlign as HorizontalAlign, pointToProjection: this.pointToProjection, mapInstance: this.mapInstance, + mapSize: this.mapSize, }); return textObject.feature; } diff --git a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage.ts b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage.ts similarity index 54% rename from src/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage.ts rename to src/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage.ts index ff893bdf..b7d306bb 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/LayerImage.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage.ts @@ -3,6 +3,11 @@ import { MapInstance } from '@/types/map'; import { MapSize } from '@/redux/map/map.types'; import { LayerImage as LayerImageModel } from '@/types/models'; import Glyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph'; +import { store } from '@/redux/store'; +import { updateLayerImageObject } from '@/redux/layers/layers.thunks'; +import { layerUpdateImage } from '@/redux/layers/layers.slice'; +import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; +import { BoundingBox } from '@/components/Map/MapViewer/MapViewer.types'; export type LayerImageProps = { elementId: number; @@ -47,11 +52,35 @@ export default class LayerImage extends Glyph { mapSize, }); this.layer = layer; - this.feature.set('getGlyphData', this.getGlyphData.bind(this)); + this.feature.set('getObjectData', this.getData.bind(this)); + this.feature.set('save', this.save.bind(this)); this.feature.set('layer', layer); } - private getGlyphData(): LayerImageModel { + private async save({ + modelId, + boundingBox, + }: { + modelId: number; + boundingBox: BoundingBox; + }): Promise<void> { + const { dispatch } = store; + const layerImage = await dispatch( + updateLayerImageObject({ + modelId, + layerId: this.layer, + ...this.getData(), + ...boundingBox, + }), + ).unwrap(); + if (layerImage) { + dispatch(layerUpdateImage({ modelId, layerId: layerImage.layer, layerImage })); + dispatch(mapEditToolsSetLayerObject(layerImage)); + this.updateElement(layerImage); + } + } + + private getData(): LayerImageModel { return { id: this.elementId, x: this.x, diff --git a/src/components/Map/MapViewer/utils/shapes/text/Text.test.ts b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.test.ts similarity index 75% rename from src/components/Map/MapViewer/utils/shapes/text/Text.test.ts rename to src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.test.ts index 2f2e145d..7db76f34 100644 --- a/src/components/Map/MapViewer/utils/shapes/text/Text.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.test.ts @@ -3,17 +3,20 @@ import { Map } from 'ol'; import { Style } from 'ol/style'; import View from 'ol/View'; import { BLACK_COLOR } from '@/components/Map/MapViewer/MapViewer.constants'; -import Text, { TextProps } from '@/components/Map/MapViewer/utils/shapes/text/Text'; +import LayerText, { + LayerTextProps, +} from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerText'; import getTextStyle from '@/components/Map/MapViewer/utils/shapes/text/getTextStyle'; import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex'; import getTextCoords from '@/components/Map/MapViewer/utils/shapes/text/getTextCoords'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; -jest.mock('./getTextCoords'); -jest.mock('./getTextStyle'); -jest.mock('../style/rgbToHex'); +jest.mock('../../text/getTextCoords'); +jest.mock('../../text/getTextStyle'); +jest.mock('../../style/rgbToHex'); describe('Text', () => { - let props: TextProps; + let props: LayerTextProps; beforeEach(() => { const dummyElement = document.createElement('div'); @@ -26,6 +29,7 @@ describe('Text', () => { }), }); props = { + elementId: 1, x: 0, y: 0, width: 100, @@ -34,12 +38,20 @@ describe('Text', () => { layer: 1, text: 'Test', fontSize: 12, - fontColor: BLACK_COLOR, + color: BLACK_COLOR, borderColor: BLACK_COLOR, + backgroundColor: BLACK_COLOR, verticalAlign: 'MIDDLE', horizontalAlign: 'CENTER', pointToProjection: jest.fn(({ x, y }) => [x, y]), mapInstance, + mapSize: { + minZoom: 1, + maxZoom: 9, + width: 0, + height: 0, + tileSize: DEFAULT_TILE_SIZE, + }, }; (getTextStyle as jest.Mock).mockReturnValue(new Style()); @@ -48,7 +60,7 @@ describe('Text', () => { }); it('should apply correct styles to the feature', () => { - const text = new Text(props); + const text = new LayerText(props); const { feature } = text; const style = feature.getStyleFunction()?.call(text, feature, 1); @@ -61,7 +73,7 @@ describe('Text', () => { }); it('should hide text when the scaled font size is too small', () => { - const text = new Text(props); + const text = new LayerText(props); const { feature } = text; const style = feature.getStyleFunction()?.call(text, feature, 20); diff --git a/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts new file mode 100644 index 00000000..133034cf --- /dev/null +++ b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts @@ -0,0 +1,315 @@ +/* eslint-disable no-magic-numbers */ +import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; +import Style from 'ol/style/Style'; +import { Point } from 'ol/geom'; +import { Feature } from 'ol'; +import { FeatureLike } from 'ol/Feature'; +import { MapInstance } from '@/types/map'; +import { LayerText as LayerTextModel, Color } from '@/types/models'; +import { TEXT_CUTOFF_SCALE } from '@/components/Map/MapViewer/MapViewer.constants'; +import { + BoundingBox, + HorizontalAlign, + VerticalAlign, +} from '@/components/Map/MapViewer/MapViewer.types'; +import getTextCoords from '@/components/Map/MapViewer/utils/shapes/text/getTextCoords'; +import getTextStyle from '@/components/Map/MapViewer/utils/shapes/text/getTextStyle'; +import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex'; +import Polygon from 'ol/geom/Polygon'; +import getStroke from '@/components/Map/MapViewer/utils/shapes/style/getStroke'; +import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; +import getScaledElementStyle from '@/components/Map/MapViewer/utils/shapes/style/getScaledElementStyle'; +import { Stroke } from 'ol/style'; +import { MapSize } from '@/redux/map/map.types'; +import getBoundingBoxFromExtent from '@/components/Map/MapViewer/utils/shapes/coords/getBoundingBoxFromExtent'; +import { Coordinate } from 'ol/coordinate'; +import { store } from '@/redux/store'; +import { updateLayerText } from '@/redux/layers/layers.thunks'; +import { layerUpdateText } from '@/redux/layers/layers.slice'; +import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; + +export interface LayerTextProps { + elementId: number; + x: number; + y: number; + width: number; + height: number; + layer: number; + zIndex: number; + text: string; + fontSize: number; + color: Color; + borderColor: Color; + backgroundColor: Color; + verticalAlign: VerticalAlign; + horizontalAlign: HorizontalAlign; + pointToProjection: UsePointToProjectionResult; + mapInstance: MapInstance; + mapSize: MapSize; +} + +export default class LayerText { + elementId: number; + + x: number; + + y: number; + + zIndex: number; + + width: number; + + height: number; + + layer: number; + + text: string; + + verticalAlign: VerticalAlign; + + horizontalAlign: HorizontalAlign; + + backgroundColor: Color; + + borderColor: Color; + + color: Color; + + fontSize: number; + + style: Style; + + polygonStyle: Style; + + polygon: Polygon = new Polygon([]); + + strokeStyle: Stroke; + + point: Point; + + feature: Feature<Polygon>; + + mapSize: MapSize; + + pointToProjection: UsePointToProjectionResult; + + constructor({ + elementId, + x, + y, + width, + height, + layer, + zIndex, + text, + fontSize, + color, + borderColor, + backgroundColor, + verticalAlign, + horizontalAlign, + pointToProjection, + mapInstance, + mapSize, + }: LayerTextProps) { + this.text = text; + this.fontSize = fontSize; + this.elementId = elementId; + this.x = x; + this.y = y; + this.zIndex = zIndex; + this.width = width; + this.height = height; + this.layer = layer; + this.text = text; + this.verticalAlign = verticalAlign; + this.horizontalAlign = horizontalAlign; + this.backgroundColor = backgroundColor; + this.borderColor = borderColor; + this.color = color; + this.mapSize = mapSize; + this.pointToProjection = pointToProjection; + + const textCoords = getTextCoords({ + x, + y, + height, + width, + fontSize, + verticalAlign, + horizontalAlign, + pointToProjection, + }); + this.drawPolygon(); + this.strokeStyle = getStroke({ + color: rgbToHex(borderColor), + width: 1, + }); + this.polygonStyle = getStyle({ + geometry: this.polygon, + borderColor, + fillColor: { rgb: 0, alpha: 0 }, + lineWidth: 1, + zIndex, + }); + + const textStyle = getTextStyle({ + text, + fontSize, + color: rgbToHex(color), + zIndex, + horizontalAlign, + }); + this.point = new Point(textCoords); + this.style = textStyle; + this.style.setGeometry(this.point); + + this.feature = new Feature({ + geometry: this.polygon, + getScale: (resolution: number): number => { + const maxZoom = mapInstance?.getView().get('originalMaxZoom'); + if (maxZoom) { + const minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom); + if (minResolution) { + return Math.round((minResolution / resolution) * 100) / 100; + } + } + return 1; + }, + layer, + }); + this.feature.setId(this.elementId); + this.feature.set('getObjectData', this.getData.bind(this)); + this.feature.set('setCoordinates', this.setCoordinates.bind(this)); + this.feature.set('refreshPolygon', this.refreshPolygon.bind(this)); + this.feature.set('save', this.save.bind(this)); + this.feature.set('updateElement', this.updateElement.bind(this)); + this.feature.setStyle(this.getStyle.bind(this)); + } + + private getData(): LayerTextModel { + return { + id: this.elementId, + x: this.x, + y: this.y, + z: this.zIndex, + width: this.width, + height: this.height, + layer: this.layer, + fontSize: this.fontSize, + notes: this.text, + verticalAlign: this.verticalAlign, + horizontalAlign: this.horizontalAlign, + backgroundColor: this.backgroundColor, + borderColor: this.borderColor, + color: this.color, + }; + } + + private drawPolygon(): void { + this.polygon = new Polygon([ + [ + this.pointToProjection({ x: this.x, y: this.y }), + this.pointToProjection({ x: this.x + this.width, y: this.y }), + this.pointToProjection({ x: this.x + this.width, y: this.y + this.height }), + this.pointToProjection({ x: this.x, y: this.y + this.height }), + this.pointToProjection({ x: this.x, y: this.y }), + ], + ]); + } + + private async save({ + modelId, + boundingBox, + }: { + modelId: number; + boundingBox: BoundingBox; + }): Promise<void> { + const { dispatch } = store; + const layerText = await dispatch( + updateLayerText({ + modelId, + layerId: this.layer, + ...this.getData(), + ...boundingBox, + }), + ).unwrap(); + if (layerText) { + dispatch(layerUpdateText({ modelId, layerId: layerText.layer, layerText })); + dispatch(mapEditToolsSetLayerObject(layerText)); + this.updateElement(layerText); + } + } + + private refreshPolygon(): void { + this.drawPolygon(); + this.polygonStyle.setGeometry(this.polygon); + this.feature.setGeometry(this.polygon); + this.feature.changed(); + } + + private refreshZIndex(): void { + this.polygonStyle.setZIndex(this.zIndex); + this.style.setZIndex(this.zIndex); + this.feature.changed(); + } + + private updateElement(layerText: LayerTextModel): void { + this.elementId = layerText.id; + this.x = layerText.x; + this.y = layerText.y; + this.zIndex = layerText.z; + this.width = layerText.width; + this.height = layerText.height; + this.text = layerText.notes; + this.fontSize = layerText.fontSize; + this.color = layerText.color; + this.borderColor = layerText.borderColor; + this.verticalAlign = layerText.verticalAlign; + this.horizontalAlign = layerText.horizontalAlign; + + this.refreshPolygon(); + this.refreshZIndex(); + this.feature.changed(); + } + + private setCoordinates(coords: Coordinate[][]): void { + const geometry = this.polygonStyle.getGeometry(); + if (geometry && geometry instanceof Polygon) { + geometry.setCoordinates(coords); + } + } + + protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { + const getScale = feature.get('getScale'); + let scale = 1; + if (getScale instanceof Function) { + scale = getScale(resolution); + } + const geometry = feature.getGeometry(); + if (geometry && geometry instanceof Polygon) { + const polygonExtent = geometry.getExtent(); + if (polygonExtent) { + const boundingBox = getBoundingBoxFromExtent(polygonExtent, this.mapSize); + const textCoords = getTextCoords({ + x: boundingBox.x, + y: boundingBox.y, + height: boundingBox.height, + width: boundingBox.width, + fontSize: this.fontSize, + verticalAlign: this.verticalAlign, + horizontalAlign: this.horizontalAlign, + pointToProjection: this.pointToProjection, + }); + this.point.setCoordinates(textCoords); + } + } + if (scale < TEXT_CUTOFF_SCALE) { + return undefined; + } + return [ + getScaledElementStyle(this.polygonStyle, this.strokeStyle, scale), + getScaledElementStyle(this.style, undefined, scale), + ]; + } +} diff --git a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.test.ts b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.test.ts similarity index 85% rename from src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.test.ts rename to src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.test.ts index c2f77a29..1cb1fec9 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.test.ts @@ -10,13 +10,13 @@ import { DEFAULT_TILE_SIZE } from '@/constants/map'; import { Collection, Feature } from 'ol'; import Transform from 'ol-ext/interaction/Transform'; import { Geometry } from 'ol/geom'; -import getTransformImageInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction'; +import getTransformInteraction from '@/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction'; jest.mock('../../../../../../../utils/map/latLngToPoint', () => ({ latLngToPoint: jest.fn(latLng => ({ x: latLng[0], y: latLng[1] })), })); -describe('getTransformImageInteraction', () => { +describe('getTransformInteraction', () => { let store = {} as ToolkitStoreWithSingleSlice<ModalState>; let modelIdMock: number; let featuresCollectionMock: Collection<Feature<Geometry>>; @@ -39,7 +39,7 @@ describe('getTransformImageInteraction', () => { }); it('returns a Transform interaction', () => { - const transformInteraction = getTransformImageInteraction( + const transformInteraction = getTransformInteraction( store.dispatch, mapSize, modelIdMock, diff --git a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.ts b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.ts similarity index 76% rename from src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.ts rename to src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.ts index 3a1435fd..c1c8565c 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformImageInteraction.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/interaction/getTransformInteraction.ts @@ -5,15 +5,13 @@ import Transform from 'ol-ext/interaction/Transform'; import { Geometry } from 'ol/geom'; import { Collection, Feature } from 'ol'; import BaseEvent from 'ol/events/Event'; -import { updateLayerImageObject } from '@/redux/layers/layers.thunks'; -import { layerUpdateImage } from '@/redux/layers/layers.slice'; import getBoundingBoxFromExtent from '@/components/Map/MapViewer/utils/shapes/coords/getBoundingBoxFromExtent'; import { MapSize } from '@/redux/map/map.types'; import { Extent } from 'ol/extent'; import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; import { openDrawer } from '@/redux/drawer/drawer.slice'; -export default function getTransformImageInteraction( +export default function getTransformInteraction( dispatch: AppDispatch, mapSize: MapSize, modelId: number, @@ -86,6 +84,11 @@ export default function getTransformImageInteraction( newGeometry.scale(1, -1); feature.setGeometry(newGeometry); transform.setSelection(new Collection([feature])); + return; + } + const setCoordinates = feature.get('setCoordinates'); + if (geometry instanceof Polygon && setCoordinates instanceof Function) { + setCoordinates(geometry.getCoordinates()); } }); @@ -96,10 +99,10 @@ export default function getTransformImageInteraction( dispatch(mapEditToolsSetLayerObject(null)); return; } - const getGlyphData = features.item(0).get('getGlyphData'); - if (getGlyphData && getGlyphData instanceof Function) { - const glyphData = getGlyphData(); - dispatch(mapEditToolsSetLayerObject(glyphData)); + const getObjectData = features.item(0).get('getObjectData'); + if (getObjectData && getObjectData instanceof Function) { + const objectData = getObjectData(); + dispatch(mapEditToolsSetLayerObject(objectData)); dispatch(openDrawer('layers')); } }); @@ -107,30 +110,13 @@ export default function getTransformImageInteraction( transform.on(['scaleend', 'translateend'], async (event: BaseEvent | Event): Promise<void> => { const transformEvent = event as unknown as { feature: Feature }; const { feature } = transformEvent; - const setCoordinates = feature.get('setCoordinates'); - const getGlyphData = feature.get('getGlyphData'); const refreshPolygon = feature.get('refreshPolygon'); + const save = feature.get('save'); const geometry = feature.getGeometry(); - if (geometry && getGlyphData instanceof Function) { - const glyphData = getGlyphData(); + if (geometry && save instanceof Function) { try { const boundingBox = getBoundingBoxFromExtent(geometry.getExtent(), mapSize); - const layerImage = await dispatch( - updateLayerImageObject({ - modelId, - layerId: glyphData.layer, - ...glyphData, - ...boundingBox, - }), - ).unwrap(); - if (layerImage) { - dispatch(layerUpdateImage({ modelId, layerId: layerImage.layer, layerImage })); - dispatch(mapEditToolsSetLayerObject(layerImage)); - } - if (geometry instanceof Polygon && setCoordinates instanceof Function) { - setCoordinates(geometry.getCoordinates()); - geometry.changed(); - } + save({ modelId, boundingBox }); } catch { if (refreshPolygon instanceof Function) { refreshPolygon(); diff --git a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph.ts b/src/components/Map/MapViewer/utils/shapes/layer/utils/updateElement.ts similarity index 60% rename from src/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph.ts rename to src/components/Map/MapViewer/utils/shapes/layer/utils/updateElement.ts index 05418973..88f50d44 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/utils/updateElement.ts @@ -1,20 +1,20 @@ import VectorSource from 'ol/source/Vector'; -import { LayerImage } from '@/types/models'; +import { LayerImage, LayerText } from '@/types/models'; import { MapInstance } from '@/types/map'; -export default function updateGlyph( +export default function updateElement( mapInstance: MapInstance, layerId: number, - layerImage: LayerImage, + layerObject: LayerImage | LayerText, ): void { mapInstance?.getAllLayers().forEach(layer => { if (layer.get('id') === layerId) { const source = layer.getSource(); if (source instanceof VectorSource) { - const feature = source.getFeatureById(layerImage.id); - const update = feature?.get('update'); + const feature = source.getFeatureById(layerObject.id); + const update = feature?.get('updateElement'); if (update && update instanceof Function) { - update(layerImage); + update(layerObject); feature.changed(); } } diff --git a/src/components/Map/MapViewer/utils/shapes/text/Text.ts b/src/components/Map/MapViewer/utils/shapes/text/Text.ts deleted file mode 100644 index c1bc5799..00000000 --- a/src/components/Map/MapViewer/utils/shapes/text/Text.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { UsePointToProjectionResult } from '@/utils/map/usePointToProjection'; -import Style from 'ol/style/Style'; -import { Point } from 'ol/geom'; -import { Feature } from 'ol'; -import { FeatureLike } from 'ol/Feature'; -import { MapInstance } from '@/types/map'; -import { Color } from '@/types/models'; -import { TEXT_CUTOFF_SCALE } from '@/components/Map/MapViewer/MapViewer.constants'; -import { HorizontalAlign, VerticalAlign } from '@/components/Map/MapViewer/MapViewer.types'; -import getTextCoords from '@/components/Map/MapViewer/utils/shapes/text/getTextCoords'; -import getTextStyle from '@/components/Map/MapViewer/utils/shapes/text/getTextStyle'; -import { rgbToHex } from '@/components/Map/MapViewer/utils/shapes/style/rgbToHex'; -import Polygon from 'ol/geom/Polygon'; -import getStroke from '@/components/Map/MapViewer/utils/shapes/style/getStroke'; -import getStyle from '@/components/Map/MapViewer/utils/shapes/style/getStyle'; -import getScaledElementStyle from '@/components/Map/MapViewer/utils/shapes/style/getScaledElementStyle'; -import { Stroke } from 'ol/style'; - -export interface TextProps { - x: number; - y: number; - width: number; - height: number; - layer: number; - zIndex: number; - text: string; - fontSize: number; - fontColor: Color; - borderColor: Color; - verticalAlign: VerticalAlign; - horizontalAlign: HorizontalAlign; - pointToProjection: UsePointToProjectionResult; - mapInstance: MapInstance; -} - -export default class Text { - text: string; - - fontSize: number; - - style: Style; - - polygonStyle: Style; - - strokeStyle: Stroke; - - point: Point; - - feature: Feature<Point>; - - constructor({ - x, - y, - width, - height, - layer, - zIndex, - text, - fontSize, - fontColor, - borderColor, - verticalAlign, - horizontalAlign, - pointToProjection, - mapInstance, - }: TextProps) { - this.text = text; - this.fontSize = fontSize; - - const textCoords = getTextCoords({ - x, - y, - height, - width, - fontSize, - verticalAlign, - horizontalAlign, - pointToProjection, - }); - const borderPolygon = new Polygon([ - [ - pointToProjection({ x, y }), - pointToProjection({ x: x + width, y }), - pointToProjection({ x: x + width, y: y + height }), - pointToProjection({ x, y: y + height }), - pointToProjection({ x, y }), - ], - ]); - this.strokeStyle = getStroke({ - color: rgbToHex(borderColor), - width: 1, - }); - this.polygonStyle = getStyle({ - geometry: borderPolygon, - borderColor, - fillColor: { rgb: 0, alpha: 0 }, - lineWidth: 1, - zIndex, - }); - - const textStyle = getTextStyle({ - text, - fontSize, - color: rgbToHex(fontColor), - zIndex, - horizontalAlign, - }); - this.point = new Point(textCoords); - this.style = textStyle; - this.style.setGeometry(this.point); - - this.feature = new Feature({ - geometry: this.point, - getScale: (resolution: number): number => { - const maxZoom = mapInstance?.getView().get('originalMaxZoom'); - if (maxZoom) { - const minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom); - if (minResolution) { - return Math.round((minResolution / resolution) * 100) / 100; - } - } - return 1; - }, - layer, - }); - - this.feature.setStyle(this.getStyle.bind(this)); - } - - protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { - const getScale = feature.get('getScale'); - let scale = 1; - if (getScale instanceof Function) { - scale = getScale(resolution); - } - if (scale < TEXT_CUTOFF_SCALE) { - return undefined; - } - return [ - getScaledElementStyle(this.polygonStyle, this.strokeStyle, scale), - getScaledElementStyle(this.style, undefined, scale), - ]; - } -} diff --git a/src/components/Map/MapViewer/utils/shapes/text/getTextCoords.ts b/src/components/Map/MapViewer/utils/shapes/text/getTextCoords.ts index e3224fc9..26acfb6b 100644 --- a/src/components/Map/MapViewer/utils/shapes/text/getTextCoords.ts +++ b/src/components/Map/MapViewer/utils/shapes/text/getTextCoords.ts @@ -12,6 +12,7 @@ export default function getTextCoords({ verticalAlign, horizontalAlign, pointToProjection, + useProjection = true, }: { x: number; y: number; @@ -20,7 +21,8 @@ export default function getTextCoords({ fontSize: number; verticalAlign: VerticalAlign; horizontalAlign: HorizontalAlign; - pointToProjection: UsePointToProjectionResult; + pointToProjection?: UsePointToProjectionResult; + useProjection?: boolean; }): Coordinate { const minX = x; const maxX = x + width; @@ -41,5 +43,8 @@ export default function getTextCoords({ textX = maxX; } - return pointToProjection({ x: textX, y: textY }); + if (useProjection && pointToProjection) { + return pointToProjection({ x: textX, y: textY }); + } + return [textX, textY]; } diff --git a/src/components/Map/MapViewer/utils/websocket/processLayerImage.ts b/src/components/Map/MapViewer/utils/websocket/processLayerImage.ts index 84018d80..6fd6ec3a 100644 --- a/src/components/Map/MapViewer/utils/websocket/processLayerImage.ts +++ b/src/components/Map/MapViewer/utils/websocket/processLayerImage.ts @@ -2,7 +2,7 @@ import { WebSocketEntityUpdateInterface } from '@/utils/websocket-entity-updates import { store } from '@/redux/store'; import { ENTITY_OPERATION_TYPES } from '@/utils/websocket-entity-updates/webSocketEntityUpdates.constants'; import { getLayerImage } from '@/redux/layers/layers.thunks'; -import updateGlyph from '@/components/Map/MapViewer/utils/shapes/elements/Glyph/updateGlyph'; +import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; import { MapInstance } from '@/types/map'; import drawElementOnLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer'; import { layerDeleteImage } from '@/redux/layers/layers.slice'; @@ -38,7 +38,7 @@ export default async function processLayerImage({ drawFunctionKey: 'drawImage', }); } else { - updateGlyph(mapInstance, data.layerId, resultImage); + updateElement(mapInstance, data.layerId, resultImage); } } else if (data.type === ENTITY_OPERATION_TYPES.ENTITY_DELETED) { dispatch( diff --git a/src/components/Map/MapViewer/utils/websocket/processLayerText.ts b/src/components/Map/MapViewer/utils/websocket/processLayerText.ts index f31697c4..e1740c97 100644 --- a/src/components/Map/MapViewer/utils/websocket/processLayerText.ts +++ b/src/components/Map/MapViewer/utils/websocket/processLayerText.ts @@ -4,6 +4,9 @@ import { ENTITY_OPERATION_TYPES } from '@/utils/websocket-entity-updates/webSock import { getLayerText } from '@/redux/layers/layers.thunks'; import { MapInstance } from '@/types/map'; import drawElementOnLayer from '@/components/Map/MapViewer/utils/shapes/layer/utils/drawElementOnLayer'; +import { layerDeleteText } from '@/redux/layers/layers.slice'; +import removeElementFromLayer from '@/components/Map/MapViewer/utils/shapes/elements/removeElementFromLayer'; +import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; export default async function processLayerText({ data, @@ -13,7 +16,10 @@ export default async function processLayerText({ mapInstance: MapInstance; }): Promise<void> { const { dispatch } = store; - if (data.type === ENTITY_OPERATION_TYPES.ENTITY_CREATED) { + if ( + data.type === ENTITY_OPERATION_TYPES.ENTITY_CREATED || + data.type === ENTITY_OPERATION_TYPES.ENTITY_UPDATED + ) { const resultText = await dispatch( getLayerText({ modelId: data.mapId, @@ -24,11 +30,24 @@ export default async function processLayerText({ if (!resultText) { return; } - drawElementOnLayer({ - mapInstance, - activeLayer: data.layerId, - object: resultText, - drawFunctionKey: 'drawText', - }); + if (data.type === ENTITY_OPERATION_TYPES.ENTITY_CREATED) { + drawElementOnLayer({ + mapInstance, + activeLayer: data.layerId, + object: resultText, + drawFunctionKey: 'drawText', + }); + } else { + updateElement(mapInstance, data.layerId, resultText); + } + } else if (data.type === ENTITY_OPERATION_TYPES.ENTITY_DELETED) { + dispatch( + layerDeleteText({ + modelId: data.mapId, + layerId: data.layerId, + textId: data.entityId, + }), + ); + removeElementFromLayer({ mapInstance, layerId: data.layerId, featureId: data.entityId }); } } diff --git a/src/models/layerTextSchema.ts b/src/models/layerTextSchema.ts index 0ec90665..a575b139 100644 --- a/src/models/layerTextSchema.ts +++ b/src/models/layerTextSchema.ts @@ -11,8 +11,8 @@ export const layerTextSchema = z.object({ layer: z.number(), fontSize: z.number(), notes: z.string(), - verticalAlign: z.string(), - horizontalAlign: z.string(), + verticalAlign: z.enum(['TOP', 'MIDDLE', 'BOTTOM']), + horizontalAlign: z.enum(['LEFT', 'RIGHT', 'CENTER', 'END', 'START']), backgroundColor: colorSchema, borderColor: colorSchema, color: colorSchema, diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 9eb33c02..18186d6f 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -64,6 +64,10 @@ export const apiPath = { `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/texts/`, getLayerText: (modelId: number, layerId: number, imageId: number | string): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/texts/${imageId}`, + updateLayerText: (modelId: number, layerId: number, textId: number | string): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/texts/${textId}`, + removeLayerText: (modelId: number, layerId: number, textId: number | string): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/texts/${textId}`, getLayer: (modelId: number, layerId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`, getGlyphImage: (glyphId: number): string => diff --git a/src/redux/layers/layers.reducers.ts b/src/redux/layers/layers.reducers.ts index 0213d714..238a3441 100644 --- a/src/redux/layers/layers.reducers.ts +++ b/src/redux/layers/layers.reducers.ts @@ -198,3 +198,35 @@ export const layerAddTextReducer = ( } layer.texts[layerText.id] = layerText; }; + +export const layerUpdateTextReducer = ( + state: LayersState, + action: PayloadAction<{ modelId: number; layerId: number; layerText: LayerText }>, +): void => { + const { modelId, layerId, layerText } = action.payload; + const { data } = state[modelId]; + if (!data) { + return; + } + const layer = data.layers.find(layerState => layerState.details.id === layerId); + if (!layer) { + return; + } + layer.texts[layerText.id] = layerText; +}; + +export const layerDeleteTextReducer = ( + state: LayersState, + action: PayloadAction<{ modelId: number; layerId: number; textId: number }>, +): void => { + const { modelId, layerId, textId } = action.payload; + const { data } = state[modelId]; + if (!data) { + return; + } + const layer = data.layers.find(layerState => layerState.details.id === layerId); + if (!layer) { + return; + } + delete layer.texts[textId]; +}; diff --git a/src/redux/layers/layers.slice.ts b/src/redux/layers/layers.slice.ts index dcd20ceb..871012fc 100644 --- a/src/redux/layers/layers.slice.ts +++ b/src/redux/layers/layers.slice.ts @@ -12,6 +12,8 @@ import { setDrawLayerReducer, setLayerToInactiveReducer, setLayerToActiveReducer, + layerDeleteTextReducer, + layerUpdateTextReducer, } from '@/redux/layers/layers.reducers'; export const layersSlice = createSlice({ @@ -25,6 +27,8 @@ export const layersSlice = createSlice({ layerUpdateImage: layerUpdateImageReducer, layerDeleteImage: layerDeleteImageReducer, layerAddText: layerAddTextReducer, + layerUpdateText: layerUpdateTextReducer, + layerDeleteText: layerDeleteTextReducer, setDrawLayer: setDrawLayerReducer, }, extraReducers: builder => { @@ -42,6 +46,8 @@ export const { layerUpdateImage, layerDeleteImage, layerAddText, + layerUpdateText, + layerDeleteText, setDrawLayer, } = layersSlice.actions; diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts index 847d84d2..66035fd8 100644 --- a/src/redux/layers/layers.thunks.ts +++ b/src/redux/layers/layers.thunks.ts @@ -1,7 +1,7 @@ /* eslint-disable no-magic-numbers */ import { z as zod } from 'zod'; import { apiPath } from '@/redux/apiPath'; -import { Layer, LayerImage, Layers, LayerText } from '@/types/models'; +import { Color, Layer, LayerImage, Layers, LayerText } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { ThunkConfig } from '@/types/store'; @@ -22,7 +22,11 @@ import { layerLineSchema } from '@/models/layerLineSchema'; import { layerImageSchema } from '@/models/layerImageSchema'; import arrayToKeyValue from '@/utils/array/arrayToKeyValue'; import { LayerTextFactoryForm } from '@/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.types'; -import { BoundingBox } from '@/components/Map/MapViewer/MapViewer.types'; +import { + BoundingBox, + HorizontalAlign, + VerticalAlign, +} from '@/components/Map/MapViewer/MapViewer.types'; export const getLayer = createAsyncThunk< Layer | null, @@ -307,3 +311,81 @@ export const getLayerText = createAsyncThunk< return Promise.reject(getError({ error })); } }); + +export const removeLayerText = createAsyncThunk< + null, + { modelId: number; layerId: number; textId: number }, + ThunkConfig +>('layers/removeLayerText', async ({ modelId, layerId, textId }) => { + try { + await axiosInstanceNewAPI.delete<void>(apiPath.removeLayerText(modelId, layerId, textId)); + return null; + } catch (error) { + return Promise.reject(getError({ error })); + } +}); + +export const updateLayerText = createAsyncThunk< + LayerText | null, + { + modelId: number; + layerId: number; + id: number; + x: number; + y: number; + z: number; + width: number; + height: number; + fontSize: number; + notes: string; + verticalAlign: VerticalAlign; + horizontalAlign: HorizontalAlign; + color: Color; + borderColor: Color; + }, + ThunkConfig +>( + 'layers/updateLayerText', + async ({ + modelId, + layerId, + id, + x, + y, + z, + width, + height, + fontSize, + notes, + verticalAlign, + horizontalAlign, + color, + borderColor, + }) => { + try { + const { data } = await axiosInstanceNewAPI.put<LayerText>( + apiPath.updateLayerText(modelId, layerId, id), + { + x, + y, + z, + width, + height, + fontSize, + notes, + verticalAlign, + horizontalAlign, + color, + borderColor, + }, + ); + const isDataValid = validateDataUsingZodSchema(data, layerTextSchema); + if (isDataValid) { + return data; + } + return null; + } catch (error) { + return Promise.reject(getError({ error })); + } + }, +); diff --git a/src/redux/mapEditTools/mapEditTools.mock.ts b/src/redux/mapEditTools/mapEditTools.mock.ts index d6fe529c..fbbacec7 100644 --- a/src/redux/mapEditTools/mapEditTools.mock.ts +++ b/src/redux/mapEditTools/mapEditTools.mock.ts @@ -2,5 +2,5 @@ import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types'; export const MAP_EDIT_TOOLS_STATE_INITIAL_MOCK: MapEditToolsState = { activeAction: null, - layerImageObject: null, + layerObject: null, }; diff --git a/src/redux/mapEditTools/mapEditTools.reducers.ts b/src/redux/mapEditTools/mapEditTools.reducers.ts index da3f5f7f..d45e719f 100644 --- a/src/redux/mapEditTools/mapEditTools.reducers.ts +++ b/src/redux/mapEditTools/mapEditTools.reducers.ts @@ -2,7 +2,7 @@ import { PayloadAction } from '@reduxjs/toolkit'; import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types'; -import { LayerImage } from '@/types/models'; +import { LayerImage, LayerText } from '@/types/models'; export const mapEditToolsSetActiveActionReducer = ( state: MapEditToolsState, @@ -13,7 +13,7 @@ export const mapEditToolsSetActiveActionReducer = ( export const mapEditToolsSetLayerObjectReducer = ( state: MapEditToolsState, - action: PayloadAction<LayerImage | null>, + action: PayloadAction<LayerImage | LayerText | null>, ): void => { - state.layerImageObject = action.payload; + state.layerObject = action.payload; }; diff --git a/src/redux/mapEditTools/mapEditTools.selectors.ts b/src/redux/mapEditTools/mapEditTools.selectors.ts index 2b29bd35..d3ea7d12 100644 --- a/src/redux/mapEditTools/mapEditTools.selectors.ts +++ b/src/redux/mapEditTools/mapEditTools.selectors.ts @@ -9,9 +9,9 @@ export const mapEditToolsActiveActionSelector = createSelector( state => state.activeAction, ); -export const mapEditToolsLayerImageObjectSelector = createSelector( +export const mapEditToolsLayerObjectSelector = createSelector( mapEditToolsSelector, - state => state.layerImageObject, + state => state.layerObject, ); export const isMapEditToolsActiveSelector = createSelector(mapEditToolsSelector, state => diff --git a/src/redux/mapEditTools/mapEditTools.types.ts b/src/redux/mapEditTools/mapEditTools.types.ts index e141e6b8..4c216c40 100644 --- a/src/redux/mapEditTools/mapEditTools.types.ts +++ b/src/redux/mapEditTools/mapEditTools.types.ts @@ -1,7 +1,7 @@ import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; -import { LayerImage } from '@/types/models'; +import { LayerImage, LayerText } from '@/types/models'; export type MapEditToolsState = { activeAction: keyof typeof MAP_EDIT_ACTIONS | null; - layerImageObject: LayerImage | null; + layerObject: LayerImage | LayerText | null; }; -- GitLab From 09dff5302aad6a606bc317acb391aa0a7b2cc0bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Mon, 17 Feb 2025 13:08:07 +0100 Subject: [PATCH 2/8] feat(layer-text): Add editing text notes, font size, color alignment etc. --- ...rImageObjectEditFactoryModal.component.tsx | 2 +- .../LayerTextEditFactoryModal.component.tsx | 109 ++++++++++++++++++ .../LayerTextFactory.constants.ts | 12 +- .../LayerTextFactory.types.ts | 5 +- .../LayerTextForm.component.tsx | 4 +- .../FunctionalArea/Modal/Modal.component.tsx | 6 + .../ModalLayout/ModalLayout.component.tsx | 3 +- .../LayersDrawerObjectsList.component.tsx | 59 +++++++++- .../Map/MapViewer/MapViewer.types.ts | 2 +- .../utils/shapes/elements/Glyph/Glyph.ts | 52 +++++---- .../utils/shapes/layer/elements/LayerText.ts | 56 ++++----- src/models/bioEntitySchema.ts | 2 +- src/models/layerTextSchema.ts | 2 +- src/models/modelElementModificationSchema.ts | 2 +- src/models/modelElementSchema.ts | 2 +- src/redux/modal/modal.reducers.ts | 6 + src/redux/modal/modal.slice.ts | 3 + src/types/modal.ts | 3 +- 18 files changed, 257 insertions(+), 73 deletions(-) create mode 100644 src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextEditFactoryModal.component.tsx diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx index dc280845..b90d5b25 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx @@ -69,7 +69,7 @@ export const LayerImageObjectEditFactoryModal: React.FC = () => { const typedError = error as SerializedError; showToast({ type: 'error', - message: typedError.message || 'An error occurred while adding a new image', + message: typedError.message || 'An error occurred while editing the layer image', }); } finally { setIsSending(false); diff --git a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextEditFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextEditFactoryModal.component.tsx new file mode 100644 index 00000000..c73e9d93 --- /dev/null +++ b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextEditFactoryModal.component.tsx @@ -0,0 +1,109 @@ +/* eslint-disable no-magic-numbers */ +import React, { useState } from 'react'; +import './LayerTextFactoryModal.styles.css'; +import { LayerTextForm } from '@/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextForm.component'; +import { LoadingIndicator } from '@/shared/LoadingIndicator'; +import { Button } from '@/shared/Button'; +import { LayerTextFactoryForm } from '@/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.types'; +import { Color } from '@/types/models'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; +import { showToast } from '@/utils/showToast'; +import { closeModal } from '@/redux/modal/modal.slice'; +import { SerializedError } from '@reduxjs/toolkit'; +import { updateLayerText } from '@/redux/layers/layers.thunks'; +import { layerUpdateText } from '@/redux/layers/layers.slice'; +import { useMapInstance } from '@/utils/context/mapInstanceContext'; +import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; +import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import updateElement from '@/components/Map/MapViewer/utils/shapes/layer/utils/updateElement'; + +export const LayerTextEditFactoryModal: React.FC = () => { + const layerObject = useAppSelector(mapEditToolsLayerObjectSelector); + const currentModelId = useAppSelector(currentModelIdSelector); + const dispatch = useAppDispatch(); + const { mapInstance } = useMapInstance(); + + if (!layerObject || !('notes' in layerObject)) { + throw new Error('Invalid layer text object'); + } + + const [isSending, setIsSending] = useState<boolean>(false); + const [data, setData] = useState<LayerTextFactoryForm>({ + notes: layerObject.notes, + fontSize: layerObject.fontSize, + horizontalAlign: layerObject.horizontalAlign, + verticalAlign: layerObject.verticalAlign, + color: layerObject.color, + borderColor: layerObject.borderColor, + }); + + const handleSubmit = async (): Promise<void> => { + if (!layerObject) { + return; + } + try { + const layerText = await dispatch( + updateLayerText({ + modelId: currentModelId, + layerId: layerObject.layer, + id: layerObject.id, + x: layerObject.x, + y: layerObject.y, + z: layerObject.z, + width: layerObject.width, + height: layerObject.height, + notes: data.notes, + fontSize: data.fontSize, + horizontalAlign: data.horizontalAlign, + verticalAlign: data.verticalAlign, + color: data.color, + borderColor: data.borderColor, + }), + ).unwrap(); + + if (layerText) { + dispatch(layerUpdateText({ modelId: currentModelId, layerId: layerText.layer, layerText })); + dispatch(mapEditToolsSetLayerObject(layerText)); + updateElement(mapInstance, layerText.layer, layerText); + } + showToast({ + type: 'success', + message: 'The text has been successfully updated', + }); + dispatch(closeModal()); + } catch (error) { + const typedError = error as SerializedError; + showToast({ + type: 'error', + message: typedError.message || 'An error occurred while editing the layer text', + }); + } finally { + setIsSending(false); + } + }; + + const changeValues = (value: string | number | Color, key: string): void => { + setData(prevData => ({ ...prevData, [key]: value })); + }; + + return ( + <div className="relative w-[900px] border border-t-[#E1E0E6] bg-white p-[24px]"> + {isSending && ( + <div className="c-layer-text-factory-modal-loader"> + <LoadingIndicator width={44} height={44} /> + </div> + )} + <LayerTextForm onChange={changeValues} data={data} /> + <hr className="py-2" /> + <Button + type="button" + onClick={handleSubmit} + className="justify-center self-end justify-self-end text-base font-medium" + > + Submit + </Button> + </div> + ); +}; diff --git a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.constants.ts b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.constants.ts index feddd690..cbee5e1c 100644 --- a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.constants.ts +++ b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.constants.ts @@ -1,21 +1,21 @@ /* eslint-disable no-magic-numbers */ +import { HorizontalAlign, VerticalAlign } from '@/components/Map/MapViewer/MapViewer.types'; + export const TEXT_FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 24, 30, 36, 48, 60, 72, 96]; export const TEXT_HORIZONTAL_ALIGNMENTS = [ { id: 'LEFT', name: 'left' }, { id: 'RIGHT', name: 'right' }, { id: 'CENTER', name: 'center' }, - { id: 'END', name: 'end' }, - { id: 'START', name: 'start' }, -]; +] as const; export const TEXT_VERTICAL_ALIGNMENTS = [ { id: 'TOP', name: 'top' }, { id: 'MIDDLE', name: 'middle' }, { id: 'BOTTOM', name: 'bottom' }, -]; +] as const; export const DEFAULT_TEXT_FONT_SIZE = 12; -export const DEFAULT_HORIZONTAL_ALIGNMENT = TEXT_HORIZONTAL_ALIGNMENTS[0].id; -export const DEFAULT_VERTICAL_ALIGNMENT = TEXT_VERTICAL_ALIGNMENTS[0].id; +export const DEFAULT_HORIZONTAL_ALIGNMENT: HorizontalAlign = TEXT_HORIZONTAL_ALIGNMENTS[0].id; +export const DEFAULT_VERTICAL_ALIGNMENT: VerticalAlign = TEXT_VERTICAL_ALIGNMENTS[0].id; diff --git a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.types.ts b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.types.ts index bb47feaa..231eb942 100644 --- a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.types.ts +++ b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactory.types.ts @@ -1,10 +1,11 @@ import { Color } from '@/types/models'; +import { HorizontalAlign, VerticalAlign } from '@/components/Map/MapViewer/MapViewer.types'; export type LayerTextFactoryForm = { notes: string; fontSize: number; - horizontalAlign: string; - verticalAlign: string; + horizontalAlign: HorizontalAlign; + verticalAlign: VerticalAlign; color: Color; borderColor: Color; }; diff --git a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextForm.component.tsx b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextForm.component.tsx index 22bcde3c..2dac649b 100644 --- a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextForm.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextForm.component.tsx @@ -41,7 +41,7 @@ export const LayerTextForm = ({ data, onChange }: LayerTextFormProps): React.JSX <div> <span>Horizontal alignment:</span> <Select - options={TEXT_HORIZONTAL_ALIGNMENTS} + options={[...TEXT_HORIZONTAL_ALIGNMENTS]} selectedId={data.horizontalAlign} testId="horizontal-alignment-select" onChange={value => onChange(value, 'horizontalAlign')} @@ -50,7 +50,7 @@ export const LayerTextForm = ({ data, onChange }: LayerTextFormProps): React.JSX <div> <span>Vertical alignment:</span> <Select - options={TEXT_VERTICAL_ALIGNMENTS} + options={[...TEXT_VERTICAL_ALIGNMENTS]} selectedId={data.verticalAlign} testId="vertical-alignment-select" onChange={value => onChange(value, 'verticalAlign')} diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx index 6cc342ee..bbd8c7d5 100644 --- a/src/components/FunctionalArea/Modal/Modal.component.tsx +++ b/src/components/FunctionalArea/Modal/Modal.component.tsx @@ -10,6 +10,7 @@ import { LayerImageObjectFactoryModal, } from '@/components/FunctionalArea/Modal/LayerImageObjectModal'; import { LayerTextFactoryModal } from '@/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component'; +import { LayerTextEditFactoryModal } from '@/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextEditFactoryModal.component'; import { EditOverlayModal } from './EditOverlayModal'; import { LoginModal } from './LoginModal'; import { ErrorReportModal } from './ErrorReportModal'; @@ -105,6 +106,11 @@ export const Modal = (): React.ReactNode => { <LayerTextFactoryModal /> </ModalLayout> )} + {isOpen && modalName === 'layer-text-edit-factory' && ( + <ModalLayout> + <LayerTextEditFactoryModal /> + </ModalLayout> + )} </> ); }; diff --git a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx index 59d85797..e9fd515a 100644 --- a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx +++ b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx @@ -34,7 +34,8 @@ export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => { modalName === 'add-comment' && 'h-auto w-[400px]', modalName === 'error-report' && 'h-auto w-[800px]', modalName === 'layer-factory' && 'h-auto w-[400px]', - modalName === 'layer-text-factory' && 'h-auto w-[900px]', + ['layer-text-factory', 'layer-text-edit-factory'].includes(modalName) && + 'h-auto w-[900px]', ['layer-image-object-factory', 'layer-image-object-edit-factory'].includes(modalName) && 'h-auto w-[800px]', ['edit-overlay', 'logged-in-menu'].includes(modalName) && 'h-auto w-[432px]', diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx index fa98e051..70a6acf5 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx @@ -13,8 +13,14 @@ import { removeLayerImage, removeLayerText, updateLayerImageObject, + updateLayerText, } from '@/redux/layers/layers.thunks'; -import { layerDeleteImage, layerDeleteText, layerUpdateImage } from '@/redux/layers/layers.slice'; +import { + layerDeleteImage, + layerDeleteText, + layerUpdateImage, + layerUpdateText, +} from '@/redux/layers/layers.slice'; import removeElementFromLayer from '@/components/Map/MapViewer/utils/shapes/elements/removeElementFromLayer'; import { showToast } from '@/utils/showToast'; import { SerializedError } from '@reduxjs/toolkit'; @@ -28,7 +34,10 @@ import { useSetBounds } from '@/utils/map/useSetBounds'; import { mapEditToolsLayerObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { Coordinate } from 'ol/coordinate'; -import { openLayerImageObjectEditFactoryModal } from '@/redux/modal/modal.slice'; +import { + openLayerImageObjectEditFactoryModal, + openLayerTextEditFactoryModal, +} from '@/redux/modal/modal.slice'; interface LayersDrawerObjectsListProps { layerId: number; @@ -173,6 +182,42 @@ export const LayersDrawerObjectsList = ({ await updateImageZIndex({ zIndex: lowestZIndex - 1, layerImage }); }; + const updateTextZIndex = async ({ + zIndex, + layerText, + }: { + zIndex: number; + layerText: LayerText; + }): Promise<void> => { + const newLayerText = await dispatch( + updateLayerText({ + modelId: currentModelId, + layerId: layerText.layer, + ...layerText, + z: zIndex, + }), + ).unwrap(); + if (newLayerText) { + dispatch( + layerUpdateText({ + modelId: currentModelId, + layerId: newLayerText.layer, + layerText: newLayerText, + }), + ); + dispatch(mapEditToolsSetLayerObject(newLayerText)); + updateElement(mapInstance, newLayerText.layer, newLayerText); + } + }; + + const bringTextToFront = async (layerText: LayerText): Promise<void> => { + await updateTextZIndex({ zIndex: highestZIndex + 1, layerText }); + }; + + const bringTextToBack = async (layerText: LayerText): Promise<void> => { + await updateTextZIndex({ zIndex: lowestZIndex - 1, layerText }); + }; + const centerObject = (layerObject: LayerImage | LayerText): void => { if (mapEditToolsLayerImageObject && mapEditToolsLayerImageObject.id === layerObject.id) { const point1 = pointToProjection({ x: layerObject.x, y: layerObject.y }); @@ -188,6 +233,10 @@ export const LayersDrawerObjectsList = ({ dispatch(openLayerImageObjectEditFactoryModal()); }; + const editText = (): void => { + dispatch(openLayerTextEditFactoryModal()); + }; + if (!layer) { return null; } @@ -208,11 +257,11 @@ export const LayersDrawerObjectsList = ({ <LayersDrawerTextItem layerText={layerText} key={layerText.id} - bringToFront={() => {}} - bringToBack={() => {}} + bringToFront={() => bringTextToFront(layerText)} + bringToBack={() => bringTextToBack(layerText)} removeObject={() => removeObject(layerText)} centerObject={() => centerObject(layerText)} - editObject={() => {}} + editObject={() => editText()} isLayerVisible={isLayerVisible} isLayerActive={isLayerActive} /> diff --git a/src/components/Map/MapViewer/MapViewer.types.ts b/src/components/Map/MapViewer/MapViewer.types.ts index f10c9070..4204320f 100644 --- a/src/components/Map/MapViewer/MapViewer.types.ts +++ b/src/components/Map/MapViewer/MapViewer.types.ts @@ -8,7 +8,7 @@ export type MapConfig = { }; export type VerticalAlign = 'TOP' | 'MIDDLE' | 'BOTTOM'; -export type HorizontalAlign = 'LEFT' | 'RIGHT' | 'CENTER' | 'END' | 'START'; +export type HorizontalAlign = 'LEFT' | 'RIGHT' | 'CENTER'; export type OverlayBioEntityGroupedElementsType = { [id: string]: Array<OverlayBioEntityRender & { amount: number }>; diff --git a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts b/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts index 342cdfac..57b36875 100644 --- a/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts +++ b/src/components/Map/MapViewer/utils/shapes/elements/Glyph/Glyph.ts @@ -34,13 +34,13 @@ export type GlyphProps = { export default class Glyph { feature: Feature<Polygon>; - style: Style = new Style({}); + style: Style = new Style(); - noGlyphStyle: Style; + noGlyphStyle: Style = new Style(); imageScale: number = 1; - polygonStyle: Style; + polygonStyle: Style = new Style(); polygon: Polygon = new Polygon([]); @@ -113,26 +113,7 @@ export default class Glyph { this.drawPolygon(); - this.polygonStyle = getStyle({ - geometry: this.polygon, - zIndex: this.zIndex, - borderColor: { ...WHITE_COLOR, alpha: 0 }, - fillColor: { ...WHITE_COLOR, alpha: 0 }, - }); - - this.noGlyphStyle = getStyle({ - geometry: this.polygon, - zIndex: this.zIndex, - fillColor: '#E7E7E7', - }); - this.noGlyphStyle.setText( - new Text({ - text: 'No image', - font: '12pt Arial', - fill: getFill({ color: '#000' }), - overflow: true, - }), - ); + this.setStyles(); this.feature = new Feature({ geometry: this.polygon, @@ -172,6 +153,29 @@ export default class Glyph { ]); } + private setStyles(): void { + this.polygonStyle = getStyle({ + geometry: this.polygon, + zIndex: this.zIndex, + borderColor: { ...WHITE_COLOR, alpha: 0 }, + fillColor: { ...WHITE_COLOR, alpha: 0 }, + }); + + this.noGlyphStyle = getStyle({ + geometry: this.polygon, + zIndex: this.zIndex, + fillColor: '#E7E7E7', + }); + this.noGlyphStyle.setText( + new Text({ + text: 'No image', + font: '12pt Arial', + fill: getFill({ color: '#000' }), + overflow: true, + }), + ); + } + private refreshPolygon(): void { this.drawPolygon(); this.polygonStyle.setGeometry(this.polygon); @@ -195,7 +199,7 @@ export default class Glyph { this.glyphId = imageObject.glyph; this.refreshPolygon(); - this.refreshZIndex(); + this.setStyles(); this.drawImage(); this.feature.changed(); } diff --git a/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts index 133034cf..26565345 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/elements/LayerText.ts @@ -5,7 +5,7 @@ import { Point } from 'ol/geom'; import { Feature } from 'ol'; import { FeatureLike } from 'ol/Feature'; import { MapInstance } from '@/types/map'; -import { LayerText as LayerTextModel, Color } from '@/types/models'; +import { Color, LayerText as LayerTextModel } from '@/types/models'; import { TEXT_CUTOFF_SCALE } from '@/components/Map/MapViewer/MapViewer.constants'; import { BoundingBox, @@ -77,13 +77,13 @@ export default class LayerText { fontSize: number; - style: Style; + style: Style = new Style(); - polygonStyle: Style; + polygonStyle: Style = new Style(); polygon: Polygon = new Polygon([]); - strokeStyle: Stroke; + strokeStyle: Stroke = new Stroke(); point: Point; @@ -140,29 +140,11 @@ export default class LayerText { horizontalAlign, pointToProjection, }); + this.point = new Point(textCoords); + this.drawPolygon(); - this.strokeStyle = getStroke({ - color: rgbToHex(borderColor), - width: 1, - }); - this.polygonStyle = getStyle({ - geometry: this.polygon, - borderColor, - fillColor: { rgb: 0, alpha: 0 }, - lineWidth: 1, - zIndex, - }); - const textStyle = getTextStyle({ - text, - fontSize, - color: rgbToHex(color), - zIndex, - horizontalAlign, - }); - this.point = new Point(textCoords); - this.style = textStyle; - this.style.setGeometry(this.point); + this.setStyles(); this.feature = new Feature({ geometry: this.polygon, @@ -254,6 +236,28 @@ export default class LayerText { this.feature.changed(); } + private setStyles(): void { + this.strokeStyle = getStroke({ + color: rgbToHex(this.borderColor), + width: 1, + }); + this.polygonStyle = getStyle({ + geometry: this.polygon, + borderColor: this.borderColor, + fillColor: { rgb: 0, alpha: 0 }, + lineWidth: 1, + zIndex: this.zIndex, + }); + this.style = getTextStyle({ + text: this.text, + fontSize: this.fontSize, + color: rgbToHex(this.color), + zIndex: this.zIndex, + horizontalAlign: this.horizontalAlign, + }); + this.style.setGeometry(this.point); + } + private updateElement(layerText: LayerTextModel): void { this.elementId = layerText.id; this.x = layerText.x; @@ -269,7 +273,7 @@ export default class LayerText { this.horizontalAlign = layerText.horizontalAlign; this.refreshPolygon(); - this.refreshZIndex(); + this.setStyles(); this.feature.changed(); } diff --git a/src/models/bioEntitySchema.ts b/src/models/bioEntitySchema.ts index ec78db95..5c754989 100644 --- a/src/models/bioEntitySchema.ts +++ b/src/models/bioEntitySchema.ts @@ -26,7 +26,7 @@ export const bioEntitySchema = z.object({ nameWidth: z.number(), nameHeight: z.number(), nameVerticalAlign: z.enum(['TOP', 'MIDDLE', 'BOTTOM']), - nameHorizontalAlign: z.enum(['LEFT', 'RIGHT', 'CENTER', 'END', 'START']), + nameHorizontalAlign: z.enum(['LEFT', 'RIGHT', 'CENTER']), width: z .number() .optional() diff --git a/src/models/layerTextSchema.ts b/src/models/layerTextSchema.ts index a575b139..d0460100 100644 --- a/src/models/layerTextSchema.ts +++ b/src/models/layerTextSchema.ts @@ -12,7 +12,7 @@ export const layerTextSchema = z.object({ fontSize: z.number(), notes: z.string(), verticalAlign: z.enum(['TOP', 'MIDDLE', 'BOTTOM']), - horizontalAlign: z.enum(['LEFT', 'RIGHT', 'CENTER', 'END', 'START']), + horizontalAlign: z.enum(['LEFT', 'RIGHT', 'CENTER']), backgroundColor: colorSchema, borderColor: colorSchema, color: colorSchema, diff --git a/src/models/modelElementModificationSchema.ts b/src/models/modelElementModificationSchema.ts index 3d7f318e..d69ce89a 100644 --- a/src/models/modelElementModificationSchema.ts +++ b/src/models/modelElementModificationSchema.ts @@ -15,7 +15,7 @@ export const modelElementModificationSchema = z.object({ nameWidth: z.number(), nameHeight: z.number(), nameVerticalAlign: z.enum(['TOP', 'MIDDLE', 'BOTTOM']), - nameHorizontalAlign: z.enum(['LEFT', 'RIGHT', 'CENTER', 'END', 'START']), + nameHorizontalAlign: z.enum(['LEFT', 'RIGHT', 'CENTER']), species: z.number(), borderColor: colorSchema, fillColor: colorSchema, diff --git a/src/models/modelElementSchema.ts b/src/models/modelElementSchema.ts index 86b82f67..32eae25e 100644 --- a/src/models/modelElementSchema.ts +++ b/src/models/modelElementSchema.ts @@ -36,7 +36,7 @@ export const modelElementSchema = z.object({ nameWidth: z.number(), nameHeight: z.number(), nameVerticalAlign: z.enum(['TOP', 'MIDDLE', 'BOTTOM']), - nameHorizontalAlign: z.enum(['LEFT', 'RIGHT', 'CENTER', 'END', 'START']), + nameHorizontalAlign: z.enum(['LEFT', 'RIGHT', 'CENTER']), synonyms: z.array(z.string()), formerSymbols: z.array(z.string()), activity: z.boolean().optional(), diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts index 1b903069..923d2858 100644 --- a/src/redux/modal/modal.reducers.ts +++ b/src/redux/modal/modal.reducers.ts @@ -165,3 +165,9 @@ export const openLayerTextFactoryModalReducer = ( state.modalTitle = 'Add text'; state.isOpen = true; }; + +export const openLayerTextEditFactoryModalReducer = (state: ModalState): void => { + state.isOpen = true; + state.modalName = 'layer-text-edit-factory'; + state.modalTitle = 'Edit text'; +}; diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts index bf01d86a..ed74baec 100644 --- a/src/redux/modal/modal.slice.ts +++ b/src/redux/modal/modal.slice.ts @@ -20,6 +20,7 @@ import { openLayerImageObjectFactoryModalReducer, openLayerImageObjectEditFactoryModalReducer, openLayerTextFactoryModalReducer, + openLayerTextEditFactoryModalReducer, } from './modal.reducers'; const modalSlice = createSlice({ @@ -45,6 +46,7 @@ const modalSlice = createSlice({ openLayerImageObjectFactoryModal: openLayerImageObjectFactoryModalReducer, openLayerImageObjectEditFactoryModal: openLayerImageObjectEditFactoryModalReducer, openLayerTextFactoryModal: openLayerTextFactoryModalReducer, + openLayerTextEditFactoryModal: openLayerTextEditFactoryModalReducer, }, }); @@ -68,6 +70,7 @@ export const { openLayerImageObjectFactoryModal, openLayerImageObjectEditFactoryModal, openLayerTextFactoryModal, + openLayerTextEditFactoryModal, } = modalSlice.actions; export default modalSlice.reducer; diff --git a/src/types/modal.ts b/src/types/modal.ts index 8b02e86a..579c8137 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -15,4 +15,5 @@ export type ModalName = | 'layer-factory' | 'layer-image-object-factory' | 'layer-image-object-edit-factory' - | 'layer-text-factory'; + | 'layer-text-factory' + | 'layer-text-edit-factory'; -- GitLab From 7f8d0b665cbd00568e46ba57597c5d22ab28192b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Tue, 18 Feb 2025 10:10:19 +0100 Subject: [PATCH 3/8] feat(layers): add layers zIndex editing --- .../LayerFactoryModal.component.tsx | 4 + ...LayerImageObjectFactoryModal.component.tsx | 9 +- .../LayerTextFactoryModal.component.tsx | 9 +- .../LayerDrawerTextItem.component.tsx | 12 +- .../LayersDrawer/LayersDrawer.component.tsx | 46 ++++-- .../LayersDrawerImageItem.component.tsx | 12 +- .../LayersDrawerLayer.component.tsx | 107 +++++++++++--- .../LayersDrawerLayerActions.component.tsx | 39 +++++ .../LayersDrawerObjectActions.component.tsx | 20 +-- .../LayersDrawerObjectsList.component.tsx | 32 ++-- .../useOlMapAdditionalLayers.ts | 1 + .../commentsLayer/useOlMapCommentsLayer.ts | 1 + .../config/mapCardLayer/useOlMapCardLayer.ts | 1 + .../config/pinsLayer/useOlMapPinsLayer.ts | 1 + .../processLayer/useOlMapProcessLayer.ts | 1 + .../reactionsLayer/useOlMapReactionsLayer.ts | 1 + .../utils/shapes/layer/Layer.test.ts | 1 + .../Map/MapViewer/utils/shapes/layer/Layer.ts | 3 + src/models/layerSchema.ts | 1 + src/redux/layers/layers.reducers.test.ts | 4 +- src/redux/layers/layers.selectors.ts | 139 +++++++++++++----- src/redux/layers/layers.thunks.test.ts | 18 ++- src/redux/layers/layers.thunks.ts | 28 ++-- src/redux/layers/layers.types.ts | 2 + src/shared/Icon/Icon.component.tsx | 12 +- src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx | 12 +- src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx | 12 +- src/shared/Icon/Icons/LayerArrowDownIcon.tsx | 18 +++ src/shared/Icon/Icons/LayerArrowUpIcon.tsx | 18 +++ .../{BringBackIcon.tsx => MoveBackIcon.tsx} | 6 +- .../{BringFrontIcon.tsx => MoveFrontIcon.tsx} | 6 +- .../IconButton/IconButton.component.tsx | 8 +- src/types/iconTypes.ts | 6 +- 33 files changed, 430 insertions(+), 160 deletions(-) create mode 100644 src/shared/Icon/Icons/LayerArrowDownIcon.tsx create mode 100644 src/shared/Icon/Icons/LayerArrowUpIcon.tsx rename src/shared/Icon/Icons/{BringBackIcon.tsx => MoveBackIcon.tsx} (78%) rename src/shared/Icon/Icons/{BringFrontIcon.tsx => MoveFrontIcon.tsx} (77%) diff --git a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx index 8d925951..b806d2b6 100644 --- a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx @@ -19,11 +19,13 @@ import { SerializedError } from '@reduxjs/toolkit'; import { layerFactoryStateSelector } from '@/redux/modal/modal.selector'; import './LayerFactoryModal.styles.css'; import { LoadingIndicator } from '@/shared/LoadingIndicator'; +import { maxLayerZIndexAboveDiagramSelector } from '@/redux/layers/layers.selectors'; export const LayerFactoryModal: React.FC = () => { const dispatch = useAppDispatch(); const currentModelId = useAppSelector(currentModelIdSelector); const layerFactoryState = useAppSelector(layerFactoryStateSelector); + const maxLayerZIndexAboveDiagram = useAppSelector(maxLayerZIndexAboveDiagramSelector); const [loaded, setLoaded] = useState<boolean>(false); const [data, setData] = useState<LayerStoreInterface>({ @@ -31,6 +33,7 @@ export const LayerFactoryModal: React.FC = () => { visible: false, locked: false, modelId: currentModelId, + zIndex: maxLayerZIndexAboveDiagram, }); const fetchData = useMemo(() => { @@ -42,6 +45,7 @@ export const LayerFactoryModal: React.FC = () => { visible: layer.visible, locked: layer.locked, modelId: currentModelId, + zIndex: layer.z, }); } setLoaded(true); diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx index 1efb7d88..4d1513e9 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx @@ -4,7 +4,10 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { layerImageObjectFactoryStateSelector } from '@/redux/modal/modal.selector'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; -import { highestZIndexSelector, layersDrawLayerSelector } from '@/redux/layers/layers.selectors'; +import { + layersDrawLayerSelector, + maxObjectZIndexForLayerSelector, +} from '@/redux/layers/layers.selectors'; import { addLayerImageObject } from '@/redux/layers/layers.thunks'; import { addGlyph } from '@/redux/glyphs/glyphs.thunks'; import { SerializedError } from '@reduxjs/toolkit'; @@ -22,7 +25,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => { const drawLayer = useAppSelector(layersDrawLayerSelector); const layerImageObjectFactoryState = useAppSelector(layerImageObjectFactoryStateSelector); const dispatch = useAppDispatch(); - const highestZIndex = useAppSelector(highestZIndexSelector); + const maxZIndex = useAppSelector(state => maxObjectZIndexForLayerSelector(state, drawLayer)); const { mapInstance } = useMapInstance(); const [selectedGlyph, setSelectedGlyph] = useState<number | null>(null); @@ -49,7 +52,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => { layerId: drawLayer, x: layerImageObjectFactoryState.x, y: layerImageObjectFactoryState.y, - z: highestZIndex + 1, + z: maxZIndex + 1, width: layerImageObjectFactoryState.width, height: layerImageObjectFactoryState.height, glyph: glyphId, diff --git a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx index e1ccf612..bfb80aff 100644 --- a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx @@ -13,7 +13,10 @@ import { LayerTextFactoryForm } from '@/components/FunctionalArea/Modal/LayerTex import { Color } from '@/types/models'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { layerTextFactoryStateSelector } from '@/redux/modal/modal.selector'; -import { highestZIndexSelector, layersDrawLayerSelector } from '@/redux/layers/layers.selectors'; +import { + layersDrawLayerSelector, + maxObjectZIndexForLayerSelector, +} from '@/redux/layers/layers.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { showToast } from '@/utils/showToast'; @@ -31,7 +34,7 @@ export const LayerTextFactoryModal: React.FC = () => { const currentModelId = useAppSelector(currentModelIdSelector); const layerTextFactoryState = useAppSelector(layerTextFactoryStateSelector); const dispatch = useAppDispatch(); - const highestZIndex = useAppSelector(highestZIndexSelector); + const maxZIndex = useAppSelector(state => maxObjectZIndexForLayerSelector(state, drawLayer)); const { mapInstance } = useMapInstance(); const [isSending, setIsSending] = useState<boolean>(false); @@ -55,7 +58,7 @@ export const LayerTextFactoryModal: React.FC = () => { layerId: drawLayer, boundingBox: layerTextFactoryState, textData: data, - z: highestZIndex + 1, + z: maxZIndex + 1, }), ).unwrap(); if (!textData) { diff --git a/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx index 6f57c762..a9c3d3b3 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx @@ -10,8 +10,8 @@ import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors' interface LayersDrawerTextItemProps { layerText: LayerText; - bringToFront: () => void; - bringToBack: () => void; + moveToFront: () => void; + moveToBack: () => void; removeObject: () => void; centerObject: () => void; editObject: () => void; @@ -21,8 +21,8 @@ interface LayersDrawerTextItemProps { export const LayersDrawerTextItem = ({ layerText, - bringToFront, - bringToBack, + moveToFront, + moveToBack, removeObject, centerObject, editObject, @@ -68,8 +68,8 @@ export const LayersDrawerTextItem = ({ </div> {showActions && ( <LayersDrawerObjectActions - bringToFront={bringToFront} - bringToBack={bringToBack} + moveToFront={moveToFront} + moveToBack={moveToBack} removeObject={removeObject} centerObject={centerObject} editObject={editObject} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx index 12bef081..33c259f3 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx @@ -4,7 +4,7 @@ import { DrawerHeading } from '@/shared/DrawerHeading'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { layersForCurrentModelSelector } from '@/redux/layers/layers.selectors'; import { Button } from '@/shared/Button'; -import { JSX, useEffect, useRef } from 'react'; +import { JSX, useEffect, useMemo, useRef } from 'react'; import { openLayerFactoryModal } from '@/redux/modal/modal.slice'; import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; import { LayersDrawerLayer } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component'; @@ -21,6 +21,19 @@ export const LayersDrawer = (): JSX.Element => { dispatch(openLayerFactoryModal()); }; + const sortedLayers = useMemo(() => { + return [...layersForCurrentModel].sort((layerA, layerB) => layerB.details.z - layerA.details.z); + }, [layersForCurrentModel]); + + const negativeZLayers = useMemo( + () => sortedLayers.filter(layer => layer.details.z < 0), + [sortedLayers], + ); + const positiveZLayers = useMemo( + () => sortedLayers.filter(layer => layer.details.z >= 0), + [sortedLayers], + ); + useEffect(() => { if (!mapEditToolsLayerImageObject || !layersDrawerRef.current) { return; @@ -61,20 +74,33 @@ export const LayersDrawer = (): JSX.Element => { ref={layersDrawerRef} > {hasPrivilegeToWriteProject && ( - <div className="flex justify-start pt-2"> + <div className="flex justify-start py-2"> <Button icon="plus" isIcon isFrontIcon onClick={addNewLayer}> Add layer </Button> </div> )} - <div className="flex flex-col gap-4"> - {layersForCurrentModel.map(layer => ( - <LayersDrawerLayer - key={layer.details.id} - layerId={layer.details.id} - layerName={layer.details.name} - /> - ))} + <div className="flex flex-col gap-2"> + {Boolean(positiveZLayers.length) && ( + <span className="border-b-2 border-dashed border-b-gray-400 text-center text-lg font-semibold"> + Layers above the diagram + </span> + )} + <div className="flex flex-col gap-5"> + {positiveZLayers.map(layer => ( + <LayersDrawerLayer key={layer.details.id} layerDetails={layer.details} /> + ))} + </div> + {Boolean(negativeZLayers.length) && ( + <span className="border-b-2 border-dashed border-b-gray-400 text-center text-lg font-semibold"> + Layers below the diagram + </span> + )} + <div className="flex flex-col gap-5"> + {negativeZLayers.map(layer => ( + <LayersDrawerLayer key={layer.details.id} layerDetails={layer.details} /> + ))} + </div> </div> </div> </div> diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx index 0622f141..e160b807 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx @@ -11,8 +11,8 @@ import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors' interface LayersDrawerImageItemProps { layerImage: LayerImage; - bringToFront: () => void; - bringToBack: () => void; + moveToFront: () => void; + moveToBack: () => void; removeObject: () => void; centerObject: () => void; editObject: () => void; @@ -22,8 +22,8 @@ interface LayersDrawerImageItemProps { export const LayersDrawerImageItem = ({ layerImage, - bringToFront, - bringToBack, + moveToFront, + moveToBack, removeObject, centerObject, editObject, @@ -72,8 +72,8 @@ export const LayersDrawerImageItem = ({ </div> {showActions && ( <LayersDrawerObjectActions - bringToFront={bringToFront} - bringToBack={bringToBack} + moveToFront={moveToFront} + moveToBack={moveToBack} removeObject={removeObject} centerObject={centerObject} editObject={editObject} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx index 49b8536f..8b0f9b01 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx @@ -1,14 +1,19 @@ +/* eslint-disable no-magic-numbers */ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { layersActiveLayersSelector, layersVisibilityForCurrentModelSelector, + maxLayerZIndexAboveDiagramSelector, + maxLayerZIndexBelowDiagramSelector, + minLayerZIndexAboveDiagramSelector, + minLayerZIndexBelowDiagramSelector, } from '@/redux/layers/layers.selectors'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { openLayerFactoryModal } from '@/redux/modal/modal.slice'; import QuestionModal from '@/components/FunctionalArea/Modal/QuestionModal/QustionModal.component'; import { useState, JSX, useMemo } from 'react'; -import { getLayersForModel, removeLayer } from '@/redux/layers/layers.thunks'; +import { getLayersForModel, removeLayer, updateLayer } from '@/redux/layers/layers.thunks'; import { showToast } from '@/utils/showToast'; import { SerializedError } from '@reduxjs/toolkit'; import { LayersDrawerLayerActions } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component'; @@ -21,38 +26,42 @@ import { import { LayersDrawerObjectsList } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component'; import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice'; import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; +import { Layer } from '@/types/models'; interface LayersDrawerLayerProps { - layerId: number; - layerName: string; + layerDetails: Layer; } -export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps): JSX.Element => { +export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX.Element => { const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector); const activeLayers = useAppSelector(layersActiveLayersSelector); const currentModelId = useAppSelector(currentModelIdSelector); + const maxLayerZIndexAboveDiagram = useAppSelector(maxLayerZIndexAboveDiagramSelector); + const maxLayerZIndexBelowDiagram = useAppSelector(maxLayerZIndexBelowDiagramSelector); + const minLayerZIndexAboveDiagram = useAppSelector(minLayerZIndexAboveDiagramSelector); + const minLayerZIndexBelowDiagram = useAppSelector(minLayerZIndexBelowDiagramSelector); const dispatch = useAppDispatch(); const [isModalOpen, setIsModalOpen] = useState(false); const isLayerVisible = useMemo(() => { - return layersVisibilityForCurrentModel[layerId]; - }, [layerId, layersVisibilityForCurrentModel]); + return layersVisibilityForCurrentModel[layerDetails.id]; + }, [layerDetails.id, layersVisibilityForCurrentModel]); const isLayerActive = useMemo(() => { - return activeLayers.includes(layerId); - }, [activeLayers, layerId]); + return activeLayers.includes(layerDetails.id); + }, [activeLayers, layerDetails.id]); const editLayer = (): void => { - dispatch(openLayerFactoryModal(layerId)); + dispatch(openLayerFactoryModal(layerDetails.id)); }; const addImage = (): void => { - dispatch(setDrawLayer({ modelId: currentModelId, layerId })); + dispatch(setDrawLayer({ modelId: currentModelId, layerId: layerDetails.id })); dispatch(mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.DRAW_IMAGE)); }; const addText = (): void => { - dispatch(setDrawLayer({ modelId: currentModelId, layerId })); + dispatch(setDrawLayer({ modelId: currentModelId, layerId: layerDetails.id })); dispatch(mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.ADD_TEXT)); }; @@ -61,11 +70,11 @@ export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps }; const confirmRemove = async (): Promise<void> => { - if (!layerId) { + if (!layerDetails.id) { return; } try { - await dispatch(removeLayer({ modelId: currentModelId, layerId })).unwrap(); + await dispatch(removeLayer({ modelId: currentModelId, layerId: layerDetails.id })).unwrap(); showToast({ type: 'success', message: 'The layer has been successfully removed', @@ -87,12 +96,61 @@ export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps const toggleActiveLayer = (value: boolean): void => { if (value) { - dispatch(setLayerToActive({ modelId: currentModelId, layerId })); + dispatch(setLayerToActive({ modelId: currentModelId, layerId: layerDetails.id })); } else { - dispatch(setLayerToInactive({ modelId: currentModelId, layerId })); + dispatch(setLayerToInactive({ modelId: currentModelId, layerId: layerDetails.id })); } }; + const updateLayerZIndex = async (zIndex: number): Promise<void> => { + try { + dispatch( + updateLayer({ + name: layerDetails.name, + visible: layerDetails.visible, + locked: layerDetails.locked, + modelId: currentModelId, + layerId: layerDetails.id, + zIndex, + }), + ); + } catch (error) { + const typedError = error as SerializedError; + showToast({ + type: 'error', + message: typedError.message || 'An error occurred while updating the layer', + }); + } + }; + + const moveToFront = (): void => { + if (layerDetails.z > 0) { + updateLayerZIndex(maxLayerZIndexAboveDiagram); + } else if (layerDetails.z < 0) { + const zIndex = Math.min(maxLayerZIndexBelowDiagram, -1); + updateLayerZIndex(zIndex); + } + }; + + const moveToBack = (): void => { + if (layerDetails.z > 0) { + const zIndex = Math.max(minLayerZIndexAboveDiagram, 1); + updateLayerZIndex(zIndex); + } else if (layerDetails.z < 0) { + updateLayerZIndex(minLayerZIndexBelowDiagram); + } + }; + + const moveAboveDiagram = (): void => { + const zIndex = Math.max(minLayerZIndexAboveDiagram - 1, 1); + updateLayerZIndex(zIndex); + }; + + const moveBelowDiagram = (): void => { + const zIndex = Math.min(minLayerZIndexBelowDiagram + 1, -1); + updateLayerZIndex(zIndex); + }; + return ( <div> <QuestionModal @@ -101,9 +159,13 @@ export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps onConfirm={confirmRemove} question="Are you sure you want to remove the layer?" /> - <div className="flex items-center justify-between py-3"> - <span className={`font-semibold ${isLayerVisible ? 'opacity-100' : 'opacity-40'}`}> - {layerName} + <div className="flex items-center justify-between pb-3"> + <span + className={`font-semibold ${ + isLayerVisible ? 'opacity-100' : 'opacity-40' + } min-w-0 flex-1 truncate `} + > + {layerDetails.name} </span> <LayersDrawerLayerActions toggleVisibility={() => @@ -111,7 +173,7 @@ export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps setLayerVisibility({ modelId: currentModelId, visible: !isLayerVisible, - layerId, + layerId: layerDetails.id, }), ) } @@ -120,12 +182,17 @@ export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps removeLayer={onRemoveLayer} addImage={addImage} addText={addText} + moveToFront={moveToFront} + moveToBack={moveToBack} + moveAboveDiagram={moveAboveDiagram} + moveBelowDiagram={moveBelowDiagram} + zIndex={layerDetails.z} isVisible={isLayerVisible} isActive={isLayerActive} /> </div> <LayersDrawerObjectsList - layerId={layerId} + layerId={layerDetails.id} isLayerVisible={isLayerVisible} isLayerActive={isLayerActive} /> diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx index 18627b13..81435100 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-magic-numbers */ import { IconButton } from '@/shared/IconButton'; import { JSX } from 'react'; import { LayerDrawerLayerContextMenu } from '@/components/Map/Drawer/LayersDrawer/LayerDrawerLayerContextMenu.component'; @@ -9,10 +10,15 @@ type LayersDrawerLayerActionsProps = { removeLayer: () => void; isVisible: boolean; isActive: boolean; + zIndex: number; toggleVisibility: () => void; toggleActiveLayer: (value: boolean) => void; addImage: () => void; addText: () => void; + moveToFront: () => void; + moveToBack: () => void; + moveAboveDiagram: () => void; + moveBelowDiagram: () => void; }; export const LayersDrawerLayerActions = ({ @@ -20,10 +26,15 @@ export const LayersDrawerLayerActions = ({ removeLayer, isVisible, isActive, + zIndex, toggleVisibility, toggleActiveLayer, addImage, addText, + moveToFront, + moveToBack, + moveAboveDiagram, + moveBelowDiagram, }: LayersDrawerLayerActionsProps): JSX.Element => { const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); @@ -43,6 +54,34 @@ export const LayersDrawerLayerActions = ({ className="h-auto w-auto bg-transparent p-0" onClick={() => toggleActiveLayer(!isActive)} /> + <IconButton + title="Move to front" + icon="move-front" + className="h-auto w-auto bg-transparent p-0" + onClick={moveToFront} + /> + <IconButton + title="Move to back" + icon="move-back" + className="h-auto w-auto bg-transparent p-0" + onClick={moveToBack} + /> + {zIndex < 0 && ( + <IconButton + title="Move above the diagram" + icon="layer-arrow-up" + className="h-auto w-auto bg-transparent p-0" + onClick={moveAboveDiagram} + /> + )} + {zIndex > 0 && ( + <IconButton + title="Move below the diagram" + icon="layer-arrow-down" + className="h-auto w-auto bg-transparent p-0" + onClick={moveBelowDiagram} + /> + )} <LayerDrawerLayerContextMenu removeLayer={removeLayer} editLayer={editLayer} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx index 4cf6871e..a97189ea 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx @@ -2,16 +2,16 @@ import { JSX } from 'react'; import { IconButton } from '@/shared/IconButton'; interface LayersDrawerObjectActionsProps { - bringToFront: () => void; - bringToBack: () => void; + moveToFront: () => void; + moveToBack: () => void; removeObject: () => void; centerObject: () => void; editObject: () => void; } export const LayersDrawerObjectActions = ({ - bringToFront, - bringToBack, + moveToFront, + moveToBack, removeObject, centerObject, editObject, @@ -31,16 +31,16 @@ export const LayersDrawerObjectActions = ({ onClick={editObject} /> <IconButton - icon="bring-front" + icon="move-front" className="h-auto w-auto bg-transparent p-0" - title="Bring to front" - onClick={bringToFront} + title="Move to front" + onClick={moveToFront} /> <IconButton - icon="bring-back" + icon="move-back" className="h-auto w-auto bg-transparent p-0" - title="Bring to back" - onClick={bringToBack} + title="Move to back" + onClick={moveToBack} /> <IconButton icon="trash" diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx index 70a6acf5..9ec19fe4 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx @@ -1,9 +1,9 @@ /* eslint-disable no-magic-numbers */ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { - highestZIndexSelector, layerByIdSelector, - lowestZIndexSelector, + maxObjectZIndexForLayerSelector, + minObjectZIndexForLayerSelector, } from '@/redux/layers/layers.selectors'; import { JSX, useState } from 'react'; import { LayersDrawerImageItem } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component'; @@ -64,8 +64,8 @@ export const LayersDrawerObjectsList = ({ isLayerActive, }: LayersDrawerObjectsListProps): JSX.Element | null => { const currentModelId = useAppSelector(mapModelIdSelector); - const highestZIndex = useAppSelector(highestZIndexSelector); - const lowestZIndex = useAppSelector(lowestZIndexSelector); + const maxZIndex = useAppSelector(state => maxObjectZIndexForLayerSelector(state, layerId)); + const minZIndex = useAppSelector(state => minObjectZIndexForLayerSelector(state, layerId)); const layer = useAppSelector(state => layerByIdSelector(state, layerId)); const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerObjectSelector); const [removeModalState, setRemoveModalState] = useState<undefined | 'text' | 'image'>(undefined); @@ -174,12 +174,12 @@ export const LayersDrawerObjectsList = ({ } }; - const bringImageToFront = async (layerImage: LayerImage): Promise<void> => { - await updateImageZIndex({ zIndex: highestZIndex + 1, layerImage }); + const moveImageToFront = async (layerImage: LayerImage): Promise<void> => { + await updateImageZIndex({ zIndex: maxZIndex + 1, layerImage }); }; - const bringImageToBack = async (layerImage: LayerImage): Promise<void> => { - await updateImageZIndex({ zIndex: lowestZIndex - 1, layerImage }); + const moveImageToBack = async (layerImage: LayerImage): Promise<void> => { + await updateImageZIndex({ zIndex: minZIndex - 1, layerImage }); }; const updateTextZIndex = async ({ @@ -210,12 +210,12 @@ export const LayersDrawerObjectsList = ({ } }; - const bringTextToFront = async (layerText: LayerText): Promise<void> => { - await updateTextZIndex({ zIndex: highestZIndex + 1, layerText }); + const moveTextToFront = async (layerText: LayerText): Promise<void> => { + await updateTextZIndex({ zIndex: maxZIndex + 1, layerText }); }; - const bringTextToBack = async (layerText: LayerText): Promise<void> => { - await updateTextZIndex({ zIndex: lowestZIndex - 1, layerText }); + const moveTextToBack = async (layerText: LayerText): Promise<void> => { + await updateTextZIndex({ zIndex: minZIndex - 1, layerText }); }; const centerObject = (layerObject: LayerImage | LayerText): void => { @@ -257,8 +257,8 @@ export const LayersDrawerObjectsList = ({ <LayersDrawerTextItem layerText={layerText} key={layerText.id} - bringToFront={() => bringTextToFront(layerText)} - bringToBack={() => bringTextToBack(layerText)} + moveToFront={() => moveTextToFront(layerText)} + moveToBack={() => moveTextToBack(layerText)} removeObject={() => removeObject(layerText)} centerObject={() => centerObject(layerText)} editObject={() => editText()} @@ -270,8 +270,8 @@ export const LayersDrawerObjectsList = ({ <LayersDrawerImageItem layerImage={layerImage} key={layerImage.id} - bringToFront={() => bringImageToFront(layerImage)} - bringToBack={() => bringImageToBack(layerImage)} + moveToFront={() => moveImageToFront(layerImage)} + moveToBack={() => moveImageToBack(layerImage)} removeObject={() => removeObject(layerImage)} centerObject={() => centerObject(layerImage)} editObject={() => editImage()} diff --git a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index 5e378293..6d41944f 100644 --- a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -125,6 +125,7 @@ export const useOlMapAdditionalLayers = ( const vectorLayers = useMemo(() => { return layersState.map(layer => { const additionalLayer = new Layer({ + zIndex: layer.details.z, texts: layer.texts, rects: layer.rects, ovals: layer.ovals, diff --git a/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts b/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts index 7ac3b73b..a3640020 100644 --- a/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts @@ -36,6 +36,7 @@ export const useOlMapCommentsLayer = (): VectorLayer<VectorSource<Feature<Geomet return useMemo(() => { const vectorLayer = new VectorLayer({ + zIndex: Infinity, source: vectorSource, }); vectorLayer.set('type', LAYER_TYPE.COMMENTS_LAYER); diff --git a/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts b/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts index 69cec807..db3dbad4 100644 --- a/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts +++ b/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts @@ -38,6 +38,7 @@ export const useOlMapCardLayer = (): VectorLayer<VectorSource<Feature<Polygon>>> return useMemo(() => { const vectorLayer = new VectorLayer({ + zIndex: -Infinity, source: vectorSource, style: new Style({ fill: new Fill({ diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts index f7b6001c..4cd5a32b 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts @@ -77,6 +77,7 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>> return useMemo(() => { const vectorLayer = new VectorLayer({ + zIndex: Infinity, source: vectorSource, }); vectorLayer.set('type', LAYER_TYPE.PINS_LAYER); diff --git a/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts b/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts index 2811ec30..98cb6e1e 100644 --- a/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts +++ b/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts @@ -274,6 +274,7 @@ export const useOlMapProcessLayer = ({ return useMemo(() => { const vectorLayer = new VectorLayer({ + zIndex: 0, source: vectorSource, updateWhileAnimating: true, updateWhileInteracting: true, diff --git a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index bfb2668e..df526271 100644 --- a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -63,6 +63,7 @@ export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<Geome return useMemo(() => { const vectorLayer = new VectorLayer({ + zIndex: Infinity, source: vectorSource, style: new Style({ fill: new Fill({ color: LINE_COLOR }), diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts index 7b8bb971..10349c6d 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts @@ -133,6 +133,7 @@ describe('Layer', () => { pointToProjection: jest.fn(point => [point.x, point.y]), mapInstance, mapSize, + zIndex: 1, lineTypes: {}, arrowTypes: {}, }; diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts index 7f697a72..14d5f8b5 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts @@ -34,6 +34,7 @@ import LayerText from '@/components/Map/MapViewer/utils/shapes/layer/elements/La import LayerImage from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage'; export interface LayerProps { + zIndex: number; texts: { [key: string]: LayerTextModel }; rects: Array<LayerRect>; ovals: Array<LayerOval>; @@ -78,6 +79,7 @@ export default class Layer { >; constructor({ + zIndex, texts, rects, ovals, @@ -118,6 +120,7 @@ export default class Layer { this.vectorSource.addFeatures(arrowsFeatures); this.vectorLayer = new VectorLayer({ + zIndex, source: this.vectorSource, visible, updateWhileAnimating: true, diff --git a/src/models/layerSchema.ts b/src/models/layerSchema.ts index 380146a7..11fcd88d 100644 --- a/src/models/layerSchema.ts +++ b/src/models/layerSchema.ts @@ -5,4 +5,5 @@ export const layerSchema = z.object({ name: z.string(), visible: z.boolean(), locked: z.boolean(), + z: z.number(), }); diff --git a/src/redux/layers/layers.reducers.test.ts b/src/redux/layers/layers.reducers.test.ts index 7bcb10ba..d93fb4f8 100644 --- a/src/redux/layers/layers.reducers.test.ts +++ b/src/redux/layers/layers.reducers.test.ts @@ -92,7 +92,7 @@ describe('layers reducer', () => { const { type } = await store.dispatch(getLayersForModel(1)); const { data, loading, error } = store.getState().layers[1]; - expect(type).toBe('vectorMap/getLayers/fulfilled'); + expect(type).toBe('layers/getLayers/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); expect(data).toEqual({ @@ -120,7 +120,7 @@ describe('layers reducer', () => { const action = await store.dispatch(getLayersForModel(1)); const { data, loading, error } = store.getState().layers[1]; - expect(action.type).toBe('vectorMap/getLayers/rejected'); + expect(action.type).toBe('layers/getLayers/rejected'); expect(() => unwrapResult(action)).toThrow( "Failed to fetch layers: The page you're looking for doesn't exist. Please verify the URL and try again.", ); diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts index 8f016c8a..8123fa00 100644 --- a/src/redux/layers/layers.selectors.ts +++ b/src/redux/layers/layers.selectors.ts @@ -2,6 +2,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { rootSelector } from '@/redux/root/root.selectors'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; +import { LayerState } from '@/redux/layers/layers.types'; export const layersSelector = createSelector(rootSelector, state => state.layers); @@ -18,7 +19,7 @@ export const layersActiveLayersSelector = createSelector( export const layersDrawLayerSelector = createSelector( layersStateForCurrentModelSelector, - state => state?.data?.drawLayer, + state => state?.data?.drawLayer || null, ); export const layerByIdSelector = createSelector( @@ -41,40 +42,112 @@ export const layersForCurrentModelSelector = createSelector( state => state?.data?.layers || [], ); -export const highestZIndexSelector = createSelector(layersForCurrentModelSelector, layers => { - if (!layers || layers.length === 0) return 0; - - const getMaxZFromItems = <T extends { z?: number }>(items: T[] = []): number => - items.length > 0 ? Math.max(...items.map(item => item.z || 0)) : 0; - - return layers.reduce((maxZ, layer) => { - const textsMaxZ = getMaxZFromItems(Object.values(layer.texts)); - const rectsMaxZ = getMaxZFromItems(layer.rects); - const ovalsMaxZ = getMaxZFromItems(layer.ovals); - const linesMaxZ = getMaxZFromItems(layer.lines); - const imagesMaxZ = getMaxZFromItems(Object.values(layer.images)); - - const layerMaxZ = Math.max(textsMaxZ, rectsMaxZ, ovalsMaxZ, linesMaxZ, imagesMaxZ); - - return Math.max(maxZ, layerMaxZ); - }, 0); -}); +export const maxLayerZIndexAboveDiagramSelector = createSelector( + layersForCurrentModelSelector, + layers => { + if (!layers || layers.length === 0) { + return 1000; + } + let maxZIndex = -Infinity; + layers.forEach((layer: LayerState) => { + if (layer.details.z > 0 && layer.details.z > maxZIndex) { + maxZIndex = layer.details.z; + } + }); + return maxZIndex; + }, +); -export const lowestZIndexSelector = createSelector(layersForCurrentModelSelector, layers => { - if (!layers || layers.length === 0) return 0; +export const maxLayerZIndexBelowDiagramSelector = createSelector( + layersForCurrentModelSelector, + layers => { + if (!layers || layers.length === 0) { + return -1000; + } + let maxZIndex = -Infinity; + layers.forEach((layer: LayerState) => { + if (layer.details.z < 0 && layer.details.z > maxZIndex) { + maxZIndex = layer.details.z; + } + }); + return maxZIndex; + }, +); - const getMinZFromItems = <T extends { z?: number }>(items: T[] = []): number => - items.length > 0 ? Math.min(...items.map(item => item.z || 0)) : 0; +export const minLayerZIndexAboveDiagramSelector = createSelector( + layersForCurrentModelSelector, + layers => { + if (!layers || layers.length === 0) { + return 1000; + } + let minZIndex = Infinity; + layers.forEach((layer: LayerState) => { + if (layer.details.z > 0 && layer.details.z < minZIndex) { + minZIndex = layer.details.z; + } + }); + return minZIndex; + }, +); - return layers.reduce((minZ, layer) => { - const textsMinZ = getMinZFromItems(Object.values(layer.texts)); - const rectsMinZ = getMinZFromItems(layer.rects); - const ovalsMinZ = getMinZFromItems(layer.ovals); - const linesMinZ = getMinZFromItems(layer.lines); - const imagesMinZ = getMinZFromItems(Object.values(layer.images)); +export const minLayerZIndexBelowDiagramSelector = createSelector( + layersForCurrentModelSelector, + layers => { + if (!layers || layers.length === 0) { + return -1000; + } + let minZIndex = Infinity; + layers.forEach((layer: LayerState) => { + if (layer.details.z < 0 && layer.details.z < minZIndex) { + minZIndex = layer.details.z; + } + }); + return minZIndex; + }, +); - const layerMinZ = Math.min(textsMinZ, rectsMinZ, ovalsMinZ, linesMinZ, imagesMinZ); +export const maxObjectZIndexForLayerSelector = createSelector( + [layersForCurrentModelSelector, (_state, layerId: number | null): number | null => layerId], + (layers, layerId) => { + if (!layers || layers.length === 0 || !layerId) { + return 0; + } + const foundLayer = layers.find(layer => layer.details.id === layerId); + if (!foundLayer) { + return 0; + } + const getMaxZFromItems = <T extends { z?: number }>(items: T[] = []): number => + items.length > 0 ? Math.max(...items.map(item => item.z || 0)) : 0; + + const textsMaxZ = getMaxZFromItems(Object.values(foundLayer.texts)); + const rectsMaxZ = getMaxZFromItems(foundLayer.rects); + const ovalsMaxZ = getMaxZFromItems(foundLayer.ovals); + const linesMaxZ = getMaxZFromItems(foundLayer.lines); + const imagesMaxZ = getMaxZFromItems(Object.values(foundLayer.images)); + + return Math.max(textsMaxZ, rectsMaxZ, ovalsMaxZ, linesMaxZ, imagesMaxZ); + }, +); - return Math.min(minZ, layerMinZ); - }, 0); -}); +export const minObjectZIndexForLayerSelector = createSelector( + [layersForCurrentModelSelector, (_state, layerId: number | null): number | null => layerId], + (layers, layerId) => { + if (!layers || layers.length === 0 || !layerId) { + return 0; + } + const foundLayer = layers.find(layer => layer.details.id === layerId); + if (!foundLayer) { + return 0; + } + const getMinZFromItems = <T extends { z?: number }>(items: T[] = []): number => + items.length > 0 ? Math.min(...items.map(item => item.z || 0)) : 0; + + const textsMinZ = getMinZFromItems(Object.values(foundLayer.texts)); + const rectsMinZ = getMinZFromItems(foundLayer.rects); + const ovalsMinZ = getMinZFromItems(foundLayer.ovals); + const linesMinZ = getMinZFromItems(foundLayer.lines); + const imagesMinZ = getMinZFromItems(Object.values(foundLayer.images)); + + return Math.min(textsMinZ, rectsMinZ, ovalsMinZ, linesMinZ, imagesMinZ); + }, +); diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts index 77e10bdb..09028272 100644 --- a/src/redux/layers/layers.thunks.test.ts +++ b/src/redux/layers/layers.thunks.test.ts @@ -111,7 +111,13 @@ describe('layers thunks', () => { mockedAxiosClient.onPost(apiPath.storeLayer(1)).reply(HttpStatusCode.Created, layerFixture); const { payload } = await store.dispatch( - addLayerForModel({ name: 'New Layer', visible: true, locked: false, modelId: 1 }), + addLayerForModel({ + name: 'New Layer', + visible: true, + locked: false, + modelId: 1, + zIndex: 1, + }), ); expect(payload).toEqual(layerFixture); }); @@ -122,7 +128,13 @@ describe('layers thunks', () => { .reply(HttpStatusCode.Created, { invalid: 'data' }); const { payload } = await store.dispatch( - addLayerForModel({ name: 'New Layer', visible: true, locked: false, modelId: 1 }), + addLayerForModel({ + name: 'New Layer', + visible: true, + locked: false, + modelId: 1, + zIndex: 1, + }), ); expect(payload).toBeNull(); }); @@ -139,6 +151,7 @@ describe('layers thunks', () => { locked: true, modelId: 1, layerId: 2, + zIndex: 1, }), ); expect(payload).toEqual(layerFixture); @@ -156,6 +169,7 @@ describe('layers thunks', () => { locked: true, modelId: 1, layerId: 2, + zIndex: 1, }), ); expect(payload).toBeNull(); diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts index 66035fd8..bda72fce 100644 --- a/src/redux/layers/layers.thunks.ts +++ b/src/redux/layers/layers.thunks.ts @@ -32,7 +32,7 @@ export const getLayer = createAsyncThunk< Layer | null, { modelId: number; layerId: number }, ThunkConfig ->('vectorMap/getLayer', async ({ modelId, layerId }) => { +>('layers/getLayer', async ({ modelId, layerId }) => { try { const { data } = await axiosInstanceNewAPI.get<Layer>(apiPath.getLayer(modelId, layerId)); @@ -48,7 +48,7 @@ export const getLayersForModel = createAsyncThunk< LayersVisibilitiesState | undefined, number, ThunkConfig ->('vectorMap/getLayers', async (modelId: number) => { +>('layers/getLayers', async (modelId: number) => { try { const { data } = await axiosInstanceNewAPI.get<Layers>(apiPath.getLayers(modelId)); const isDataValid = validateDataUsingZodSchema(data, pageableSchema(layerSchema)); @@ -103,13 +103,14 @@ export const getLayersForModel = createAsyncThunk< }); export const addLayerForModel = createAsyncThunk<Layer | null, LayerStoreInterface, ThunkConfig>( - 'vectorMap/addLayer', - async ({ name, visible, locked, modelId }) => { + 'layers/addLayer', + async ({ name, visible, locked, modelId, zIndex }) => { try { const { data } = await axiosInstanceNewAPI.post<Layer>(apiPath.storeLayer(modelId), { name, visible, locked, + z: zIndex, }); const isDataValid = validateDataUsingZodSchema(data, layerSchema); @@ -122,13 +123,14 @@ export const addLayerForModel = createAsyncThunk<Layer | null, LayerStoreInterfa ); export const updateLayer = createAsyncThunk<Layer | null, LayerUpdateInterface, ThunkConfig>( - 'vectorMap/updateLayer', - async ({ name, visible, locked, modelId, layerId }) => { + 'layers/updateLayer', + async ({ name, visible, locked, modelId, layerId, zIndex }) => { try { const { data } = await axiosInstanceNewAPI.put<Layer>(apiPath.updateLayer(modelId, layerId), { name, visible, locked, + z: zIndex, }); const isDataValid = validateDataUsingZodSchema(data, layerSchema); @@ -144,7 +146,7 @@ export const removeLayer = createAsyncThunk< null, { modelId: number; layerId: number }, ThunkConfig ->('vectorMap/removeLayer', async ({ modelId, layerId }) => { +>('layers/removeLayer', async ({ modelId, layerId }) => { try { await axiosInstanceNewAPI.delete<void>(apiPath.removeLayer(modelId, layerId)); return null; @@ -161,7 +163,7 @@ export const getLayerImage = createAsyncThunk< imageId: number; }, ThunkConfig ->('vectorMap/getLayerImage', async ({ modelId, layerId, imageId }) => { +>('layers/getLayerImage', async ({ modelId, layerId, imageId }) => { try { const { data } = await axiosInstanceNewAPI.get<LayerImage>( apiPath.getLayerImageObject(modelId, layerId, imageId), @@ -187,7 +189,7 @@ export const addLayerImageObject = createAsyncThunk< glyph: number | null; }, ThunkConfig ->('vectorMap/addLayerImageObject', async ({ modelId, layerId, x, y, z, width, height, glyph }) => { +>('layers/addLayerImageObject', async ({ modelId, layerId, x, y, z, width, height, glyph }) => { try { const { data } = await axiosInstanceNewAPI.post<LayerImage>( apiPath.addLayerImageObject(modelId, layerId), @@ -223,7 +225,7 @@ export const updateLayerImageObject = createAsyncThunk< }, ThunkConfig >( - 'vectorMap/updateLayerImageObject', + 'layers/updateLayerImageObject', async ({ modelId, layerId, id, x, y, z, width, height, glyph }) => { try { const { data } = await axiosInstanceNewAPI.put<LayerImage>( @@ -252,7 +254,7 @@ export const removeLayerImage = createAsyncThunk< null, { modelId: number; layerId: number; imageId: number }, ThunkConfig ->('vectorMap/removeLayerImage', async ({ modelId, layerId, imageId }) => { +>('layers/removeLayerImage', async ({ modelId, layerId, imageId }) => { try { await axiosInstanceNewAPI.delete<void>( apiPath.removeLayerImageObject(modelId, layerId, imageId), @@ -273,7 +275,7 @@ export const addLayerText = createAsyncThunk< textData: LayerTextFactoryForm; }, ThunkConfig ->('vectorMap/addLayerText', async ({ modelId, layerId, z, boundingBox, textData }) => { +>('layers/addLayerText', async ({ modelId, layerId, z, boundingBox, textData }) => { try { const { data } = await axiosInstanceNewAPI.post<LayerText>( apiPath.addLayerText(modelId, layerId), @@ -299,7 +301,7 @@ export const getLayerText = createAsyncThunk< textId: number; }, ThunkConfig ->('vectorMap/getLayerText', async ({ modelId, layerId, textId }) => { +>('layers/getLayerText', async ({ modelId, layerId, textId }) => { try { const { data } = await axiosInstanceNewAPI.get<LayerText>( apiPath.getLayerText(modelId, layerId, textId), diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts index 60e42717..05b4636e 100644 --- a/src/redux/layers/layers.types.ts +++ b/src/redux/layers/layers.types.ts @@ -6,6 +6,7 @@ export interface LayerStoreInterface { visible: boolean; locked: boolean; modelId: number; + zIndex: number; } export interface LayerUpdateInterface { @@ -14,6 +15,7 @@ export interface LayerUpdateInterface { visible: boolean; locked: boolean; modelId: number; + zIndex: number; } export type LayerState = { diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx index 5745b6ec..aed961c3 100644 --- a/src/shared/Icon/Icon.component.tsx +++ b/src/shared/Icon/Icon.component.tsx @@ -31,8 +31,10 @@ import { PadlockOpenIcon } from '@/shared/Icon/Icons/PadlockOpenIcon'; import { PadlockLockedIcon } from '@/shared/Icon/Icons/PadlockLockedIcon'; import { CrossedEyeIcon } from '@/shared/Icon/Icons/CrossedEyeIcon'; import { CenterIcon } from '@/shared/Icon/Icons/CenterIcon'; -import { BringFrontIcon } from '@/shared/Icon/Icons/BringFrontIcon'; -import { BringBackIcon } from '@/shared/Icon/Icons/BringBackIcon'; +import { MoveFrontIcon } from '@/shared/Icon/Icons/MoveFrontIcon'; +import { MoveBackIcon } from '@/shared/Icon/Icons/MoveBackIcon'; +import { LayerArrowUpIcon } from '@/shared/Icon/Icons/LayerArrowUpIcon'; +import { LayerArrowDownIcon } from '@/shared/Icon/Icons/LayerArrowDownIcon'; import { LocationIcon } from './Icons/LocationIcon'; import { MaginfierZoomInIcon } from './Icons/MagnifierZoomIn'; import { MaginfierZoomOutIcon } from './Icons/MagnifierZoomOut'; @@ -87,8 +89,10 @@ const icons: Record<IconTypes, IconComponentType> = { 'padlock-open': PadlockOpenIcon, 'padlock-locked': PadlockLockedIcon, center: CenterIcon, - 'bring-front': BringFrontIcon, - 'bring-back': BringBackIcon, + 'move-front': MoveFrontIcon, + 'move-back': MoveBackIcon, + 'layer-arrow-up': LayerArrowUpIcon, + 'layer-arrow-down': LayerArrowDownIcon, } as const; export const Icon = ({ name, className = '', ...rest }: IconProps): JSX.Element => { diff --git a/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx b/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx index d7ab4e38..cf5ce5f7 100644 --- a/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx +++ b/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx @@ -13,26 +13,16 @@ export const ArrowDoubleDownIcon = ({ className }: ArrowDoubleDownIconProps): JS > <path d="M8 8L8 18M8 18L5 15M8 18L11 15" - stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> <path d="M16 8L16 18M16 18L13 15M16 18L19 15" - stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - <line - x1="4" - y1="6" - x2="20" - y2="6" - stroke="currentColor" - strokeWidth="1.5" - strokeLinecap="round" - /> + <line x1="4" y1="6" x2="20" y2="6" strokeWidth="1.5" strokeLinecap="round" /> </svg> ); diff --git a/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx b/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx index ed51a602..861773cd 100644 --- a/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx +++ b/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx @@ -13,26 +13,16 @@ export const ArrowDoubleUpIcon = ({ className }: ArrowDoubleUpIconProps): JSX.El > <path d="M8 16L8 6M8 6L5 9M8 6L11 9" - stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> <path d="M16 16L16 6M16 6L13 9M16 6L19 9" - stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - <line - x1="4" - y1="18" - x2="20" - y2="18" - stroke="currentColor" - strokeWidth="1.5" - strokeLinecap="round" - /> + <line x1="4" y1="18" x2="20" y2="18" strokeWidth="1.5" strokeLinecap="round" /> </svg> ); diff --git a/src/shared/Icon/Icons/LayerArrowDownIcon.tsx b/src/shared/Icon/Icons/LayerArrowDownIcon.tsx new file mode 100644 index 00000000..71223a31 --- /dev/null +++ b/src/shared/Icon/Icons/LayerArrowDownIcon.tsx @@ -0,0 +1,18 @@ +interface LayerArrowDownIconProps { + className?: string; +} + +export const LayerArrowDownIcon = ({ className }: LayerArrowDownIconProps): JSX.Element => ( + <svg + width="20" + height="20" + viewBox="2 3 20 17" + fill="none" + xmlns="http://www.w3.org/2000/svg" + className={className} + > + <path d="M12 21L7 16H10V10H14V16H17L12 21Z" /> + <rect x="4" y="3" width="16" height="2" /> + <rect x="4" y="7" width="16" height="2" /> + </svg> +); diff --git a/src/shared/Icon/Icons/LayerArrowUpIcon.tsx b/src/shared/Icon/Icons/LayerArrowUpIcon.tsx new file mode 100644 index 00000000..bbcf233f --- /dev/null +++ b/src/shared/Icon/Icons/LayerArrowUpIcon.tsx @@ -0,0 +1,18 @@ +interface LayerArrowUpIconProps { + className?: string; +} + +export const LayerArrowUpIcon = ({ className }: LayerArrowUpIconProps): JSX.Element => ( + <svg + width="20" + height="20" + viewBox="2 3 20 19" + fill="none" + xmlns="http://www.w3.org/2000/svg" + className={className} + > + <path d="M12 3L7 8H10V14H14V8H17L12 3Z" /> + <rect x="4" y="16" width="16" height="2" /> + <rect x="4" y="20" width="16" height="2" /> + </svg> +); diff --git a/src/shared/Icon/Icons/BringBackIcon.tsx b/src/shared/Icon/Icons/MoveBackIcon.tsx similarity index 78% rename from src/shared/Icon/Icons/BringBackIcon.tsx rename to src/shared/Icon/Icons/MoveBackIcon.tsx index d086ac9b..734bd201 100644 --- a/src/shared/Icon/Icons/BringBackIcon.tsx +++ b/src/shared/Icon/Icons/MoveBackIcon.tsx @@ -1,8 +1,8 @@ -interface BringBackIconProps { +interface MoveBackIconProps { className?: string; } -export const BringBackIcon = ({ className }: BringBackIconProps): JSX.Element => ( +export const MoveBackIcon = ({ className }: MoveBackIconProps): JSX.Element => ( <svg width="20" height="20" @@ -23,6 +23,6 @@ export const BringBackIcon = ({ className }: BringBackIconProps): JSX.Element => stroke="white" fill="none" /> - <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" fill="black" /> + <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" /> </svg> ); diff --git a/src/shared/Icon/Icons/BringFrontIcon.tsx b/src/shared/Icon/Icons/MoveFrontIcon.tsx similarity index 77% rename from src/shared/Icon/Icons/BringFrontIcon.tsx rename to src/shared/Icon/Icons/MoveFrontIcon.tsx index e5aa5017..037f5cb3 100644 --- a/src/shared/Icon/Icons/BringFrontIcon.tsx +++ b/src/shared/Icon/Icons/MoveFrontIcon.tsx @@ -1,8 +1,8 @@ -interface BringFrontIconProps { +interface MoveFrontIconProps { className?: string; } -export const BringFrontIcon = ({ className }: BringFrontIconProps): JSX.Element => ( +export const MoveFrontIcon = ({ className }: MoveFrontIconProps): JSX.Element => ( <svg width="20" height="20" @@ -11,7 +11,7 @@ export const BringFrontIcon = ({ className }: BringFrontIconProps): JSX.Element fill="none" className={className} > - <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" fill="black" /> + <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" /> <rect x="30" y="30" diff --git a/src/shared/IconButton/IconButton.component.tsx b/src/shared/IconButton/IconButton.component.tsx index 1330e460..e71c6211 100644 --- a/src/shared/IconButton/IconButton.component.tsx +++ b/src/shared/IconButton/IconButton.component.tsx @@ -29,8 +29,8 @@ export const IconButton = ({ const isStrokeIcon = [ 'plugin', - 'bring-back', - 'bring-front', + 'move-back', + 'move-front', 'center', 'eye', 'crossed-eye', @@ -38,6 +38,8 @@ export const IconButton = ({ 'padlock-locked', 'layers', 'edit', + 'arrow-double-up', + 'arrow-double-down', ].includes(icon); return ( @@ -57,6 +59,8 @@ export const IconButton = ({ isStrokeIcon ? 'stroke-font-400 group-hover:stroke-primary-500 group-active:stroke-primary-500' : 'fill-font-400 group-hover:fill-primary-500 group-active:fill-primary-500', + ['move-back', 'move-front'].includes(icon) && + 'fill-font-400 stroke-font-400 group-hover:fill-primary-500 group-hover:stroke-primary-500 group-active:fill-primary-500 group-active:stroke-primary-500', isActive && getActiveFillOrStrokeColor(icon), classNameIcon, )} diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts index 0d74dd78..0083a622 100644 --- a/src/types/iconTypes.ts +++ b/src/types/iconTypes.ts @@ -38,7 +38,9 @@ export type IconTypes = | 'padlock-open' | 'padlock-locked' | 'center' - | 'bring-front' - | 'bring-back'; + | 'move-front' + | 'move-back' + | 'layer-arrow-up' + | 'layer-arrow-down'; export type IconComponentType = ({ className }: { className: string }) => JSX.Element; -- GitLab From f6e504ece2951e59900cec58b70bec18b21b4bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Tue, 18 Feb 2025 10:19:22 +0100 Subject: [PATCH 4/8] Revert "feat(layers): add layers zIndex editing" This reverts commit 7f8d0b665cbd00568e46ba57597c5d22ab28192b. --- .../LayerFactoryModal.component.tsx | 4 - ...LayerImageObjectFactoryModal.component.tsx | 9 +- .../LayerTextFactoryModal.component.tsx | 9 +- .../LayerDrawerTextItem.component.tsx | 12 +- .../LayersDrawer/LayersDrawer.component.tsx | 46 ++---- .../LayersDrawerImageItem.component.tsx | 12 +- .../LayersDrawerLayer.component.tsx | 107 +++----------- .../LayersDrawerLayerActions.component.tsx | 39 ----- .../LayersDrawerObjectActions.component.tsx | 20 +-- .../LayersDrawerObjectsList.component.tsx | 32 ++-- .../useOlMapAdditionalLayers.ts | 1 - .../commentsLayer/useOlMapCommentsLayer.ts | 1 - .../config/mapCardLayer/useOlMapCardLayer.ts | 1 - .../config/pinsLayer/useOlMapPinsLayer.ts | 1 - .../processLayer/useOlMapProcessLayer.ts | 1 - .../reactionsLayer/useOlMapReactionsLayer.ts | 1 - .../utils/shapes/layer/Layer.test.ts | 1 - .../Map/MapViewer/utils/shapes/layer/Layer.ts | 3 - src/models/layerSchema.ts | 1 - src/redux/layers/layers.reducers.test.ts | 4 +- src/redux/layers/layers.selectors.ts | 139 +++++------------- src/redux/layers/layers.thunks.test.ts | 18 +-- src/redux/layers/layers.thunks.ts | 28 ++-- src/redux/layers/layers.types.ts | 2 - src/shared/Icon/Icon.component.tsx | 12 +- src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx | 12 +- src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx | 12 +- .../{MoveBackIcon.tsx => BringBackIcon.tsx} | 6 +- .../{MoveFrontIcon.tsx => BringFrontIcon.tsx} | 6 +- src/shared/Icon/Icons/LayerArrowDownIcon.tsx | 18 --- src/shared/Icon/Icons/LayerArrowUpIcon.tsx | 18 --- .../IconButton/IconButton.component.tsx | 8 +- src/types/iconTypes.ts | 6 +- 33 files changed, 160 insertions(+), 430 deletions(-) rename src/shared/Icon/Icons/{MoveBackIcon.tsx => BringBackIcon.tsx} (78%) rename src/shared/Icon/Icons/{MoveFrontIcon.tsx => BringFrontIcon.tsx} (77%) delete mode 100644 src/shared/Icon/Icons/LayerArrowDownIcon.tsx delete mode 100644 src/shared/Icon/Icons/LayerArrowUpIcon.tsx diff --git a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx index b806d2b6..8d925951 100644 --- a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx @@ -19,13 +19,11 @@ import { SerializedError } from '@reduxjs/toolkit'; import { layerFactoryStateSelector } from '@/redux/modal/modal.selector'; import './LayerFactoryModal.styles.css'; import { LoadingIndicator } from '@/shared/LoadingIndicator'; -import { maxLayerZIndexAboveDiagramSelector } from '@/redux/layers/layers.selectors'; export const LayerFactoryModal: React.FC = () => { const dispatch = useAppDispatch(); const currentModelId = useAppSelector(currentModelIdSelector); const layerFactoryState = useAppSelector(layerFactoryStateSelector); - const maxLayerZIndexAboveDiagram = useAppSelector(maxLayerZIndexAboveDiagramSelector); const [loaded, setLoaded] = useState<boolean>(false); const [data, setData] = useState<LayerStoreInterface>({ @@ -33,7 +31,6 @@ export const LayerFactoryModal: React.FC = () => { visible: false, locked: false, modelId: currentModelId, - zIndex: maxLayerZIndexAboveDiagram, }); const fetchData = useMemo(() => { @@ -45,7 +42,6 @@ export const LayerFactoryModal: React.FC = () => { visible: layer.visible, locked: layer.locked, modelId: currentModelId, - zIndex: layer.z, }); } setLoaded(true); diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx index 4d1513e9..1efb7d88 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx @@ -4,10 +4,7 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { layerImageObjectFactoryStateSelector } from '@/redux/modal/modal.selector'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; -import { - layersDrawLayerSelector, - maxObjectZIndexForLayerSelector, -} from '@/redux/layers/layers.selectors'; +import { highestZIndexSelector, layersDrawLayerSelector } from '@/redux/layers/layers.selectors'; import { addLayerImageObject } from '@/redux/layers/layers.thunks'; import { addGlyph } from '@/redux/glyphs/glyphs.thunks'; import { SerializedError } from '@reduxjs/toolkit'; @@ -25,7 +22,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => { const drawLayer = useAppSelector(layersDrawLayerSelector); const layerImageObjectFactoryState = useAppSelector(layerImageObjectFactoryStateSelector); const dispatch = useAppDispatch(); - const maxZIndex = useAppSelector(state => maxObjectZIndexForLayerSelector(state, drawLayer)); + const highestZIndex = useAppSelector(highestZIndexSelector); const { mapInstance } = useMapInstance(); const [selectedGlyph, setSelectedGlyph] = useState<number | null>(null); @@ -52,7 +49,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => { layerId: drawLayer, x: layerImageObjectFactoryState.x, y: layerImageObjectFactoryState.y, - z: maxZIndex + 1, + z: highestZIndex + 1, width: layerImageObjectFactoryState.width, height: layerImageObjectFactoryState.height, glyph: glyphId, diff --git a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx index bfb80aff..e1ccf612 100644 --- a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx @@ -13,10 +13,7 @@ import { LayerTextFactoryForm } from '@/components/FunctionalArea/Modal/LayerTex import { Color } from '@/types/models'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { layerTextFactoryStateSelector } from '@/redux/modal/modal.selector'; -import { - layersDrawLayerSelector, - maxObjectZIndexForLayerSelector, -} from '@/redux/layers/layers.selectors'; +import { highestZIndexSelector, layersDrawLayerSelector } from '@/redux/layers/layers.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { showToast } from '@/utils/showToast'; @@ -34,7 +31,7 @@ export const LayerTextFactoryModal: React.FC = () => { const currentModelId = useAppSelector(currentModelIdSelector); const layerTextFactoryState = useAppSelector(layerTextFactoryStateSelector); const dispatch = useAppDispatch(); - const maxZIndex = useAppSelector(state => maxObjectZIndexForLayerSelector(state, drawLayer)); + const highestZIndex = useAppSelector(highestZIndexSelector); const { mapInstance } = useMapInstance(); const [isSending, setIsSending] = useState<boolean>(false); @@ -58,7 +55,7 @@ export const LayerTextFactoryModal: React.FC = () => { layerId: drawLayer, boundingBox: layerTextFactoryState, textData: data, - z: maxZIndex + 1, + z: highestZIndex + 1, }), ).unwrap(); if (!textData) { diff --git a/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx index a9c3d3b3..6f57c762 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx @@ -10,8 +10,8 @@ import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors' interface LayersDrawerTextItemProps { layerText: LayerText; - moveToFront: () => void; - moveToBack: () => void; + bringToFront: () => void; + bringToBack: () => void; removeObject: () => void; centerObject: () => void; editObject: () => void; @@ -21,8 +21,8 @@ interface LayersDrawerTextItemProps { export const LayersDrawerTextItem = ({ layerText, - moveToFront, - moveToBack, + bringToFront, + bringToBack, removeObject, centerObject, editObject, @@ -68,8 +68,8 @@ export const LayersDrawerTextItem = ({ </div> {showActions && ( <LayersDrawerObjectActions - moveToFront={moveToFront} - moveToBack={moveToBack} + bringToFront={bringToFront} + bringToBack={bringToBack} removeObject={removeObject} centerObject={centerObject} editObject={editObject} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx index 33c259f3..12bef081 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx @@ -4,7 +4,7 @@ import { DrawerHeading } from '@/shared/DrawerHeading'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { layersForCurrentModelSelector } from '@/redux/layers/layers.selectors'; import { Button } from '@/shared/Button'; -import { JSX, useEffect, useMemo, useRef } from 'react'; +import { JSX, useEffect, useRef } from 'react'; import { openLayerFactoryModal } from '@/redux/modal/modal.slice'; import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; import { LayersDrawerLayer } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component'; @@ -21,19 +21,6 @@ export const LayersDrawer = (): JSX.Element => { dispatch(openLayerFactoryModal()); }; - const sortedLayers = useMemo(() => { - return [...layersForCurrentModel].sort((layerA, layerB) => layerB.details.z - layerA.details.z); - }, [layersForCurrentModel]); - - const negativeZLayers = useMemo( - () => sortedLayers.filter(layer => layer.details.z < 0), - [sortedLayers], - ); - const positiveZLayers = useMemo( - () => sortedLayers.filter(layer => layer.details.z >= 0), - [sortedLayers], - ); - useEffect(() => { if (!mapEditToolsLayerImageObject || !layersDrawerRef.current) { return; @@ -74,33 +61,20 @@ export const LayersDrawer = (): JSX.Element => { ref={layersDrawerRef} > {hasPrivilegeToWriteProject && ( - <div className="flex justify-start py-2"> + <div className="flex justify-start pt-2"> <Button icon="plus" isIcon isFrontIcon onClick={addNewLayer}> Add layer </Button> </div> )} - <div className="flex flex-col gap-2"> - {Boolean(positiveZLayers.length) && ( - <span className="border-b-2 border-dashed border-b-gray-400 text-center text-lg font-semibold"> - Layers above the diagram - </span> - )} - <div className="flex flex-col gap-5"> - {positiveZLayers.map(layer => ( - <LayersDrawerLayer key={layer.details.id} layerDetails={layer.details} /> - ))} - </div> - {Boolean(negativeZLayers.length) && ( - <span className="border-b-2 border-dashed border-b-gray-400 text-center text-lg font-semibold"> - Layers below the diagram - </span> - )} - <div className="flex flex-col gap-5"> - {negativeZLayers.map(layer => ( - <LayersDrawerLayer key={layer.details.id} layerDetails={layer.details} /> - ))} - </div> + <div className="flex flex-col gap-4"> + {layersForCurrentModel.map(layer => ( + <LayersDrawerLayer + key={layer.details.id} + layerId={layer.details.id} + layerName={layer.details.name} + /> + ))} </div> </div> </div> diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx index e160b807..0622f141 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx @@ -11,8 +11,8 @@ import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors' interface LayersDrawerImageItemProps { layerImage: LayerImage; - moveToFront: () => void; - moveToBack: () => void; + bringToFront: () => void; + bringToBack: () => void; removeObject: () => void; centerObject: () => void; editObject: () => void; @@ -22,8 +22,8 @@ interface LayersDrawerImageItemProps { export const LayersDrawerImageItem = ({ layerImage, - moveToFront, - moveToBack, + bringToFront, + bringToBack, removeObject, centerObject, editObject, @@ -72,8 +72,8 @@ export const LayersDrawerImageItem = ({ </div> {showActions && ( <LayersDrawerObjectActions - moveToFront={moveToFront} - moveToBack={moveToBack} + bringToFront={bringToFront} + bringToBack={bringToBack} removeObject={removeObject} centerObject={centerObject} editObject={editObject} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx index 8b0f9b01..49b8536f 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx @@ -1,19 +1,14 @@ -/* eslint-disable no-magic-numbers */ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { layersActiveLayersSelector, layersVisibilityForCurrentModelSelector, - maxLayerZIndexAboveDiagramSelector, - maxLayerZIndexBelowDiagramSelector, - minLayerZIndexAboveDiagramSelector, - minLayerZIndexBelowDiagramSelector, } from '@/redux/layers/layers.selectors'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { openLayerFactoryModal } from '@/redux/modal/modal.slice'; import QuestionModal from '@/components/FunctionalArea/Modal/QuestionModal/QustionModal.component'; import { useState, JSX, useMemo } from 'react'; -import { getLayersForModel, removeLayer, updateLayer } from '@/redux/layers/layers.thunks'; +import { getLayersForModel, removeLayer } from '@/redux/layers/layers.thunks'; import { showToast } from '@/utils/showToast'; import { SerializedError } from '@reduxjs/toolkit'; import { LayersDrawerLayerActions } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component'; @@ -26,42 +21,38 @@ import { import { LayersDrawerObjectsList } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component'; import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice'; import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; -import { Layer } from '@/types/models'; interface LayersDrawerLayerProps { - layerDetails: Layer; + layerId: number; + layerName: string; } -export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX.Element => { +export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps): JSX.Element => { const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector); const activeLayers = useAppSelector(layersActiveLayersSelector); const currentModelId = useAppSelector(currentModelIdSelector); - const maxLayerZIndexAboveDiagram = useAppSelector(maxLayerZIndexAboveDiagramSelector); - const maxLayerZIndexBelowDiagram = useAppSelector(maxLayerZIndexBelowDiagramSelector); - const minLayerZIndexAboveDiagram = useAppSelector(minLayerZIndexAboveDiagramSelector); - const minLayerZIndexBelowDiagram = useAppSelector(minLayerZIndexBelowDiagramSelector); const dispatch = useAppDispatch(); const [isModalOpen, setIsModalOpen] = useState(false); const isLayerVisible = useMemo(() => { - return layersVisibilityForCurrentModel[layerDetails.id]; - }, [layerDetails.id, layersVisibilityForCurrentModel]); + return layersVisibilityForCurrentModel[layerId]; + }, [layerId, layersVisibilityForCurrentModel]); const isLayerActive = useMemo(() => { - return activeLayers.includes(layerDetails.id); - }, [activeLayers, layerDetails.id]); + return activeLayers.includes(layerId); + }, [activeLayers, layerId]); const editLayer = (): void => { - dispatch(openLayerFactoryModal(layerDetails.id)); + dispatch(openLayerFactoryModal(layerId)); }; const addImage = (): void => { - dispatch(setDrawLayer({ modelId: currentModelId, layerId: layerDetails.id })); + dispatch(setDrawLayer({ modelId: currentModelId, layerId })); dispatch(mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.DRAW_IMAGE)); }; const addText = (): void => { - dispatch(setDrawLayer({ modelId: currentModelId, layerId: layerDetails.id })); + dispatch(setDrawLayer({ modelId: currentModelId, layerId })); dispatch(mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.ADD_TEXT)); }; @@ -70,11 +61,11 @@ export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX }; const confirmRemove = async (): Promise<void> => { - if (!layerDetails.id) { + if (!layerId) { return; } try { - await dispatch(removeLayer({ modelId: currentModelId, layerId: layerDetails.id })).unwrap(); + await dispatch(removeLayer({ modelId: currentModelId, layerId })).unwrap(); showToast({ type: 'success', message: 'The layer has been successfully removed', @@ -96,61 +87,12 @@ export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX const toggleActiveLayer = (value: boolean): void => { if (value) { - dispatch(setLayerToActive({ modelId: currentModelId, layerId: layerDetails.id })); + dispatch(setLayerToActive({ modelId: currentModelId, layerId })); } else { - dispatch(setLayerToInactive({ modelId: currentModelId, layerId: layerDetails.id })); + dispatch(setLayerToInactive({ modelId: currentModelId, layerId })); } }; - const updateLayerZIndex = async (zIndex: number): Promise<void> => { - try { - dispatch( - updateLayer({ - name: layerDetails.name, - visible: layerDetails.visible, - locked: layerDetails.locked, - modelId: currentModelId, - layerId: layerDetails.id, - zIndex, - }), - ); - } catch (error) { - const typedError = error as SerializedError; - showToast({ - type: 'error', - message: typedError.message || 'An error occurred while updating the layer', - }); - } - }; - - const moveToFront = (): void => { - if (layerDetails.z > 0) { - updateLayerZIndex(maxLayerZIndexAboveDiagram); - } else if (layerDetails.z < 0) { - const zIndex = Math.min(maxLayerZIndexBelowDiagram, -1); - updateLayerZIndex(zIndex); - } - }; - - const moveToBack = (): void => { - if (layerDetails.z > 0) { - const zIndex = Math.max(minLayerZIndexAboveDiagram, 1); - updateLayerZIndex(zIndex); - } else if (layerDetails.z < 0) { - updateLayerZIndex(minLayerZIndexBelowDiagram); - } - }; - - const moveAboveDiagram = (): void => { - const zIndex = Math.max(minLayerZIndexAboveDiagram - 1, 1); - updateLayerZIndex(zIndex); - }; - - const moveBelowDiagram = (): void => { - const zIndex = Math.min(minLayerZIndexBelowDiagram + 1, -1); - updateLayerZIndex(zIndex); - }; - return ( <div> <QuestionModal @@ -159,13 +101,9 @@ export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX onConfirm={confirmRemove} question="Are you sure you want to remove the layer?" /> - <div className="flex items-center justify-between pb-3"> - <span - className={`font-semibold ${ - isLayerVisible ? 'opacity-100' : 'opacity-40' - } min-w-0 flex-1 truncate `} - > - {layerDetails.name} + <div className="flex items-center justify-between py-3"> + <span className={`font-semibold ${isLayerVisible ? 'opacity-100' : 'opacity-40'}`}> + {layerName} </span> <LayersDrawerLayerActions toggleVisibility={() => @@ -173,7 +111,7 @@ export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX setLayerVisibility({ modelId: currentModelId, visible: !isLayerVisible, - layerId: layerDetails.id, + layerId, }), ) } @@ -182,17 +120,12 @@ export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX removeLayer={onRemoveLayer} addImage={addImage} addText={addText} - moveToFront={moveToFront} - moveToBack={moveToBack} - moveAboveDiagram={moveAboveDiagram} - moveBelowDiagram={moveBelowDiagram} - zIndex={layerDetails.z} isVisible={isLayerVisible} isActive={isLayerActive} /> </div> <LayersDrawerObjectsList - layerId={layerDetails.id} + layerId={layerId} isLayerVisible={isLayerVisible} isLayerActive={isLayerActive} /> diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx index 81435100..18627b13 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-magic-numbers */ import { IconButton } from '@/shared/IconButton'; import { JSX } from 'react'; import { LayerDrawerLayerContextMenu } from '@/components/Map/Drawer/LayersDrawer/LayerDrawerLayerContextMenu.component'; @@ -10,15 +9,10 @@ type LayersDrawerLayerActionsProps = { removeLayer: () => void; isVisible: boolean; isActive: boolean; - zIndex: number; toggleVisibility: () => void; toggleActiveLayer: (value: boolean) => void; addImage: () => void; addText: () => void; - moveToFront: () => void; - moveToBack: () => void; - moveAboveDiagram: () => void; - moveBelowDiagram: () => void; }; export const LayersDrawerLayerActions = ({ @@ -26,15 +20,10 @@ export const LayersDrawerLayerActions = ({ removeLayer, isVisible, isActive, - zIndex, toggleVisibility, toggleActiveLayer, addImage, addText, - moveToFront, - moveToBack, - moveAboveDiagram, - moveBelowDiagram, }: LayersDrawerLayerActionsProps): JSX.Element => { const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); @@ -54,34 +43,6 @@ export const LayersDrawerLayerActions = ({ className="h-auto w-auto bg-transparent p-0" onClick={() => toggleActiveLayer(!isActive)} /> - <IconButton - title="Move to front" - icon="move-front" - className="h-auto w-auto bg-transparent p-0" - onClick={moveToFront} - /> - <IconButton - title="Move to back" - icon="move-back" - className="h-auto w-auto bg-transparent p-0" - onClick={moveToBack} - /> - {zIndex < 0 && ( - <IconButton - title="Move above the diagram" - icon="layer-arrow-up" - className="h-auto w-auto bg-transparent p-0" - onClick={moveAboveDiagram} - /> - )} - {zIndex > 0 && ( - <IconButton - title="Move below the diagram" - icon="layer-arrow-down" - className="h-auto w-auto bg-transparent p-0" - onClick={moveBelowDiagram} - /> - )} <LayerDrawerLayerContextMenu removeLayer={removeLayer} editLayer={editLayer} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx index a97189ea..4cf6871e 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx @@ -2,16 +2,16 @@ import { JSX } from 'react'; import { IconButton } from '@/shared/IconButton'; interface LayersDrawerObjectActionsProps { - moveToFront: () => void; - moveToBack: () => void; + bringToFront: () => void; + bringToBack: () => void; removeObject: () => void; centerObject: () => void; editObject: () => void; } export const LayersDrawerObjectActions = ({ - moveToFront, - moveToBack, + bringToFront, + bringToBack, removeObject, centerObject, editObject, @@ -31,16 +31,16 @@ export const LayersDrawerObjectActions = ({ onClick={editObject} /> <IconButton - icon="move-front" + icon="bring-front" className="h-auto w-auto bg-transparent p-0" - title="Move to front" - onClick={moveToFront} + title="Bring to front" + onClick={bringToFront} /> <IconButton - icon="move-back" + icon="bring-back" className="h-auto w-auto bg-transparent p-0" - title="Move to back" - onClick={moveToBack} + title="Bring to back" + onClick={bringToBack} /> <IconButton icon="trash" diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx index 9ec19fe4..70a6acf5 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx @@ -1,9 +1,9 @@ /* eslint-disable no-magic-numbers */ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { + highestZIndexSelector, layerByIdSelector, - maxObjectZIndexForLayerSelector, - minObjectZIndexForLayerSelector, + lowestZIndexSelector, } from '@/redux/layers/layers.selectors'; import { JSX, useState } from 'react'; import { LayersDrawerImageItem } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component'; @@ -64,8 +64,8 @@ export const LayersDrawerObjectsList = ({ isLayerActive, }: LayersDrawerObjectsListProps): JSX.Element | null => { const currentModelId = useAppSelector(mapModelIdSelector); - const maxZIndex = useAppSelector(state => maxObjectZIndexForLayerSelector(state, layerId)); - const minZIndex = useAppSelector(state => minObjectZIndexForLayerSelector(state, layerId)); + const highestZIndex = useAppSelector(highestZIndexSelector); + const lowestZIndex = useAppSelector(lowestZIndexSelector); const layer = useAppSelector(state => layerByIdSelector(state, layerId)); const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerObjectSelector); const [removeModalState, setRemoveModalState] = useState<undefined | 'text' | 'image'>(undefined); @@ -174,12 +174,12 @@ export const LayersDrawerObjectsList = ({ } }; - const moveImageToFront = async (layerImage: LayerImage): Promise<void> => { - await updateImageZIndex({ zIndex: maxZIndex + 1, layerImage }); + const bringImageToFront = async (layerImage: LayerImage): Promise<void> => { + await updateImageZIndex({ zIndex: highestZIndex + 1, layerImage }); }; - const moveImageToBack = async (layerImage: LayerImage): Promise<void> => { - await updateImageZIndex({ zIndex: minZIndex - 1, layerImage }); + const bringImageToBack = async (layerImage: LayerImage): Promise<void> => { + await updateImageZIndex({ zIndex: lowestZIndex - 1, layerImage }); }; const updateTextZIndex = async ({ @@ -210,12 +210,12 @@ export const LayersDrawerObjectsList = ({ } }; - const moveTextToFront = async (layerText: LayerText): Promise<void> => { - await updateTextZIndex({ zIndex: maxZIndex + 1, layerText }); + const bringTextToFront = async (layerText: LayerText): Promise<void> => { + await updateTextZIndex({ zIndex: highestZIndex + 1, layerText }); }; - const moveTextToBack = async (layerText: LayerText): Promise<void> => { - await updateTextZIndex({ zIndex: minZIndex - 1, layerText }); + const bringTextToBack = async (layerText: LayerText): Promise<void> => { + await updateTextZIndex({ zIndex: lowestZIndex - 1, layerText }); }; const centerObject = (layerObject: LayerImage | LayerText): void => { @@ -257,8 +257,8 @@ export const LayersDrawerObjectsList = ({ <LayersDrawerTextItem layerText={layerText} key={layerText.id} - moveToFront={() => moveTextToFront(layerText)} - moveToBack={() => moveTextToBack(layerText)} + bringToFront={() => bringTextToFront(layerText)} + bringToBack={() => bringTextToBack(layerText)} removeObject={() => removeObject(layerText)} centerObject={() => centerObject(layerText)} editObject={() => editText()} @@ -270,8 +270,8 @@ export const LayersDrawerObjectsList = ({ <LayersDrawerImageItem layerImage={layerImage} key={layerImage.id} - moveToFront={() => moveImageToFront(layerImage)} - moveToBack={() => moveImageToBack(layerImage)} + bringToFront={() => bringImageToFront(layerImage)} + bringToBack={() => bringImageToBack(layerImage)} removeObject={() => removeObject(layerImage)} centerObject={() => centerObject(layerImage)} editObject={() => editImage()} diff --git a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index 6d41944f..5e378293 100644 --- a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -125,7 +125,6 @@ export const useOlMapAdditionalLayers = ( const vectorLayers = useMemo(() => { return layersState.map(layer => { const additionalLayer = new Layer({ - zIndex: layer.details.z, texts: layer.texts, rects: layer.rects, ovals: layer.ovals, diff --git a/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts b/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts index a3640020..7ac3b73b 100644 --- a/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts @@ -36,7 +36,6 @@ export const useOlMapCommentsLayer = (): VectorLayer<VectorSource<Feature<Geomet return useMemo(() => { const vectorLayer = new VectorLayer({ - zIndex: Infinity, source: vectorSource, }); vectorLayer.set('type', LAYER_TYPE.COMMENTS_LAYER); diff --git a/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts b/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts index db3dbad4..69cec807 100644 --- a/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts +++ b/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts @@ -38,7 +38,6 @@ export const useOlMapCardLayer = (): VectorLayer<VectorSource<Feature<Polygon>>> return useMemo(() => { const vectorLayer = new VectorLayer({ - zIndex: -Infinity, source: vectorSource, style: new Style({ fill: new Fill({ diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts index 4cd5a32b..f7b6001c 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts @@ -77,7 +77,6 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>> return useMemo(() => { const vectorLayer = new VectorLayer({ - zIndex: Infinity, source: vectorSource, }); vectorLayer.set('type', LAYER_TYPE.PINS_LAYER); diff --git a/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts b/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts index 98cb6e1e..2811ec30 100644 --- a/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts +++ b/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts @@ -274,7 +274,6 @@ export const useOlMapProcessLayer = ({ return useMemo(() => { const vectorLayer = new VectorLayer({ - zIndex: 0, source: vectorSource, updateWhileAnimating: true, updateWhileInteracting: true, diff --git a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index df526271..bfb2668e 100644 --- a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -63,7 +63,6 @@ export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<Geome return useMemo(() => { const vectorLayer = new VectorLayer({ - zIndex: Infinity, source: vectorSource, style: new Style({ fill: new Fill({ color: LINE_COLOR }), diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts index 10349c6d..7b8bb971 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts @@ -133,7 +133,6 @@ describe('Layer', () => { pointToProjection: jest.fn(point => [point.x, point.y]), mapInstance, mapSize, - zIndex: 1, lineTypes: {}, arrowTypes: {}, }; diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts index 14d5f8b5..7f697a72 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts @@ -34,7 +34,6 @@ import LayerText from '@/components/Map/MapViewer/utils/shapes/layer/elements/La import LayerImage from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage'; export interface LayerProps { - zIndex: number; texts: { [key: string]: LayerTextModel }; rects: Array<LayerRect>; ovals: Array<LayerOval>; @@ -79,7 +78,6 @@ export default class Layer { >; constructor({ - zIndex, texts, rects, ovals, @@ -120,7 +118,6 @@ export default class Layer { this.vectorSource.addFeatures(arrowsFeatures); this.vectorLayer = new VectorLayer({ - zIndex, source: this.vectorSource, visible, updateWhileAnimating: true, diff --git a/src/models/layerSchema.ts b/src/models/layerSchema.ts index 11fcd88d..380146a7 100644 --- a/src/models/layerSchema.ts +++ b/src/models/layerSchema.ts @@ -5,5 +5,4 @@ export const layerSchema = z.object({ name: z.string(), visible: z.boolean(), locked: z.boolean(), - z: z.number(), }); diff --git a/src/redux/layers/layers.reducers.test.ts b/src/redux/layers/layers.reducers.test.ts index d93fb4f8..7bcb10ba 100644 --- a/src/redux/layers/layers.reducers.test.ts +++ b/src/redux/layers/layers.reducers.test.ts @@ -92,7 +92,7 @@ describe('layers reducer', () => { const { type } = await store.dispatch(getLayersForModel(1)); const { data, loading, error } = store.getState().layers[1]; - expect(type).toBe('layers/getLayers/fulfilled'); + expect(type).toBe('vectorMap/getLayers/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); expect(data).toEqual({ @@ -120,7 +120,7 @@ describe('layers reducer', () => { const action = await store.dispatch(getLayersForModel(1)); const { data, loading, error } = store.getState().layers[1]; - expect(action.type).toBe('layers/getLayers/rejected'); + expect(action.type).toBe('vectorMap/getLayers/rejected'); expect(() => unwrapResult(action)).toThrow( "Failed to fetch layers: The page you're looking for doesn't exist. Please verify the URL and try again.", ); diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts index 8123fa00..8f016c8a 100644 --- a/src/redux/layers/layers.selectors.ts +++ b/src/redux/layers/layers.selectors.ts @@ -2,7 +2,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { rootSelector } from '@/redux/root/root.selectors'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; -import { LayerState } from '@/redux/layers/layers.types'; export const layersSelector = createSelector(rootSelector, state => state.layers); @@ -19,7 +18,7 @@ export const layersActiveLayersSelector = createSelector( export const layersDrawLayerSelector = createSelector( layersStateForCurrentModelSelector, - state => state?.data?.drawLayer || null, + state => state?.data?.drawLayer, ); export const layerByIdSelector = createSelector( @@ -42,112 +41,40 @@ export const layersForCurrentModelSelector = createSelector( state => state?.data?.layers || [], ); -export const maxLayerZIndexAboveDiagramSelector = createSelector( - layersForCurrentModelSelector, - layers => { - if (!layers || layers.length === 0) { - return 1000; - } - let maxZIndex = -Infinity; - layers.forEach((layer: LayerState) => { - if (layer.details.z > 0 && layer.details.z > maxZIndex) { - maxZIndex = layer.details.z; - } - }); - return maxZIndex; - }, -); +export const highestZIndexSelector = createSelector(layersForCurrentModelSelector, layers => { + if (!layers || layers.length === 0) return 0; -export const maxLayerZIndexBelowDiagramSelector = createSelector( - layersForCurrentModelSelector, - layers => { - if (!layers || layers.length === 0) { - return -1000; - } - let maxZIndex = -Infinity; - layers.forEach((layer: LayerState) => { - if (layer.details.z < 0 && layer.details.z > maxZIndex) { - maxZIndex = layer.details.z; - } - }); - return maxZIndex; - }, -); + const getMaxZFromItems = <T extends { z?: number }>(items: T[] = []): number => + items.length > 0 ? Math.max(...items.map(item => item.z || 0)) : 0; -export const minLayerZIndexAboveDiagramSelector = createSelector( - layersForCurrentModelSelector, - layers => { - if (!layers || layers.length === 0) { - return 1000; - } - let minZIndex = Infinity; - layers.forEach((layer: LayerState) => { - if (layer.details.z > 0 && layer.details.z < minZIndex) { - minZIndex = layer.details.z; - } - }); - return minZIndex; - }, -); + return layers.reduce((maxZ, layer) => { + const textsMaxZ = getMaxZFromItems(Object.values(layer.texts)); + const rectsMaxZ = getMaxZFromItems(layer.rects); + const ovalsMaxZ = getMaxZFromItems(layer.ovals); + const linesMaxZ = getMaxZFromItems(layer.lines); + const imagesMaxZ = getMaxZFromItems(Object.values(layer.images)); -export const minLayerZIndexBelowDiagramSelector = createSelector( - layersForCurrentModelSelector, - layers => { - if (!layers || layers.length === 0) { - return -1000; - } - let minZIndex = Infinity; - layers.forEach((layer: LayerState) => { - if (layer.details.z < 0 && layer.details.z < minZIndex) { - minZIndex = layer.details.z; - } - }); - return minZIndex; - }, -); + const layerMaxZ = Math.max(textsMaxZ, rectsMaxZ, ovalsMaxZ, linesMaxZ, imagesMaxZ); -export const maxObjectZIndexForLayerSelector = createSelector( - [layersForCurrentModelSelector, (_state, layerId: number | null): number | null => layerId], - (layers, layerId) => { - if (!layers || layers.length === 0 || !layerId) { - return 0; - } - const foundLayer = layers.find(layer => layer.details.id === layerId); - if (!foundLayer) { - return 0; - } - const getMaxZFromItems = <T extends { z?: number }>(items: T[] = []): number => - items.length > 0 ? Math.max(...items.map(item => item.z || 0)) : 0; - - const textsMaxZ = getMaxZFromItems(Object.values(foundLayer.texts)); - const rectsMaxZ = getMaxZFromItems(foundLayer.rects); - const ovalsMaxZ = getMaxZFromItems(foundLayer.ovals); - const linesMaxZ = getMaxZFromItems(foundLayer.lines); - const imagesMaxZ = getMaxZFromItems(Object.values(foundLayer.images)); - - return Math.max(textsMaxZ, rectsMaxZ, ovalsMaxZ, linesMaxZ, imagesMaxZ); - }, -); + return Math.max(maxZ, layerMaxZ); + }, 0); +}); -export const minObjectZIndexForLayerSelector = createSelector( - [layersForCurrentModelSelector, (_state, layerId: number | null): number | null => layerId], - (layers, layerId) => { - if (!layers || layers.length === 0 || !layerId) { - return 0; - } - const foundLayer = layers.find(layer => layer.details.id === layerId); - if (!foundLayer) { - return 0; - } - const getMinZFromItems = <T extends { z?: number }>(items: T[] = []): number => - items.length > 0 ? Math.min(...items.map(item => item.z || 0)) : 0; - - const textsMinZ = getMinZFromItems(Object.values(foundLayer.texts)); - const rectsMinZ = getMinZFromItems(foundLayer.rects); - const ovalsMinZ = getMinZFromItems(foundLayer.ovals); - const linesMinZ = getMinZFromItems(foundLayer.lines); - const imagesMinZ = getMinZFromItems(Object.values(foundLayer.images)); - - return Math.min(textsMinZ, rectsMinZ, ovalsMinZ, linesMinZ, imagesMinZ); - }, -); +export const lowestZIndexSelector = createSelector(layersForCurrentModelSelector, layers => { + if (!layers || layers.length === 0) return 0; + + const getMinZFromItems = <T extends { z?: number }>(items: T[] = []): number => + items.length > 0 ? Math.min(...items.map(item => item.z || 0)) : 0; + + return layers.reduce((minZ, layer) => { + const textsMinZ = getMinZFromItems(Object.values(layer.texts)); + const rectsMinZ = getMinZFromItems(layer.rects); + const ovalsMinZ = getMinZFromItems(layer.ovals); + const linesMinZ = getMinZFromItems(layer.lines); + const imagesMinZ = getMinZFromItems(Object.values(layer.images)); + + const layerMinZ = Math.min(textsMinZ, rectsMinZ, ovalsMinZ, linesMinZ, imagesMinZ); + + return Math.min(minZ, layerMinZ); + }, 0); +}); diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts index 09028272..77e10bdb 100644 --- a/src/redux/layers/layers.thunks.test.ts +++ b/src/redux/layers/layers.thunks.test.ts @@ -111,13 +111,7 @@ describe('layers thunks', () => { mockedAxiosClient.onPost(apiPath.storeLayer(1)).reply(HttpStatusCode.Created, layerFixture); const { payload } = await store.dispatch( - addLayerForModel({ - name: 'New Layer', - visible: true, - locked: false, - modelId: 1, - zIndex: 1, - }), + addLayerForModel({ name: 'New Layer', visible: true, locked: false, modelId: 1 }), ); expect(payload).toEqual(layerFixture); }); @@ -128,13 +122,7 @@ describe('layers thunks', () => { .reply(HttpStatusCode.Created, { invalid: 'data' }); const { payload } = await store.dispatch( - addLayerForModel({ - name: 'New Layer', - visible: true, - locked: false, - modelId: 1, - zIndex: 1, - }), + addLayerForModel({ name: 'New Layer', visible: true, locked: false, modelId: 1 }), ); expect(payload).toBeNull(); }); @@ -151,7 +139,6 @@ describe('layers thunks', () => { locked: true, modelId: 1, layerId: 2, - zIndex: 1, }), ); expect(payload).toEqual(layerFixture); @@ -169,7 +156,6 @@ describe('layers thunks', () => { locked: true, modelId: 1, layerId: 2, - zIndex: 1, }), ); expect(payload).toBeNull(); diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts index bda72fce..66035fd8 100644 --- a/src/redux/layers/layers.thunks.ts +++ b/src/redux/layers/layers.thunks.ts @@ -32,7 +32,7 @@ export const getLayer = createAsyncThunk< Layer | null, { modelId: number; layerId: number }, ThunkConfig ->('layers/getLayer', async ({ modelId, layerId }) => { +>('vectorMap/getLayer', async ({ modelId, layerId }) => { try { const { data } = await axiosInstanceNewAPI.get<Layer>(apiPath.getLayer(modelId, layerId)); @@ -48,7 +48,7 @@ export const getLayersForModel = createAsyncThunk< LayersVisibilitiesState | undefined, number, ThunkConfig ->('layers/getLayers', async (modelId: number) => { +>('vectorMap/getLayers', async (modelId: number) => { try { const { data } = await axiosInstanceNewAPI.get<Layers>(apiPath.getLayers(modelId)); const isDataValid = validateDataUsingZodSchema(data, pageableSchema(layerSchema)); @@ -103,14 +103,13 @@ export const getLayersForModel = createAsyncThunk< }); export const addLayerForModel = createAsyncThunk<Layer | null, LayerStoreInterface, ThunkConfig>( - 'layers/addLayer', - async ({ name, visible, locked, modelId, zIndex }) => { + 'vectorMap/addLayer', + async ({ name, visible, locked, modelId }) => { try { const { data } = await axiosInstanceNewAPI.post<Layer>(apiPath.storeLayer(modelId), { name, visible, locked, - z: zIndex, }); const isDataValid = validateDataUsingZodSchema(data, layerSchema); @@ -123,14 +122,13 @@ export const addLayerForModel = createAsyncThunk<Layer | null, LayerStoreInterfa ); export const updateLayer = createAsyncThunk<Layer | null, LayerUpdateInterface, ThunkConfig>( - 'layers/updateLayer', - async ({ name, visible, locked, modelId, layerId, zIndex }) => { + 'vectorMap/updateLayer', + async ({ name, visible, locked, modelId, layerId }) => { try { const { data } = await axiosInstanceNewAPI.put<Layer>(apiPath.updateLayer(modelId, layerId), { name, visible, locked, - z: zIndex, }); const isDataValid = validateDataUsingZodSchema(data, layerSchema); @@ -146,7 +144,7 @@ export const removeLayer = createAsyncThunk< null, { modelId: number; layerId: number }, ThunkConfig ->('layers/removeLayer', async ({ modelId, layerId }) => { +>('vectorMap/removeLayer', async ({ modelId, layerId }) => { try { await axiosInstanceNewAPI.delete<void>(apiPath.removeLayer(modelId, layerId)); return null; @@ -163,7 +161,7 @@ export const getLayerImage = createAsyncThunk< imageId: number; }, ThunkConfig ->('layers/getLayerImage', async ({ modelId, layerId, imageId }) => { +>('vectorMap/getLayerImage', async ({ modelId, layerId, imageId }) => { try { const { data } = await axiosInstanceNewAPI.get<LayerImage>( apiPath.getLayerImageObject(modelId, layerId, imageId), @@ -189,7 +187,7 @@ export const addLayerImageObject = createAsyncThunk< glyph: number | null; }, ThunkConfig ->('layers/addLayerImageObject', async ({ modelId, layerId, x, y, z, width, height, glyph }) => { +>('vectorMap/addLayerImageObject', async ({ modelId, layerId, x, y, z, width, height, glyph }) => { try { const { data } = await axiosInstanceNewAPI.post<LayerImage>( apiPath.addLayerImageObject(modelId, layerId), @@ -225,7 +223,7 @@ export const updateLayerImageObject = createAsyncThunk< }, ThunkConfig >( - 'layers/updateLayerImageObject', + 'vectorMap/updateLayerImageObject', async ({ modelId, layerId, id, x, y, z, width, height, glyph }) => { try { const { data } = await axiosInstanceNewAPI.put<LayerImage>( @@ -254,7 +252,7 @@ export const removeLayerImage = createAsyncThunk< null, { modelId: number; layerId: number; imageId: number }, ThunkConfig ->('layers/removeLayerImage', async ({ modelId, layerId, imageId }) => { +>('vectorMap/removeLayerImage', async ({ modelId, layerId, imageId }) => { try { await axiosInstanceNewAPI.delete<void>( apiPath.removeLayerImageObject(modelId, layerId, imageId), @@ -275,7 +273,7 @@ export const addLayerText = createAsyncThunk< textData: LayerTextFactoryForm; }, ThunkConfig ->('layers/addLayerText', async ({ modelId, layerId, z, boundingBox, textData }) => { +>('vectorMap/addLayerText', async ({ modelId, layerId, z, boundingBox, textData }) => { try { const { data } = await axiosInstanceNewAPI.post<LayerText>( apiPath.addLayerText(modelId, layerId), @@ -301,7 +299,7 @@ export const getLayerText = createAsyncThunk< textId: number; }, ThunkConfig ->('layers/getLayerText', async ({ modelId, layerId, textId }) => { +>('vectorMap/getLayerText', async ({ modelId, layerId, textId }) => { try { const { data } = await axiosInstanceNewAPI.get<LayerText>( apiPath.getLayerText(modelId, layerId, textId), diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts index 05b4636e..60e42717 100644 --- a/src/redux/layers/layers.types.ts +++ b/src/redux/layers/layers.types.ts @@ -6,7 +6,6 @@ export interface LayerStoreInterface { visible: boolean; locked: boolean; modelId: number; - zIndex: number; } export interface LayerUpdateInterface { @@ -15,7 +14,6 @@ export interface LayerUpdateInterface { visible: boolean; locked: boolean; modelId: number; - zIndex: number; } export type LayerState = { diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx index aed961c3..5745b6ec 100644 --- a/src/shared/Icon/Icon.component.tsx +++ b/src/shared/Icon/Icon.component.tsx @@ -31,10 +31,8 @@ import { PadlockOpenIcon } from '@/shared/Icon/Icons/PadlockOpenIcon'; import { PadlockLockedIcon } from '@/shared/Icon/Icons/PadlockLockedIcon'; import { CrossedEyeIcon } from '@/shared/Icon/Icons/CrossedEyeIcon'; import { CenterIcon } from '@/shared/Icon/Icons/CenterIcon'; -import { MoveFrontIcon } from '@/shared/Icon/Icons/MoveFrontIcon'; -import { MoveBackIcon } from '@/shared/Icon/Icons/MoveBackIcon'; -import { LayerArrowUpIcon } from '@/shared/Icon/Icons/LayerArrowUpIcon'; -import { LayerArrowDownIcon } from '@/shared/Icon/Icons/LayerArrowDownIcon'; +import { BringFrontIcon } from '@/shared/Icon/Icons/BringFrontIcon'; +import { BringBackIcon } from '@/shared/Icon/Icons/BringBackIcon'; import { LocationIcon } from './Icons/LocationIcon'; import { MaginfierZoomInIcon } from './Icons/MagnifierZoomIn'; import { MaginfierZoomOutIcon } from './Icons/MagnifierZoomOut'; @@ -89,10 +87,8 @@ const icons: Record<IconTypes, IconComponentType> = { 'padlock-open': PadlockOpenIcon, 'padlock-locked': PadlockLockedIcon, center: CenterIcon, - 'move-front': MoveFrontIcon, - 'move-back': MoveBackIcon, - 'layer-arrow-up': LayerArrowUpIcon, - 'layer-arrow-down': LayerArrowDownIcon, + 'bring-front': BringFrontIcon, + 'bring-back': BringBackIcon, } as const; export const Icon = ({ name, className = '', ...rest }: IconProps): JSX.Element => { diff --git a/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx b/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx index cf5ce5f7..d7ab4e38 100644 --- a/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx +++ b/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx @@ -13,16 +13,26 @@ export const ArrowDoubleDownIcon = ({ className }: ArrowDoubleDownIconProps): JS > <path d="M8 8L8 18M8 18L5 15M8 18L11 15" + stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> <path d="M16 8L16 18M16 18L13 15M16 18L19 15" + stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - <line x1="4" y1="6" x2="20" y2="6" strokeWidth="1.5" strokeLinecap="round" /> + <line + x1="4" + y1="6" + x2="20" + y2="6" + stroke="currentColor" + strokeWidth="1.5" + strokeLinecap="round" + /> </svg> ); diff --git a/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx b/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx index 861773cd..ed51a602 100644 --- a/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx +++ b/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx @@ -13,16 +13,26 @@ export const ArrowDoubleUpIcon = ({ className }: ArrowDoubleUpIconProps): JSX.El > <path d="M8 16L8 6M8 6L5 9M8 6L11 9" + stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> <path d="M16 16L16 6M16 6L13 9M16 6L19 9" + stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - <line x1="4" y1="18" x2="20" y2="18" strokeWidth="1.5" strokeLinecap="round" /> + <line + x1="4" + y1="18" + x2="20" + y2="18" + stroke="currentColor" + strokeWidth="1.5" + strokeLinecap="round" + /> </svg> ); diff --git a/src/shared/Icon/Icons/MoveBackIcon.tsx b/src/shared/Icon/Icons/BringBackIcon.tsx similarity index 78% rename from src/shared/Icon/Icons/MoveBackIcon.tsx rename to src/shared/Icon/Icons/BringBackIcon.tsx index 734bd201..d086ac9b 100644 --- a/src/shared/Icon/Icons/MoveBackIcon.tsx +++ b/src/shared/Icon/Icons/BringBackIcon.tsx @@ -1,8 +1,8 @@ -interface MoveBackIconProps { +interface BringBackIconProps { className?: string; } -export const MoveBackIcon = ({ className }: MoveBackIconProps): JSX.Element => ( +export const BringBackIcon = ({ className }: BringBackIconProps): JSX.Element => ( <svg width="20" height="20" @@ -23,6 +23,6 @@ export const MoveBackIcon = ({ className }: MoveBackIconProps): JSX.Element => ( stroke="white" fill="none" /> - <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" /> + <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" fill="black" /> </svg> ); diff --git a/src/shared/Icon/Icons/MoveFrontIcon.tsx b/src/shared/Icon/Icons/BringFrontIcon.tsx similarity index 77% rename from src/shared/Icon/Icons/MoveFrontIcon.tsx rename to src/shared/Icon/Icons/BringFrontIcon.tsx index 037f5cb3..e5aa5017 100644 --- a/src/shared/Icon/Icons/MoveFrontIcon.tsx +++ b/src/shared/Icon/Icons/BringFrontIcon.tsx @@ -1,8 +1,8 @@ -interface MoveFrontIconProps { +interface BringFrontIconProps { className?: string; } -export const MoveFrontIcon = ({ className }: MoveFrontIconProps): JSX.Element => ( +export const BringFrontIcon = ({ className }: BringFrontIconProps): JSX.Element => ( <svg width="20" height="20" @@ -11,7 +11,7 @@ export const MoveFrontIcon = ({ className }: MoveFrontIconProps): JSX.Element => fill="none" className={className} > - <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" /> + <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" fill="black" /> <rect x="30" y="30" diff --git a/src/shared/Icon/Icons/LayerArrowDownIcon.tsx b/src/shared/Icon/Icons/LayerArrowDownIcon.tsx deleted file mode 100644 index 71223a31..00000000 --- a/src/shared/Icon/Icons/LayerArrowDownIcon.tsx +++ /dev/null @@ -1,18 +0,0 @@ -interface LayerArrowDownIconProps { - className?: string; -} - -export const LayerArrowDownIcon = ({ className }: LayerArrowDownIconProps): JSX.Element => ( - <svg - width="20" - height="20" - viewBox="2 3 20 17" - fill="none" - xmlns="http://www.w3.org/2000/svg" - className={className} - > - <path d="M12 21L7 16H10V10H14V16H17L12 21Z" /> - <rect x="4" y="3" width="16" height="2" /> - <rect x="4" y="7" width="16" height="2" /> - </svg> -); diff --git a/src/shared/Icon/Icons/LayerArrowUpIcon.tsx b/src/shared/Icon/Icons/LayerArrowUpIcon.tsx deleted file mode 100644 index bbcf233f..00000000 --- a/src/shared/Icon/Icons/LayerArrowUpIcon.tsx +++ /dev/null @@ -1,18 +0,0 @@ -interface LayerArrowUpIconProps { - className?: string; -} - -export const LayerArrowUpIcon = ({ className }: LayerArrowUpIconProps): JSX.Element => ( - <svg - width="20" - height="20" - viewBox="2 3 20 19" - fill="none" - xmlns="http://www.w3.org/2000/svg" - className={className} - > - <path d="M12 3L7 8H10V14H14V8H17L12 3Z" /> - <rect x="4" y="16" width="16" height="2" /> - <rect x="4" y="20" width="16" height="2" /> - </svg> -); diff --git a/src/shared/IconButton/IconButton.component.tsx b/src/shared/IconButton/IconButton.component.tsx index e71c6211..1330e460 100644 --- a/src/shared/IconButton/IconButton.component.tsx +++ b/src/shared/IconButton/IconButton.component.tsx @@ -29,8 +29,8 @@ export const IconButton = ({ const isStrokeIcon = [ 'plugin', - 'move-back', - 'move-front', + 'bring-back', + 'bring-front', 'center', 'eye', 'crossed-eye', @@ -38,8 +38,6 @@ export const IconButton = ({ 'padlock-locked', 'layers', 'edit', - 'arrow-double-up', - 'arrow-double-down', ].includes(icon); return ( @@ -59,8 +57,6 @@ export const IconButton = ({ isStrokeIcon ? 'stroke-font-400 group-hover:stroke-primary-500 group-active:stroke-primary-500' : 'fill-font-400 group-hover:fill-primary-500 group-active:fill-primary-500', - ['move-back', 'move-front'].includes(icon) && - 'fill-font-400 stroke-font-400 group-hover:fill-primary-500 group-hover:stroke-primary-500 group-active:fill-primary-500 group-active:stroke-primary-500', isActive && getActiveFillOrStrokeColor(icon), classNameIcon, )} diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts index 0083a622..0d74dd78 100644 --- a/src/types/iconTypes.ts +++ b/src/types/iconTypes.ts @@ -38,9 +38,7 @@ export type IconTypes = | 'padlock-open' | 'padlock-locked' | 'center' - | 'move-front' - | 'move-back' - | 'layer-arrow-up' - | 'layer-arrow-down'; + | 'bring-front' + | 'bring-back'; export type IconComponentType = ({ className }: { className: string }) => JSX.Element; -- GitLab From 9a22256d5716408896071fe7f94a3eb95ca86932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Tue, 18 Feb 2025 10:24:21 +0100 Subject: [PATCH 5/8] feat(layers): add layers zIndex editing -- GitLab From 3b5fd23cd01eca739523f29616d79577817a2bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Tue, 18 Feb 2025 10:27:03 +0100 Subject: [PATCH 6/8] Revert "Revert "feat(layers): add layers zIndex editing"" This reverts commit f6e504ece2951e59900cec58b70bec18b21b4bde. --- .../LayerFactoryModal.component.tsx | 4 + ...LayerImageObjectFactoryModal.component.tsx | 9 +- .../LayerTextFactoryModal.component.tsx | 9 +- .../LayerDrawerTextItem.component.tsx | 12 +- .../LayersDrawer/LayersDrawer.component.tsx | 46 ++++-- .../LayersDrawerImageItem.component.tsx | 12 +- .../LayersDrawerLayer.component.tsx | 107 +++++++++++--- .../LayersDrawerLayerActions.component.tsx | 39 +++++ .../LayersDrawerObjectActions.component.tsx | 20 +-- .../LayersDrawerObjectsList.component.tsx | 32 ++-- .../useOlMapAdditionalLayers.ts | 1 + .../commentsLayer/useOlMapCommentsLayer.ts | 1 + .../config/mapCardLayer/useOlMapCardLayer.ts | 1 + .../config/pinsLayer/useOlMapPinsLayer.ts | 1 + .../processLayer/useOlMapProcessLayer.ts | 1 + .../reactionsLayer/useOlMapReactionsLayer.ts | 1 + .../utils/shapes/layer/Layer.test.ts | 1 + .../Map/MapViewer/utils/shapes/layer/Layer.ts | 3 + src/models/layerSchema.ts | 1 + src/redux/layers/layers.reducers.test.ts | 4 +- src/redux/layers/layers.selectors.ts | 139 +++++++++++++----- src/redux/layers/layers.thunks.test.ts | 18 ++- src/redux/layers/layers.thunks.ts | 28 ++-- src/redux/layers/layers.types.ts | 2 + src/shared/Icon/Icon.component.tsx | 12 +- src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx | 12 +- src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx | 12 +- src/shared/Icon/Icons/LayerArrowDownIcon.tsx | 18 +++ src/shared/Icon/Icons/LayerArrowUpIcon.tsx | 18 +++ .../{BringBackIcon.tsx => MoveBackIcon.tsx} | 6 +- .../{BringFrontIcon.tsx => MoveFrontIcon.tsx} | 6 +- .../IconButton/IconButton.component.tsx | 8 +- src/types/iconTypes.ts | 6 +- 33 files changed, 430 insertions(+), 160 deletions(-) create mode 100644 src/shared/Icon/Icons/LayerArrowDownIcon.tsx create mode 100644 src/shared/Icon/Icons/LayerArrowUpIcon.tsx rename src/shared/Icon/Icons/{BringBackIcon.tsx => MoveBackIcon.tsx} (78%) rename src/shared/Icon/Icons/{BringFrontIcon.tsx => MoveFrontIcon.tsx} (77%) diff --git a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx index 8d925951..b806d2b6 100644 --- a/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerFactoryModal/LayerFactoryModal.component.tsx @@ -19,11 +19,13 @@ import { SerializedError } from '@reduxjs/toolkit'; import { layerFactoryStateSelector } from '@/redux/modal/modal.selector'; import './LayerFactoryModal.styles.css'; import { LoadingIndicator } from '@/shared/LoadingIndicator'; +import { maxLayerZIndexAboveDiagramSelector } from '@/redux/layers/layers.selectors'; export const LayerFactoryModal: React.FC = () => { const dispatch = useAppDispatch(); const currentModelId = useAppSelector(currentModelIdSelector); const layerFactoryState = useAppSelector(layerFactoryStateSelector); + const maxLayerZIndexAboveDiagram = useAppSelector(maxLayerZIndexAboveDiagramSelector); const [loaded, setLoaded] = useState<boolean>(false); const [data, setData] = useState<LayerStoreInterface>({ @@ -31,6 +33,7 @@ export const LayerFactoryModal: React.FC = () => { visible: false, locked: false, modelId: currentModelId, + zIndex: maxLayerZIndexAboveDiagram, }); const fetchData = useMemo(() => { @@ -42,6 +45,7 @@ export const LayerFactoryModal: React.FC = () => { visible: layer.visible, locked: layer.locked, modelId: currentModelId, + zIndex: layer.z, }); } setLoaded(true); diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx index 1efb7d88..4d1513e9 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx @@ -4,7 +4,10 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { layerImageObjectFactoryStateSelector } from '@/redux/modal/modal.selector'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; -import { highestZIndexSelector, layersDrawLayerSelector } from '@/redux/layers/layers.selectors'; +import { + layersDrawLayerSelector, + maxObjectZIndexForLayerSelector, +} from '@/redux/layers/layers.selectors'; import { addLayerImageObject } from '@/redux/layers/layers.thunks'; import { addGlyph } from '@/redux/glyphs/glyphs.thunks'; import { SerializedError } from '@reduxjs/toolkit'; @@ -22,7 +25,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => { const drawLayer = useAppSelector(layersDrawLayerSelector); const layerImageObjectFactoryState = useAppSelector(layerImageObjectFactoryStateSelector); const dispatch = useAppDispatch(); - const highestZIndex = useAppSelector(highestZIndexSelector); + const maxZIndex = useAppSelector(state => maxObjectZIndexForLayerSelector(state, drawLayer)); const { mapInstance } = useMapInstance(); const [selectedGlyph, setSelectedGlyph] = useState<number | null>(null); @@ -49,7 +52,7 @@ export const LayerImageObjectFactoryModal: React.FC = () => { layerId: drawLayer, x: layerImageObjectFactoryState.x, y: layerImageObjectFactoryState.y, - z: highestZIndex + 1, + z: maxZIndex + 1, width: layerImageObjectFactoryState.width, height: layerImageObjectFactoryState.height, glyph: glyphId, diff --git a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx index e1ccf612..bfb80aff 100644 --- a/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerTextFactoryModal/LayerTextFactoryModal.component.tsx @@ -13,7 +13,10 @@ import { LayerTextFactoryForm } from '@/components/FunctionalArea/Modal/LayerTex import { Color } from '@/types/models'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { layerTextFactoryStateSelector } from '@/redux/modal/modal.selector'; -import { highestZIndexSelector, layersDrawLayerSelector } from '@/redux/layers/layers.selectors'; +import { + layersDrawLayerSelector, + maxObjectZIndexForLayerSelector, +} from '@/redux/layers/layers.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { showToast } from '@/utils/showToast'; @@ -31,7 +34,7 @@ export const LayerTextFactoryModal: React.FC = () => { const currentModelId = useAppSelector(currentModelIdSelector); const layerTextFactoryState = useAppSelector(layerTextFactoryStateSelector); const dispatch = useAppDispatch(); - const highestZIndex = useAppSelector(highestZIndexSelector); + const maxZIndex = useAppSelector(state => maxObjectZIndexForLayerSelector(state, drawLayer)); const { mapInstance } = useMapInstance(); const [isSending, setIsSending] = useState<boolean>(false); @@ -55,7 +58,7 @@ export const LayerTextFactoryModal: React.FC = () => { layerId: drawLayer, boundingBox: layerTextFactoryState, textData: data, - z: highestZIndex + 1, + z: maxZIndex + 1, }), ).unwrap(); if (!textData) { diff --git a/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx index 6f57c762..a9c3d3b3 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayerDrawerTextItem.component.tsx @@ -10,8 +10,8 @@ import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors' interface LayersDrawerTextItemProps { layerText: LayerText; - bringToFront: () => void; - bringToBack: () => void; + moveToFront: () => void; + moveToBack: () => void; removeObject: () => void; centerObject: () => void; editObject: () => void; @@ -21,8 +21,8 @@ interface LayersDrawerTextItemProps { export const LayersDrawerTextItem = ({ layerText, - bringToFront, - bringToBack, + moveToFront, + moveToBack, removeObject, centerObject, editObject, @@ -68,8 +68,8 @@ export const LayersDrawerTextItem = ({ </div> {showActions && ( <LayersDrawerObjectActions - bringToFront={bringToFront} - bringToBack={bringToBack} + moveToFront={moveToFront} + moveToBack={moveToBack} removeObject={removeObject} centerObject={centerObject} editObject={editObject} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx index 12bef081..33c259f3 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawer.component.tsx @@ -4,7 +4,7 @@ import { DrawerHeading } from '@/shared/DrawerHeading'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { layersForCurrentModelSelector } from '@/redux/layers/layers.selectors'; import { Button } from '@/shared/Button'; -import { JSX, useEffect, useRef } from 'react'; +import { JSX, useEffect, useMemo, useRef } from 'react'; import { openLayerFactoryModal } from '@/redux/modal/modal.slice'; import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors'; import { LayersDrawerLayer } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component'; @@ -21,6 +21,19 @@ export const LayersDrawer = (): JSX.Element => { dispatch(openLayerFactoryModal()); }; + const sortedLayers = useMemo(() => { + return [...layersForCurrentModel].sort((layerA, layerB) => layerB.details.z - layerA.details.z); + }, [layersForCurrentModel]); + + const negativeZLayers = useMemo( + () => sortedLayers.filter(layer => layer.details.z < 0), + [sortedLayers], + ); + const positiveZLayers = useMemo( + () => sortedLayers.filter(layer => layer.details.z >= 0), + [sortedLayers], + ); + useEffect(() => { if (!mapEditToolsLayerImageObject || !layersDrawerRef.current) { return; @@ -61,20 +74,33 @@ export const LayersDrawer = (): JSX.Element => { ref={layersDrawerRef} > {hasPrivilegeToWriteProject && ( - <div className="flex justify-start pt-2"> + <div className="flex justify-start py-2"> <Button icon="plus" isIcon isFrontIcon onClick={addNewLayer}> Add layer </Button> </div> )} - <div className="flex flex-col gap-4"> - {layersForCurrentModel.map(layer => ( - <LayersDrawerLayer - key={layer.details.id} - layerId={layer.details.id} - layerName={layer.details.name} - /> - ))} + <div className="flex flex-col gap-2"> + {Boolean(positiveZLayers.length) && ( + <span className="border-b-2 border-dashed border-b-gray-400 text-center text-lg font-semibold"> + Layers above the diagram + </span> + )} + <div className="flex flex-col gap-5"> + {positiveZLayers.map(layer => ( + <LayersDrawerLayer key={layer.details.id} layerDetails={layer.details} /> + ))} + </div> + {Boolean(negativeZLayers.length) && ( + <span className="border-b-2 border-dashed border-b-gray-400 text-center text-lg font-semibold"> + Layers below the diagram + </span> + )} + <div className="flex flex-col gap-5"> + {negativeZLayers.map(layer => ( + <LayersDrawerLayer key={layer.details.id} layerDetails={layer.details} /> + ))} + </div> </div> </div> </div> diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx index 0622f141..e160b807 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component.tsx @@ -11,8 +11,8 @@ import { hasPrivilegeToWriteProjectSelector } from '@/redux/user/user.selectors' interface LayersDrawerImageItemProps { layerImage: LayerImage; - bringToFront: () => void; - bringToBack: () => void; + moveToFront: () => void; + moveToBack: () => void; removeObject: () => void; centerObject: () => void; editObject: () => void; @@ -22,8 +22,8 @@ interface LayersDrawerImageItemProps { export const LayersDrawerImageItem = ({ layerImage, - bringToFront, - bringToBack, + moveToFront, + moveToBack, removeObject, centerObject, editObject, @@ -72,8 +72,8 @@ export const LayersDrawerImageItem = ({ </div> {showActions && ( <LayersDrawerObjectActions - bringToFront={bringToFront} - bringToBack={bringToBack} + moveToFront={moveToFront} + moveToBack={moveToBack} removeObject={removeObject} centerObject={centerObject} editObject={editObject} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx index 49b8536f..8b0f9b01 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx @@ -1,14 +1,19 @@ +/* eslint-disable no-magic-numbers */ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { layersActiveLayersSelector, layersVisibilityForCurrentModelSelector, + maxLayerZIndexAboveDiagramSelector, + maxLayerZIndexBelowDiagramSelector, + minLayerZIndexAboveDiagramSelector, + minLayerZIndexBelowDiagramSelector, } from '@/redux/layers/layers.selectors'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { openLayerFactoryModal } from '@/redux/modal/modal.slice'; import QuestionModal from '@/components/FunctionalArea/Modal/QuestionModal/QustionModal.component'; import { useState, JSX, useMemo } from 'react'; -import { getLayersForModel, removeLayer } from '@/redux/layers/layers.thunks'; +import { getLayersForModel, removeLayer, updateLayer } from '@/redux/layers/layers.thunks'; import { showToast } from '@/utils/showToast'; import { SerializedError } from '@reduxjs/toolkit'; import { LayersDrawerLayerActions } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component'; @@ -21,38 +26,42 @@ import { import { LayersDrawerObjectsList } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component'; import { mapEditToolsSetActiveAction } from '@/redux/mapEditTools/mapEditTools.slice'; import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; +import { Layer } from '@/types/models'; interface LayersDrawerLayerProps { - layerId: number; - layerName: string; + layerDetails: Layer; } -export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps): JSX.Element => { +export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX.Element => { const layersVisibilityForCurrentModel = useAppSelector(layersVisibilityForCurrentModelSelector); const activeLayers = useAppSelector(layersActiveLayersSelector); const currentModelId = useAppSelector(currentModelIdSelector); + const maxLayerZIndexAboveDiagram = useAppSelector(maxLayerZIndexAboveDiagramSelector); + const maxLayerZIndexBelowDiagram = useAppSelector(maxLayerZIndexBelowDiagramSelector); + const minLayerZIndexAboveDiagram = useAppSelector(minLayerZIndexAboveDiagramSelector); + const minLayerZIndexBelowDiagram = useAppSelector(minLayerZIndexBelowDiagramSelector); const dispatch = useAppDispatch(); const [isModalOpen, setIsModalOpen] = useState(false); const isLayerVisible = useMemo(() => { - return layersVisibilityForCurrentModel[layerId]; - }, [layerId, layersVisibilityForCurrentModel]); + return layersVisibilityForCurrentModel[layerDetails.id]; + }, [layerDetails.id, layersVisibilityForCurrentModel]); const isLayerActive = useMemo(() => { - return activeLayers.includes(layerId); - }, [activeLayers, layerId]); + return activeLayers.includes(layerDetails.id); + }, [activeLayers, layerDetails.id]); const editLayer = (): void => { - dispatch(openLayerFactoryModal(layerId)); + dispatch(openLayerFactoryModal(layerDetails.id)); }; const addImage = (): void => { - dispatch(setDrawLayer({ modelId: currentModelId, layerId })); + dispatch(setDrawLayer({ modelId: currentModelId, layerId: layerDetails.id })); dispatch(mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.DRAW_IMAGE)); }; const addText = (): void => { - dispatch(setDrawLayer({ modelId: currentModelId, layerId })); + dispatch(setDrawLayer({ modelId: currentModelId, layerId: layerDetails.id })); dispatch(mapEditToolsSetActiveAction(MAP_EDIT_ACTIONS.ADD_TEXT)); }; @@ -61,11 +70,11 @@ export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps }; const confirmRemove = async (): Promise<void> => { - if (!layerId) { + if (!layerDetails.id) { return; } try { - await dispatch(removeLayer({ modelId: currentModelId, layerId })).unwrap(); + await dispatch(removeLayer({ modelId: currentModelId, layerId: layerDetails.id })).unwrap(); showToast({ type: 'success', message: 'The layer has been successfully removed', @@ -87,12 +96,61 @@ export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps const toggleActiveLayer = (value: boolean): void => { if (value) { - dispatch(setLayerToActive({ modelId: currentModelId, layerId })); + dispatch(setLayerToActive({ modelId: currentModelId, layerId: layerDetails.id })); } else { - dispatch(setLayerToInactive({ modelId: currentModelId, layerId })); + dispatch(setLayerToInactive({ modelId: currentModelId, layerId: layerDetails.id })); } }; + const updateLayerZIndex = async (zIndex: number): Promise<void> => { + try { + dispatch( + updateLayer({ + name: layerDetails.name, + visible: layerDetails.visible, + locked: layerDetails.locked, + modelId: currentModelId, + layerId: layerDetails.id, + zIndex, + }), + ); + } catch (error) { + const typedError = error as SerializedError; + showToast({ + type: 'error', + message: typedError.message || 'An error occurred while updating the layer', + }); + } + }; + + const moveToFront = (): void => { + if (layerDetails.z > 0) { + updateLayerZIndex(maxLayerZIndexAboveDiagram); + } else if (layerDetails.z < 0) { + const zIndex = Math.min(maxLayerZIndexBelowDiagram, -1); + updateLayerZIndex(zIndex); + } + }; + + const moveToBack = (): void => { + if (layerDetails.z > 0) { + const zIndex = Math.max(minLayerZIndexAboveDiagram, 1); + updateLayerZIndex(zIndex); + } else if (layerDetails.z < 0) { + updateLayerZIndex(minLayerZIndexBelowDiagram); + } + }; + + const moveAboveDiagram = (): void => { + const zIndex = Math.max(minLayerZIndexAboveDiagram - 1, 1); + updateLayerZIndex(zIndex); + }; + + const moveBelowDiagram = (): void => { + const zIndex = Math.min(minLayerZIndexBelowDiagram + 1, -1); + updateLayerZIndex(zIndex); + }; + return ( <div> <QuestionModal @@ -101,9 +159,13 @@ export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps onConfirm={confirmRemove} question="Are you sure you want to remove the layer?" /> - <div className="flex items-center justify-between py-3"> - <span className={`font-semibold ${isLayerVisible ? 'opacity-100' : 'opacity-40'}`}> - {layerName} + <div className="flex items-center justify-between pb-3"> + <span + className={`font-semibold ${ + isLayerVisible ? 'opacity-100' : 'opacity-40' + } min-w-0 flex-1 truncate `} + > + {layerDetails.name} </span> <LayersDrawerLayerActions toggleVisibility={() => @@ -111,7 +173,7 @@ export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps setLayerVisibility({ modelId: currentModelId, visible: !isLayerVisible, - layerId, + layerId: layerDetails.id, }), ) } @@ -120,12 +182,17 @@ export const LayersDrawerLayer = ({ layerId, layerName }: LayersDrawerLayerProps removeLayer={onRemoveLayer} addImage={addImage} addText={addText} + moveToFront={moveToFront} + moveToBack={moveToBack} + moveAboveDiagram={moveAboveDiagram} + moveBelowDiagram={moveBelowDiagram} + zIndex={layerDetails.z} isVisible={isLayerVisible} isActive={isLayerActive} /> </div> <LayersDrawerObjectsList - layerId={layerId} + layerId={layerDetails.id} isLayerVisible={isLayerVisible} isLayerActive={isLayerActive} /> diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx index 18627b13..81435100 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayerActions.component.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-magic-numbers */ import { IconButton } from '@/shared/IconButton'; import { JSX } from 'react'; import { LayerDrawerLayerContextMenu } from '@/components/Map/Drawer/LayersDrawer/LayerDrawerLayerContextMenu.component'; @@ -9,10 +10,15 @@ type LayersDrawerLayerActionsProps = { removeLayer: () => void; isVisible: boolean; isActive: boolean; + zIndex: number; toggleVisibility: () => void; toggleActiveLayer: (value: boolean) => void; addImage: () => void; addText: () => void; + moveToFront: () => void; + moveToBack: () => void; + moveAboveDiagram: () => void; + moveBelowDiagram: () => void; }; export const LayersDrawerLayerActions = ({ @@ -20,10 +26,15 @@ export const LayersDrawerLayerActions = ({ removeLayer, isVisible, isActive, + zIndex, toggleVisibility, toggleActiveLayer, addImage, addText, + moveToFront, + moveToBack, + moveAboveDiagram, + moveBelowDiagram, }: LayersDrawerLayerActionsProps): JSX.Element => { const hasPrivilegeToWriteProject = useAppSelector(hasPrivilegeToWriteProjectSelector); @@ -43,6 +54,34 @@ export const LayersDrawerLayerActions = ({ className="h-auto w-auto bg-transparent p-0" onClick={() => toggleActiveLayer(!isActive)} /> + <IconButton + title="Move to front" + icon="move-front" + className="h-auto w-auto bg-transparent p-0" + onClick={moveToFront} + /> + <IconButton + title="Move to back" + icon="move-back" + className="h-auto w-auto bg-transparent p-0" + onClick={moveToBack} + /> + {zIndex < 0 && ( + <IconButton + title="Move above the diagram" + icon="layer-arrow-up" + className="h-auto w-auto bg-transparent p-0" + onClick={moveAboveDiagram} + /> + )} + {zIndex > 0 && ( + <IconButton + title="Move below the diagram" + icon="layer-arrow-down" + className="h-auto w-auto bg-transparent p-0" + onClick={moveBelowDiagram} + /> + )} <LayerDrawerLayerContextMenu removeLayer={removeLayer} editLayer={editLayer} diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx index 4cf6871e..a97189ea 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectActions.component.tsx @@ -2,16 +2,16 @@ import { JSX } from 'react'; import { IconButton } from '@/shared/IconButton'; interface LayersDrawerObjectActionsProps { - bringToFront: () => void; - bringToBack: () => void; + moveToFront: () => void; + moveToBack: () => void; removeObject: () => void; centerObject: () => void; editObject: () => void; } export const LayersDrawerObjectActions = ({ - bringToFront, - bringToBack, + moveToFront, + moveToBack, removeObject, centerObject, editObject, @@ -31,16 +31,16 @@ export const LayersDrawerObjectActions = ({ onClick={editObject} /> <IconButton - icon="bring-front" + icon="move-front" className="h-auto w-auto bg-transparent p-0" - title="Bring to front" - onClick={bringToFront} + title="Move to front" + onClick={moveToFront} /> <IconButton - icon="bring-back" + icon="move-back" className="h-auto w-auto bg-transparent p-0" - title="Bring to back" - onClick={bringToBack} + title="Move to back" + onClick={moveToBack} /> <IconButton icon="trash" diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx index 70a6acf5..9ec19fe4 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerObjectsList.component.tsx @@ -1,9 +1,9 @@ /* eslint-disable no-magic-numbers */ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { - highestZIndexSelector, layerByIdSelector, - lowestZIndexSelector, + maxObjectZIndexForLayerSelector, + minObjectZIndexForLayerSelector, } from '@/redux/layers/layers.selectors'; import { JSX, useState } from 'react'; import { LayersDrawerImageItem } from '@/components/Map/Drawer/LayersDrawer/LayersDrawerImageItem.component'; @@ -64,8 +64,8 @@ export const LayersDrawerObjectsList = ({ isLayerActive, }: LayersDrawerObjectsListProps): JSX.Element | null => { const currentModelId = useAppSelector(mapModelIdSelector); - const highestZIndex = useAppSelector(highestZIndexSelector); - const lowestZIndex = useAppSelector(lowestZIndexSelector); + const maxZIndex = useAppSelector(state => maxObjectZIndexForLayerSelector(state, layerId)); + const minZIndex = useAppSelector(state => minObjectZIndexForLayerSelector(state, layerId)); const layer = useAppSelector(state => layerByIdSelector(state, layerId)); const mapEditToolsLayerImageObject = useAppSelector(mapEditToolsLayerObjectSelector); const [removeModalState, setRemoveModalState] = useState<undefined | 'text' | 'image'>(undefined); @@ -174,12 +174,12 @@ export const LayersDrawerObjectsList = ({ } }; - const bringImageToFront = async (layerImage: LayerImage): Promise<void> => { - await updateImageZIndex({ zIndex: highestZIndex + 1, layerImage }); + const moveImageToFront = async (layerImage: LayerImage): Promise<void> => { + await updateImageZIndex({ zIndex: maxZIndex + 1, layerImage }); }; - const bringImageToBack = async (layerImage: LayerImage): Promise<void> => { - await updateImageZIndex({ zIndex: lowestZIndex - 1, layerImage }); + const moveImageToBack = async (layerImage: LayerImage): Promise<void> => { + await updateImageZIndex({ zIndex: minZIndex - 1, layerImage }); }; const updateTextZIndex = async ({ @@ -210,12 +210,12 @@ export const LayersDrawerObjectsList = ({ } }; - const bringTextToFront = async (layerText: LayerText): Promise<void> => { - await updateTextZIndex({ zIndex: highestZIndex + 1, layerText }); + const moveTextToFront = async (layerText: LayerText): Promise<void> => { + await updateTextZIndex({ zIndex: maxZIndex + 1, layerText }); }; - const bringTextToBack = async (layerText: LayerText): Promise<void> => { - await updateTextZIndex({ zIndex: lowestZIndex - 1, layerText }); + const moveTextToBack = async (layerText: LayerText): Promise<void> => { + await updateTextZIndex({ zIndex: minZIndex - 1, layerText }); }; const centerObject = (layerObject: LayerImage | LayerText): void => { @@ -257,8 +257,8 @@ export const LayersDrawerObjectsList = ({ <LayersDrawerTextItem layerText={layerText} key={layerText.id} - bringToFront={() => bringTextToFront(layerText)} - bringToBack={() => bringTextToBack(layerText)} + moveToFront={() => moveTextToFront(layerText)} + moveToBack={() => moveTextToBack(layerText)} removeObject={() => removeObject(layerText)} centerObject={() => centerObject(layerText)} editObject={() => editText()} @@ -270,8 +270,8 @@ export const LayersDrawerObjectsList = ({ <LayersDrawerImageItem layerImage={layerImage} key={layerImage.id} - bringToFront={() => bringImageToFront(layerImage)} - bringToBack={() => bringImageToBack(layerImage)} + moveToFront={() => moveImageToFront(layerImage)} + moveToBack={() => moveImageToBack(layerImage)} removeObject={() => removeObject(layerImage)} centerObject={() => centerObject(layerImage)} editObject={() => editImage()} diff --git a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index 5e378293..6d41944f 100644 --- a/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -125,6 +125,7 @@ export const useOlMapAdditionalLayers = ( const vectorLayers = useMemo(() => { return layersState.map(layer => { const additionalLayer = new Layer({ + zIndex: layer.details.z, texts: layer.texts, rects: layer.rects, ovals: layer.ovals, diff --git a/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts b/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts index 7ac3b73b..a3640020 100644 --- a/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/commentsLayer/useOlMapCommentsLayer.ts @@ -36,6 +36,7 @@ export const useOlMapCommentsLayer = (): VectorLayer<VectorSource<Feature<Geomet return useMemo(() => { const vectorLayer = new VectorLayer({ + zIndex: Infinity, source: vectorSource, }); vectorLayer.set('type', LAYER_TYPE.COMMENTS_LAYER); diff --git a/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts b/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts index 69cec807..db3dbad4 100644 --- a/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts +++ b/src/components/Map/MapViewer/utils/config/mapCardLayer/useOlMapCardLayer.ts @@ -38,6 +38,7 @@ export const useOlMapCardLayer = (): VectorLayer<VectorSource<Feature<Polygon>>> return useMemo(() => { const vectorLayer = new VectorLayer({ + zIndex: -Infinity, source: vectorSource, style: new Style({ fill: new Fill({ diff --git a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts index f7b6001c..4cd5a32b 100644 --- a/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/pinsLayer/useOlMapPinsLayer.ts @@ -77,6 +77,7 @@ export const useOlMapPinsLayer = (): VectorLayer<VectorSource<Feature<Geometry>> return useMemo(() => { const vectorLayer = new VectorLayer({ + zIndex: Infinity, source: vectorSource, }); vectorLayer.set('type', LAYER_TYPE.PINS_LAYER); diff --git a/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts b/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts index 2811ec30..98cb6e1e 100644 --- a/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts +++ b/src/components/Map/MapViewer/utils/config/processLayer/useOlMapProcessLayer.ts @@ -274,6 +274,7 @@ export const useOlMapProcessLayer = ({ return useMemo(() => { const vectorLayer = new VectorLayer({ + zIndex: 0, source: vectorSource, updateWhileAnimating: true, updateWhileInteracting: true, diff --git a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts index bfb2668e..df526271 100644 --- a/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts +++ b/src/components/Map/MapViewer/utils/config/reactionsLayer/useOlMapReactionsLayer.ts @@ -63,6 +63,7 @@ export const useOlMapReactionsLayer = (): VectorLayer<VectorSource<Feature<Geome return useMemo(() => { const vectorLayer = new VectorLayer({ + zIndex: Infinity, source: vectorSource, style: new Style({ fill: new Fill({ color: LINE_COLOR }), diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts index 7b8bb971..10349c6d 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.test.ts @@ -133,6 +133,7 @@ describe('Layer', () => { pointToProjection: jest.fn(point => [point.x, point.y]), mapInstance, mapSize, + zIndex: 1, lineTypes: {}, arrowTypes: {}, }; diff --git a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts index 7f697a72..14d5f8b5 100644 --- a/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/utils/shapes/layer/Layer.ts @@ -34,6 +34,7 @@ import LayerText from '@/components/Map/MapViewer/utils/shapes/layer/elements/La import LayerImage from '@/components/Map/MapViewer/utils/shapes/layer/elements/LayerImage'; export interface LayerProps { + zIndex: number; texts: { [key: string]: LayerTextModel }; rects: Array<LayerRect>; ovals: Array<LayerOval>; @@ -78,6 +79,7 @@ export default class Layer { >; constructor({ + zIndex, texts, rects, ovals, @@ -118,6 +120,7 @@ export default class Layer { this.vectorSource.addFeatures(arrowsFeatures); this.vectorLayer = new VectorLayer({ + zIndex, source: this.vectorSource, visible, updateWhileAnimating: true, diff --git a/src/models/layerSchema.ts b/src/models/layerSchema.ts index 380146a7..11fcd88d 100644 --- a/src/models/layerSchema.ts +++ b/src/models/layerSchema.ts @@ -5,4 +5,5 @@ export const layerSchema = z.object({ name: z.string(), visible: z.boolean(), locked: z.boolean(), + z: z.number(), }); diff --git a/src/redux/layers/layers.reducers.test.ts b/src/redux/layers/layers.reducers.test.ts index 7bcb10ba..d93fb4f8 100644 --- a/src/redux/layers/layers.reducers.test.ts +++ b/src/redux/layers/layers.reducers.test.ts @@ -92,7 +92,7 @@ describe('layers reducer', () => { const { type } = await store.dispatch(getLayersForModel(1)); const { data, loading, error } = store.getState().layers[1]; - expect(type).toBe('vectorMap/getLayers/fulfilled'); + expect(type).toBe('layers/getLayers/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); expect(data).toEqual({ @@ -120,7 +120,7 @@ describe('layers reducer', () => { const action = await store.dispatch(getLayersForModel(1)); const { data, loading, error } = store.getState().layers[1]; - expect(action.type).toBe('vectorMap/getLayers/rejected'); + expect(action.type).toBe('layers/getLayers/rejected'); expect(() => unwrapResult(action)).toThrow( "Failed to fetch layers: The page you're looking for doesn't exist. Please verify the URL and try again.", ); diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts index 8f016c8a..8123fa00 100644 --- a/src/redux/layers/layers.selectors.ts +++ b/src/redux/layers/layers.selectors.ts @@ -2,6 +2,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { rootSelector } from '@/redux/root/root.selectors'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; +import { LayerState } from '@/redux/layers/layers.types'; export const layersSelector = createSelector(rootSelector, state => state.layers); @@ -18,7 +19,7 @@ export const layersActiveLayersSelector = createSelector( export const layersDrawLayerSelector = createSelector( layersStateForCurrentModelSelector, - state => state?.data?.drawLayer, + state => state?.data?.drawLayer || null, ); export const layerByIdSelector = createSelector( @@ -41,40 +42,112 @@ export const layersForCurrentModelSelector = createSelector( state => state?.data?.layers || [], ); -export const highestZIndexSelector = createSelector(layersForCurrentModelSelector, layers => { - if (!layers || layers.length === 0) return 0; - - const getMaxZFromItems = <T extends { z?: number }>(items: T[] = []): number => - items.length > 0 ? Math.max(...items.map(item => item.z || 0)) : 0; - - return layers.reduce((maxZ, layer) => { - const textsMaxZ = getMaxZFromItems(Object.values(layer.texts)); - const rectsMaxZ = getMaxZFromItems(layer.rects); - const ovalsMaxZ = getMaxZFromItems(layer.ovals); - const linesMaxZ = getMaxZFromItems(layer.lines); - const imagesMaxZ = getMaxZFromItems(Object.values(layer.images)); - - const layerMaxZ = Math.max(textsMaxZ, rectsMaxZ, ovalsMaxZ, linesMaxZ, imagesMaxZ); - - return Math.max(maxZ, layerMaxZ); - }, 0); -}); +export const maxLayerZIndexAboveDiagramSelector = createSelector( + layersForCurrentModelSelector, + layers => { + if (!layers || layers.length === 0) { + return 1000; + } + let maxZIndex = -Infinity; + layers.forEach((layer: LayerState) => { + if (layer.details.z > 0 && layer.details.z > maxZIndex) { + maxZIndex = layer.details.z; + } + }); + return maxZIndex; + }, +); -export const lowestZIndexSelector = createSelector(layersForCurrentModelSelector, layers => { - if (!layers || layers.length === 0) return 0; +export const maxLayerZIndexBelowDiagramSelector = createSelector( + layersForCurrentModelSelector, + layers => { + if (!layers || layers.length === 0) { + return -1000; + } + let maxZIndex = -Infinity; + layers.forEach((layer: LayerState) => { + if (layer.details.z < 0 && layer.details.z > maxZIndex) { + maxZIndex = layer.details.z; + } + }); + return maxZIndex; + }, +); - const getMinZFromItems = <T extends { z?: number }>(items: T[] = []): number => - items.length > 0 ? Math.min(...items.map(item => item.z || 0)) : 0; +export const minLayerZIndexAboveDiagramSelector = createSelector( + layersForCurrentModelSelector, + layers => { + if (!layers || layers.length === 0) { + return 1000; + } + let minZIndex = Infinity; + layers.forEach((layer: LayerState) => { + if (layer.details.z > 0 && layer.details.z < minZIndex) { + minZIndex = layer.details.z; + } + }); + return minZIndex; + }, +); - return layers.reduce((minZ, layer) => { - const textsMinZ = getMinZFromItems(Object.values(layer.texts)); - const rectsMinZ = getMinZFromItems(layer.rects); - const ovalsMinZ = getMinZFromItems(layer.ovals); - const linesMinZ = getMinZFromItems(layer.lines); - const imagesMinZ = getMinZFromItems(Object.values(layer.images)); +export const minLayerZIndexBelowDiagramSelector = createSelector( + layersForCurrentModelSelector, + layers => { + if (!layers || layers.length === 0) { + return -1000; + } + let minZIndex = Infinity; + layers.forEach((layer: LayerState) => { + if (layer.details.z < 0 && layer.details.z < minZIndex) { + minZIndex = layer.details.z; + } + }); + return minZIndex; + }, +); - const layerMinZ = Math.min(textsMinZ, rectsMinZ, ovalsMinZ, linesMinZ, imagesMinZ); +export const maxObjectZIndexForLayerSelector = createSelector( + [layersForCurrentModelSelector, (_state, layerId: number | null): number | null => layerId], + (layers, layerId) => { + if (!layers || layers.length === 0 || !layerId) { + return 0; + } + const foundLayer = layers.find(layer => layer.details.id === layerId); + if (!foundLayer) { + return 0; + } + const getMaxZFromItems = <T extends { z?: number }>(items: T[] = []): number => + items.length > 0 ? Math.max(...items.map(item => item.z || 0)) : 0; + + const textsMaxZ = getMaxZFromItems(Object.values(foundLayer.texts)); + const rectsMaxZ = getMaxZFromItems(foundLayer.rects); + const ovalsMaxZ = getMaxZFromItems(foundLayer.ovals); + const linesMaxZ = getMaxZFromItems(foundLayer.lines); + const imagesMaxZ = getMaxZFromItems(Object.values(foundLayer.images)); + + return Math.max(textsMaxZ, rectsMaxZ, ovalsMaxZ, linesMaxZ, imagesMaxZ); + }, +); - return Math.min(minZ, layerMinZ); - }, 0); -}); +export const minObjectZIndexForLayerSelector = createSelector( + [layersForCurrentModelSelector, (_state, layerId: number | null): number | null => layerId], + (layers, layerId) => { + if (!layers || layers.length === 0 || !layerId) { + return 0; + } + const foundLayer = layers.find(layer => layer.details.id === layerId); + if (!foundLayer) { + return 0; + } + const getMinZFromItems = <T extends { z?: number }>(items: T[] = []): number => + items.length > 0 ? Math.min(...items.map(item => item.z || 0)) : 0; + + const textsMinZ = getMinZFromItems(Object.values(foundLayer.texts)); + const rectsMinZ = getMinZFromItems(foundLayer.rects); + const ovalsMinZ = getMinZFromItems(foundLayer.ovals); + const linesMinZ = getMinZFromItems(foundLayer.lines); + const imagesMinZ = getMinZFromItems(Object.values(foundLayer.images)); + + return Math.min(textsMinZ, rectsMinZ, ovalsMinZ, linesMinZ, imagesMinZ); + }, +); diff --git a/src/redux/layers/layers.thunks.test.ts b/src/redux/layers/layers.thunks.test.ts index 77e10bdb..09028272 100644 --- a/src/redux/layers/layers.thunks.test.ts +++ b/src/redux/layers/layers.thunks.test.ts @@ -111,7 +111,13 @@ describe('layers thunks', () => { mockedAxiosClient.onPost(apiPath.storeLayer(1)).reply(HttpStatusCode.Created, layerFixture); const { payload } = await store.dispatch( - addLayerForModel({ name: 'New Layer', visible: true, locked: false, modelId: 1 }), + addLayerForModel({ + name: 'New Layer', + visible: true, + locked: false, + modelId: 1, + zIndex: 1, + }), ); expect(payload).toEqual(layerFixture); }); @@ -122,7 +128,13 @@ describe('layers thunks', () => { .reply(HttpStatusCode.Created, { invalid: 'data' }); const { payload } = await store.dispatch( - addLayerForModel({ name: 'New Layer', visible: true, locked: false, modelId: 1 }), + addLayerForModel({ + name: 'New Layer', + visible: true, + locked: false, + modelId: 1, + zIndex: 1, + }), ); expect(payload).toBeNull(); }); @@ -139,6 +151,7 @@ describe('layers thunks', () => { locked: true, modelId: 1, layerId: 2, + zIndex: 1, }), ); expect(payload).toEqual(layerFixture); @@ -156,6 +169,7 @@ describe('layers thunks', () => { locked: true, modelId: 1, layerId: 2, + zIndex: 1, }), ); expect(payload).toBeNull(); diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts index 66035fd8..bda72fce 100644 --- a/src/redux/layers/layers.thunks.ts +++ b/src/redux/layers/layers.thunks.ts @@ -32,7 +32,7 @@ export const getLayer = createAsyncThunk< Layer | null, { modelId: number; layerId: number }, ThunkConfig ->('vectorMap/getLayer', async ({ modelId, layerId }) => { +>('layers/getLayer', async ({ modelId, layerId }) => { try { const { data } = await axiosInstanceNewAPI.get<Layer>(apiPath.getLayer(modelId, layerId)); @@ -48,7 +48,7 @@ export const getLayersForModel = createAsyncThunk< LayersVisibilitiesState | undefined, number, ThunkConfig ->('vectorMap/getLayers', async (modelId: number) => { +>('layers/getLayers', async (modelId: number) => { try { const { data } = await axiosInstanceNewAPI.get<Layers>(apiPath.getLayers(modelId)); const isDataValid = validateDataUsingZodSchema(data, pageableSchema(layerSchema)); @@ -103,13 +103,14 @@ export const getLayersForModel = createAsyncThunk< }); export const addLayerForModel = createAsyncThunk<Layer | null, LayerStoreInterface, ThunkConfig>( - 'vectorMap/addLayer', - async ({ name, visible, locked, modelId }) => { + 'layers/addLayer', + async ({ name, visible, locked, modelId, zIndex }) => { try { const { data } = await axiosInstanceNewAPI.post<Layer>(apiPath.storeLayer(modelId), { name, visible, locked, + z: zIndex, }); const isDataValid = validateDataUsingZodSchema(data, layerSchema); @@ -122,13 +123,14 @@ export const addLayerForModel = createAsyncThunk<Layer | null, LayerStoreInterfa ); export const updateLayer = createAsyncThunk<Layer | null, LayerUpdateInterface, ThunkConfig>( - 'vectorMap/updateLayer', - async ({ name, visible, locked, modelId, layerId }) => { + 'layers/updateLayer', + async ({ name, visible, locked, modelId, layerId, zIndex }) => { try { const { data } = await axiosInstanceNewAPI.put<Layer>(apiPath.updateLayer(modelId, layerId), { name, visible, locked, + z: zIndex, }); const isDataValid = validateDataUsingZodSchema(data, layerSchema); @@ -144,7 +146,7 @@ export const removeLayer = createAsyncThunk< null, { modelId: number; layerId: number }, ThunkConfig ->('vectorMap/removeLayer', async ({ modelId, layerId }) => { +>('layers/removeLayer', async ({ modelId, layerId }) => { try { await axiosInstanceNewAPI.delete<void>(apiPath.removeLayer(modelId, layerId)); return null; @@ -161,7 +163,7 @@ export const getLayerImage = createAsyncThunk< imageId: number; }, ThunkConfig ->('vectorMap/getLayerImage', async ({ modelId, layerId, imageId }) => { +>('layers/getLayerImage', async ({ modelId, layerId, imageId }) => { try { const { data } = await axiosInstanceNewAPI.get<LayerImage>( apiPath.getLayerImageObject(modelId, layerId, imageId), @@ -187,7 +189,7 @@ export const addLayerImageObject = createAsyncThunk< glyph: number | null; }, ThunkConfig ->('vectorMap/addLayerImageObject', async ({ modelId, layerId, x, y, z, width, height, glyph }) => { +>('layers/addLayerImageObject', async ({ modelId, layerId, x, y, z, width, height, glyph }) => { try { const { data } = await axiosInstanceNewAPI.post<LayerImage>( apiPath.addLayerImageObject(modelId, layerId), @@ -223,7 +225,7 @@ export const updateLayerImageObject = createAsyncThunk< }, ThunkConfig >( - 'vectorMap/updateLayerImageObject', + 'layers/updateLayerImageObject', async ({ modelId, layerId, id, x, y, z, width, height, glyph }) => { try { const { data } = await axiosInstanceNewAPI.put<LayerImage>( @@ -252,7 +254,7 @@ export const removeLayerImage = createAsyncThunk< null, { modelId: number; layerId: number; imageId: number }, ThunkConfig ->('vectorMap/removeLayerImage', async ({ modelId, layerId, imageId }) => { +>('layers/removeLayerImage', async ({ modelId, layerId, imageId }) => { try { await axiosInstanceNewAPI.delete<void>( apiPath.removeLayerImageObject(modelId, layerId, imageId), @@ -273,7 +275,7 @@ export const addLayerText = createAsyncThunk< textData: LayerTextFactoryForm; }, ThunkConfig ->('vectorMap/addLayerText', async ({ modelId, layerId, z, boundingBox, textData }) => { +>('layers/addLayerText', async ({ modelId, layerId, z, boundingBox, textData }) => { try { const { data } = await axiosInstanceNewAPI.post<LayerText>( apiPath.addLayerText(modelId, layerId), @@ -299,7 +301,7 @@ export const getLayerText = createAsyncThunk< textId: number; }, ThunkConfig ->('vectorMap/getLayerText', async ({ modelId, layerId, textId }) => { +>('layers/getLayerText', async ({ modelId, layerId, textId }) => { try { const { data } = await axiosInstanceNewAPI.get<LayerText>( apiPath.getLayerText(modelId, layerId, textId), diff --git a/src/redux/layers/layers.types.ts b/src/redux/layers/layers.types.ts index 60e42717..05b4636e 100644 --- a/src/redux/layers/layers.types.ts +++ b/src/redux/layers/layers.types.ts @@ -6,6 +6,7 @@ export interface LayerStoreInterface { visible: boolean; locked: boolean; modelId: number; + zIndex: number; } export interface LayerUpdateInterface { @@ -14,6 +15,7 @@ export interface LayerUpdateInterface { visible: boolean; locked: boolean; modelId: number; + zIndex: number; } export type LayerState = { diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx index 5745b6ec..aed961c3 100644 --- a/src/shared/Icon/Icon.component.tsx +++ b/src/shared/Icon/Icon.component.tsx @@ -31,8 +31,10 @@ import { PadlockOpenIcon } from '@/shared/Icon/Icons/PadlockOpenIcon'; import { PadlockLockedIcon } from '@/shared/Icon/Icons/PadlockLockedIcon'; import { CrossedEyeIcon } from '@/shared/Icon/Icons/CrossedEyeIcon'; import { CenterIcon } from '@/shared/Icon/Icons/CenterIcon'; -import { BringFrontIcon } from '@/shared/Icon/Icons/BringFrontIcon'; -import { BringBackIcon } from '@/shared/Icon/Icons/BringBackIcon'; +import { MoveFrontIcon } from '@/shared/Icon/Icons/MoveFrontIcon'; +import { MoveBackIcon } from '@/shared/Icon/Icons/MoveBackIcon'; +import { LayerArrowUpIcon } from '@/shared/Icon/Icons/LayerArrowUpIcon'; +import { LayerArrowDownIcon } from '@/shared/Icon/Icons/LayerArrowDownIcon'; import { LocationIcon } from './Icons/LocationIcon'; import { MaginfierZoomInIcon } from './Icons/MagnifierZoomIn'; import { MaginfierZoomOutIcon } from './Icons/MagnifierZoomOut'; @@ -87,8 +89,10 @@ const icons: Record<IconTypes, IconComponentType> = { 'padlock-open': PadlockOpenIcon, 'padlock-locked': PadlockLockedIcon, center: CenterIcon, - 'bring-front': BringFrontIcon, - 'bring-back': BringBackIcon, + 'move-front': MoveFrontIcon, + 'move-back': MoveBackIcon, + 'layer-arrow-up': LayerArrowUpIcon, + 'layer-arrow-down': LayerArrowDownIcon, } as const; export const Icon = ({ name, className = '', ...rest }: IconProps): JSX.Element => { diff --git a/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx b/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx index d7ab4e38..cf5ce5f7 100644 --- a/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx +++ b/src/shared/Icon/Icons/ArrowDoubleDownIcon.tsx @@ -13,26 +13,16 @@ export const ArrowDoubleDownIcon = ({ className }: ArrowDoubleDownIconProps): JS > <path d="M8 8L8 18M8 18L5 15M8 18L11 15" - stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> <path d="M16 8L16 18M16 18L13 15M16 18L19 15" - stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - <line - x1="4" - y1="6" - x2="20" - y2="6" - stroke="currentColor" - strokeWidth="1.5" - strokeLinecap="round" - /> + <line x1="4" y1="6" x2="20" y2="6" strokeWidth="1.5" strokeLinecap="round" /> </svg> ); diff --git a/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx b/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx index ed51a602..861773cd 100644 --- a/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx +++ b/src/shared/Icon/Icons/ArrowDoubleUpIcon.tsx @@ -13,26 +13,16 @@ export const ArrowDoubleUpIcon = ({ className }: ArrowDoubleUpIconProps): JSX.El > <path d="M8 16L8 6M8 6L5 9M8 6L11 9" - stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> <path d="M16 16L16 6M16 6L13 9M16 6L19 9" - stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - <line - x1="4" - y1="18" - x2="20" - y2="18" - stroke="currentColor" - strokeWidth="1.5" - strokeLinecap="round" - /> + <line x1="4" y1="18" x2="20" y2="18" strokeWidth="1.5" strokeLinecap="round" /> </svg> ); diff --git a/src/shared/Icon/Icons/LayerArrowDownIcon.tsx b/src/shared/Icon/Icons/LayerArrowDownIcon.tsx new file mode 100644 index 00000000..71223a31 --- /dev/null +++ b/src/shared/Icon/Icons/LayerArrowDownIcon.tsx @@ -0,0 +1,18 @@ +interface LayerArrowDownIconProps { + className?: string; +} + +export const LayerArrowDownIcon = ({ className }: LayerArrowDownIconProps): JSX.Element => ( + <svg + width="20" + height="20" + viewBox="2 3 20 17" + fill="none" + xmlns="http://www.w3.org/2000/svg" + className={className} + > + <path d="M12 21L7 16H10V10H14V16H17L12 21Z" /> + <rect x="4" y="3" width="16" height="2" /> + <rect x="4" y="7" width="16" height="2" /> + </svg> +); diff --git a/src/shared/Icon/Icons/LayerArrowUpIcon.tsx b/src/shared/Icon/Icons/LayerArrowUpIcon.tsx new file mode 100644 index 00000000..bbcf233f --- /dev/null +++ b/src/shared/Icon/Icons/LayerArrowUpIcon.tsx @@ -0,0 +1,18 @@ +interface LayerArrowUpIconProps { + className?: string; +} + +export const LayerArrowUpIcon = ({ className }: LayerArrowUpIconProps): JSX.Element => ( + <svg + width="20" + height="20" + viewBox="2 3 20 19" + fill="none" + xmlns="http://www.w3.org/2000/svg" + className={className} + > + <path d="M12 3L7 8H10V14H14V8H17L12 3Z" /> + <rect x="4" y="16" width="16" height="2" /> + <rect x="4" y="20" width="16" height="2" /> + </svg> +); diff --git a/src/shared/Icon/Icons/BringBackIcon.tsx b/src/shared/Icon/Icons/MoveBackIcon.tsx similarity index 78% rename from src/shared/Icon/Icons/BringBackIcon.tsx rename to src/shared/Icon/Icons/MoveBackIcon.tsx index d086ac9b..734bd201 100644 --- a/src/shared/Icon/Icons/BringBackIcon.tsx +++ b/src/shared/Icon/Icons/MoveBackIcon.tsx @@ -1,8 +1,8 @@ -interface BringBackIconProps { +interface MoveBackIconProps { className?: string; } -export const BringBackIcon = ({ className }: BringBackIconProps): JSX.Element => ( +export const MoveBackIcon = ({ className }: MoveBackIconProps): JSX.Element => ( <svg width="20" height="20" @@ -23,6 +23,6 @@ export const BringBackIcon = ({ className }: BringBackIconProps): JSX.Element => stroke="white" fill="none" /> - <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" fill="black" /> + <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" /> </svg> ); diff --git a/src/shared/Icon/Icons/BringFrontIcon.tsx b/src/shared/Icon/Icons/MoveFrontIcon.tsx similarity index 77% rename from src/shared/Icon/Icons/BringFrontIcon.tsx rename to src/shared/Icon/Icons/MoveFrontIcon.tsx index e5aa5017..037f5cb3 100644 --- a/src/shared/Icon/Icons/BringFrontIcon.tsx +++ b/src/shared/Icon/Icons/MoveFrontIcon.tsx @@ -1,8 +1,8 @@ -interface BringFrontIconProps { +interface MoveFrontIconProps { className?: string; } -export const BringFrontIcon = ({ className }: BringFrontIconProps): JSX.Element => ( +export const MoveFrontIcon = ({ className }: MoveFrontIconProps): JSX.Element => ( <svg width="20" height="20" @@ -11,7 +11,7 @@ export const BringFrontIcon = ({ className }: BringFrontIconProps): JSX.Element fill="none" className={className} > - <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" fill="black" /> + <rect x="4" y="4" width="64" height="64" rx="10" ry="10" strokeWidth="8" /> <rect x="30" y="30" diff --git a/src/shared/IconButton/IconButton.component.tsx b/src/shared/IconButton/IconButton.component.tsx index 1330e460..e71c6211 100644 --- a/src/shared/IconButton/IconButton.component.tsx +++ b/src/shared/IconButton/IconButton.component.tsx @@ -29,8 +29,8 @@ export const IconButton = ({ const isStrokeIcon = [ 'plugin', - 'bring-back', - 'bring-front', + 'move-back', + 'move-front', 'center', 'eye', 'crossed-eye', @@ -38,6 +38,8 @@ export const IconButton = ({ 'padlock-locked', 'layers', 'edit', + 'arrow-double-up', + 'arrow-double-down', ].includes(icon); return ( @@ -57,6 +59,8 @@ export const IconButton = ({ isStrokeIcon ? 'stroke-font-400 group-hover:stroke-primary-500 group-active:stroke-primary-500' : 'fill-font-400 group-hover:fill-primary-500 group-active:fill-primary-500', + ['move-back', 'move-front'].includes(icon) && + 'fill-font-400 stroke-font-400 group-hover:fill-primary-500 group-hover:stroke-primary-500 group-active:fill-primary-500 group-active:stroke-primary-500', isActive && getActiveFillOrStrokeColor(icon), classNameIcon, )} diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts index 0d74dd78..0083a622 100644 --- a/src/types/iconTypes.ts +++ b/src/types/iconTypes.ts @@ -38,7 +38,9 @@ export type IconTypes = | 'padlock-open' | 'padlock-locked' | 'center' - | 'bring-front' - | 'bring-back'; + | 'move-front' + | 'move-back' + | 'layer-arrow-up' + | 'layer-arrow-down'; export type IconComponentType = ({ className }: { className: string }) => JSX.Element; -- GitLab From 55a29206777f4d2a37fea8821a3796f70a25c2d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Tue, 18 Feb 2025 10:49:03 +0100 Subject: [PATCH 7/8] feat(layers): fetch layers after updating layer z index --- .../Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx index 8b0f9b01..53dd8d14 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx @@ -114,6 +114,7 @@ export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX zIndex, }), ); + dispatch(getLayersForModel(currentModelId)); } catch (error) { const typedError = error as SerializedError; showToast({ -- GitLab From 70547634bee979c09b32b33d6e222d05da58b4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Wed, 19 Feb 2025 13:58:12 +0100 Subject: [PATCH 8/8] fix(layers): correct min/max zIndex selectors --- .../LayersDrawerLayer.component.tsx | 16 ++++++------- src/redux/layers/layers.selectors.ts | 24 +++++++++++-------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx index 53dd8d14..93e12281 100644 --- a/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx +++ b/src/components/Map/Drawer/LayersDrawer/LayersDrawerLayer.component.tsx @@ -104,7 +104,7 @@ export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX const updateLayerZIndex = async (zIndex: number): Promise<void> => { try { - dispatch( + await dispatch( updateLayer({ name: layerDetails.name, visible: layerDetails.visible, @@ -114,7 +114,7 @@ export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX zIndex, }), ); - dispatch(getLayersForModel(currentModelId)); + await dispatch(getLayersForModel(currentModelId)); } catch (error) { const typedError = error as SerializedError; showToast({ @@ -126,29 +126,29 @@ export const LayersDrawerLayer = ({ layerDetails }: LayersDrawerLayerProps): JSX const moveToFront = (): void => { if (layerDetails.z > 0) { - updateLayerZIndex(maxLayerZIndexAboveDiagram); + updateLayerZIndex(maxLayerZIndexAboveDiagram + 1); } else if (layerDetails.z < 0) { - const zIndex = Math.min(maxLayerZIndexBelowDiagram, -1); + const zIndex = Math.min(maxLayerZIndexBelowDiagram + 1, -1); updateLayerZIndex(zIndex); } }; const moveToBack = (): void => { if (layerDetails.z > 0) { - const zIndex = Math.max(minLayerZIndexAboveDiagram, 1); + const zIndex = Math.max(minLayerZIndexAboveDiagram - 1, 1); updateLayerZIndex(zIndex); } else if (layerDetails.z < 0) { - updateLayerZIndex(minLayerZIndexBelowDiagram); + updateLayerZIndex(minLayerZIndexBelowDiagram - 1); } }; const moveAboveDiagram = (): void => { - const zIndex = Math.max(minLayerZIndexAboveDiagram - 1, 1); + const zIndex = Math.max(maxLayerZIndexAboveDiagram + 1, 1); updateLayerZIndex(zIndex); }; const moveBelowDiagram = (): void => { - const zIndex = Math.min(minLayerZIndexBelowDiagram + 1, -1); + const zIndex = Math.min(minLayerZIndexBelowDiagram - 1, -1); updateLayerZIndex(zIndex); }; diff --git a/src/redux/layers/layers.selectors.ts b/src/redux/layers/layers.selectors.ts index 8123fa00..37eee222 100644 --- a/src/redux/layers/layers.selectors.ts +++ b/src/redux/layers/layers.selectors.ts @@ -45,11 +45,12 @@ export const layersForCurrentModelSelector = createSelector( export const maxLayerZIndexAboveDiagramSelector = createSelector( layersForCurrentModelSelector, layers => { - if (!layers || layers.length === 0) { - return 1000; + const layersAboveDiagram = layers.filter(layer => layer.details.z > 0); + if (layersAboveDiagram.length === 0) { + return -1000; } let maxZIndex = -Infinity; - layers.forEach((layer: LayerState) => { + layersAboveDiagram.forEach((layer: LayerState) => { if (layer.details.z > 0 && layer.details.z > maxZIndex) { maxZIndex = layer.details.z; } @@ -61,11 +62,12 @@ export const maxLayerZIndexAboveDiagramSelector = createSelector( export const maxLayerZIndexBelowDiagramSelector = createSelector( layersForCurrentModelSelector, layers => { - if (!layers || layers.length === 0) { + const layerBelowDiagram = layers.filter(layer => layer.details.z < 0); + if (layerBelowDiagram.length === 0) { return -1000; } let maxZIndex = -Infinity; - layers.forEach((layer: LayerState) => { + layerBelowDiagram.forEach((layer: LayerState) => { if (layer.details.z < 0 && layer.details.z > maxZIndex) { maxZIndex = layer.details.z; } @@ -77,11 +79,12 @@ export const maxLayerZIndexBelowDiagramSelector = createSelector( export const minLayerZIndexAboveDiagramSelector = createSelector( layersForCurrentModelSelector, layers => { - if (!layers || layers.length === 0) { - return 1000; + const layersAboveDiagram = layers.filter(layer => layer.details.z > 0); + if (layersAboveDiagram.length === 0) { + return -1000; } let minZIndex = Infinity; - layers.forEach((layer: LayerState) => { + layersAboveDiagram.forEach((layer: LayerState) => { if (layer.details.z > 0 && layer.details.z < minZIndex) { minZIndex = layer.details.z; } @@ -93,11 +96,12 @@ export const minLayerZIndexAboveDiagramSelector = createSelector( export const minLayerZIndexBelowDiagramSelector = createSelector( layersForCurrentModelSelector, layers => { - if (!layers || layers.length === 0) { + const layerBelowDiagram = layers.filter(layer => layer.details.z < 0); + if (layerBelowDiagram.length === 0) { return -1000; } let minZIndex = Infinity; - layers.forEach((layer: LayerState) => { + layerBelowDiagram.forEach((layer: LayerState) => { if (layer.details.z < 0 && layer.details.z < minZIndex) { minZIndex = layer.details.z; } -- GitLab