diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts deleted file mode 100644 index 119478065cc2155d8a9123fa0ecf696cc4bc9e9d..0000000000000000000000000000000000000000 --- a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LayerImageObjectFactoryModal } from './LayerImageObjectFactoryModal.component'; diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8095d60e53fdca8b3fe19cf285e37612339545a9 --- /dev/null +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx @@ -0,0 +1,173 @@ +/* eslint-disable no-magic-numbers */ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; +import { StoreType } from '@/redux/store'; +import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; +import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures'; +import { GLYPHS_STATE_INITIAL_MOCK } from '@/redux/glyphs/glyphs.mock'; +import { apiPath } from '@/redux/apiPath'; +import { HttpStatusCode } from 'axios'; +import { layerImageFixture } from '@/models/fixtures/layerImageFixture'; +import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse'; +import { + LAYER_STATE_DEFAULT_DATA, + LAYERS_STATE_INITIAL_LAYER_MOCK, +} from '@/redux/layers/layers.mock'; +import { MODELS_DATA_MOCK_WITH_MAIN_MAP } from '@/redux/models/models.mock'; +import { overlayFixture } from '@/models/fixtures/overlaysFixture'; +import { showToast } from '@/utils/showToast'; +import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types'; +import { MAP_EDIT_TOOLS_STATE_INITIAL_MOCK } from '@/redux/mapEditTools/mapEditTools.mock'; +import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; +import { Feature } from 'ol'; +import Polygon from 'ol/geom/Polygon'; +import { LayerImageObjectEditFactoryModal } from './LayerImageObjectEditFactoryModal.component'; + +const mockedAxiosNewClient = mockNetworkNewAPIResponse(); + +const glyph = { id: 1, file: 23, filename: 'Glyph1.png' }; + +jest.mock('../../../../utils/showToast'); + +const renderComponent = ( + initialMapEditToolsState: MapEditToolsState = MAP_EDIT_TOOLS_STATE_INITIAL_MOCK, +): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore({ + ...INITIAL_STORE_STATE_MOCK, + glyphs: { + ...GLYPHS_STATE_INITIAL_MOCK, + data: [glyph], + }, + layers: { + 0: { + ...LAYERS_STATE_INITIAL_LAYER_MOCK, + data: { + ...LAYER_STATE_DEFAULT_DATA, + activeLayer: 1, + }, + }, + }, + modal: { + isOpen: true, + modalTitle: overlayFixture.name, + modalName: 'edit-overlay', + editOverlayState: overlayFixture, + molArtState: {}, + overviewImagesState: {}, + errorReportState: {}, + layerFactoryState: { id: undefined }, + layerImageObjectFactoryState: { + x: 1, + y: 1, + width: 1, + height: 1, + }, + }, + models: { + ...MODELS_DATA_MOCK_WITH_MAIN_MAP, + }, + mapEditTools: initialMapEditToolsState, + }); + return { + store, + ...render( + <Wrapper> + <LayerImageObjectEditFactoryModal /> + </Wrapper>, + ), + }; +}; + +describe('LayerImageObjectEditFactoryModal - component', () => { + it('should render LayerImageObjectEditFactoryModal component with initial state', () => { + renderComponent(); + + expect(screen.getByText(/Glyph:/i)).toBeInTheDocument(); + expect(screen.getByText(/File:/i)).toBeInTheDocument(); + expect(screen.getByText(/Submit/i)).toBeInTheDocument(); + expect(screen.getByText(/No Image/i)).toBeInTheDocument(); + }); + + it('should display a list of glyphs in the dropdown', async () => { + renderComponent(); + + const dropdown = screen.getByTestId('autocomplete'); + if (!dropdown.firstChild) { + throw new Error('Dropdown does not have a firstChild'); + } + fireEvent.keyDown(dropdown.firstChild, { key: 'ArrowDown' }); + await waitFor(() => expect(screen.getByText(glyph.filename)).toBeInTheDocument()); + fireEvent.click(screen.getByText(glyph.filename)); + }); + + it('should update the selected glyph on dropdown change', async () => { + renderComponent(); + + const dropdown = screen.getByTestId('autocomplete'); + if (!dropdown.firstChild) { + throw new Error('Dropdown does not have a firstChild'); + } + fireEvent.keyDown(dropdown.firstChild, { key: 'ArrowDown' }); + await waitFor(() => expect(screen.getByText(glyph.filename)).toBeInTheDocument()); + fireEvent.click(screen.getByText(glyph.filename)); + + await waitFor(() => { + const imgPreview: HTMLImageElement = screen.getByTestId('layer-image-preview'); + const decodedSrc = decodeURIComponent(imgPreview.src); + expect(decodedSrc).toContain(`glyphs/${glyph.id}/fileContent`); + }); + }); + + it('should handle form submission correctly', async () => { + mockedAxiosNewClient + .onPut(apiPath.updateLayerImageObject(0, 1, 1)) + .reply(HttpStatusCode.Ok, layerImageFixture); + const geometry = new Polygon([ + [ + [10, 10], + [10, 10], + ], + ]); + const layerObjectFeature = new Feature({ geometry }); + const glyphData = { + id: 1, + x: 1, + y: 1, + width: 1, + height: 1, + glyph: 1, + z: 1, + }; + const getGlyphDataMock = jest.fn(() => glyphData); + jest.spyOn(layerObjectFeature, 'get').mockImplementation(key => { + if (key === 'setGlyph') return (): void => {}; + if (key === 'getGlyphData') return getGlyphDataMock; + return undefined; + }); + renderComponent({ + activeAction: MAP_EDIT_ACTIONS.TRANSFORM_IMAGE, + layerImageObject: glyphData, + }); + + const submitButton = screen.getByText(/Submit/i); + + await act(async () => { + fireEvent.click(submitButton); + }); + + expect(showToast).toHaveBeenCalledWith({ + message: 'The layer image object has been successfully updated', + type: 'success', + }); + }); + + it('should display "No Image" when there is no image file', () => { + const { store } = renderComponent(); + + store.dispatch({ + type: 'glyphs/clearGlyphData', + }); + + expect(screen.getByText(/No Image/i)).toBeInTheDocument(); + }); +}); diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..589b3a14c24b319247e8e34e3161dc860ce165d7 --- /dev/null +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx @@ -0,0 +1,98 @@ +/* eslint-disable no-magic-numbers */ +import React, { useState } from 'react'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; + +import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { LayerImageObjectForm } from '@/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; +import { layersActiveLayerSelector } from '@/redux/layers/layers.selectors'; +import { addGlyph } from '@/redux/glyphs/glyphs.thunks'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { updateLayerImageObject } from '@/redux/layers/layers.thunks'; +import { layerUpdateImage } from '@/redux/layers/layers.slice'; +import { showToast } from '@/utils/showToast'; +import { closeModal } from '@/redux/modal/modal.slice'; +import { SerializedError } from '@reduxjs/toolkit'; +import { useMapInstance } from '@/utils/context/mapInstanceContext'; +import VectorSource from 'ol/source/Vector'; + +export const LayerImageObjectEditFactoryModal: React.FC = () => { + const layerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); + const { mapInstance } = useMapInstance(); + + const currentModelId = useAppSelector(currentModelIdSelector); + const activeLayer = useAppSelector(layersActiveLayerSelector); + const dispatch = useAppDispatch(); + + const [selectedGlyph, setSelectedGlyph] = useState<number | null>( + layerImageObject?.glyph || null, + ); + const [file, setFile] = useState<File | null>(null); + const [isSending, setIsSending] = useState<boolean>(false); + + const handleSubmit = async (): Promise<void> => { + if (!layerImageObject || !activeLayer) { + return; + } + setIsSending(true); + + try { + let glyphId = selectedGlyph; + if (file) { + const data = await dispatch(addGlyph(file)).unwrap(); + if (!data) { + return; + } + glyphId = data.id; + } + const layerImage = await dispatch( + updateLayerImageObject({ + modelId: currentModelId, + layerId: activeLayer, + ...layerImageObject, + glyph: glyphId, + }), + ).unwrap(); + if (layerImage) { + dispatch(layerUpdateImage({ modelId: currentModelId, layerId: activeLayer, layerImage })); + mapInstance?.getAllLayers().forEach(layer => { + if (layer.get('id') === activeLayer) { + const source = layer.getSource(); + if (source instanceof VectorSource) { + const feature = source.getFeatureById(layerImage.id); + const setGlyph = feature?.get('setGlyph'); + if (setGlyph && setGlyph instanceof Function) { + setGlyph(layerImage.glyph); + feature.changed(); + } + } + } + }); + } + showToast({ + type: 'success', + message: 'The layer image object has been successfully updated', + }); + dispatch(closeModal()); + } catch (error) { + const typedError = error as SerializedError; + showToast({ + type: 'error', + message: typedError.message || 'An error occurred while adding a new image object', + }); + } finally { + setIsSending(false); + } + }; + + return ( + <LayerImageObjectForm + file={file} + selectedGlyph={selectedGlyph} + isSending={isSending} + onSubmit={handleSubmit} + setFile={setFile} + setSelectedGlyph={setSelectedGlyph} + /> + ); +}; diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.test.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx similarity index 100% rename from src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.test.tsx rename to src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.test.tsx diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx similarity index 51% rename from src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx rename to src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx index b750d0618794a63ed1219c7c3aa35c8bf471f450..98bc5fc540f0df197c285892bdb65a5a40bbcaa9 100644 --- a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.component.tsx +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectFactoryModal.component.tsx @@ -1,14 +1,7 @@ /* eslint-disable no-magic-numbers */ -import React, { useState, useRef } from 'react'; -import { glyphsDataSelector } from '@/redux/glyphs/glyphs.selectors'; +import React, { useState } from 'react'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { layerImageObjectFactoryStateSelector } from '@/redux/modal/modal.selector'; -import { Button } from '@/shared/Button'; -import { BASE_NEW_API_URL } from '@/constants'; -import { apiPath } from '@/redux/apiPath'; -import { Input } from '@/shared/Input'; -import Image from 'next/image'; -import { Glyph } from '@/types/models'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { currentModelIdSelector } from '@/redux/models/models.selectors'; import { highestZIndexSelector, layersActiveLayerSelector } from '@/redux/layers/layers.selectors'; @@ -17,57 +10,22 @@ import { addGlyph } from '@/redux/glyphs/glyphs.thunks'; import { SerializedError } from '@reduxjs/toolkit'; import { showToast } from '@/utils/showToast'; import { closeModal } from '@/redux/modal/modal.slice'; -import { LoadingIndicator } from '@/shared/LoadingIndicator'; -import './LayerImageObjectFactoryModal.styles.css'; +import './LayerImageObjectForm.styles.css'; import { useMapInstance } from '@/utils/context/mapInstanceContext'; import { layerAddImage } from '@/redux/layers/layers.slice'; -import { Autocomplete } from '@/shared/Autocomplete'; +import { LayerImageObjectForm } from '@/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component'; export const LayerImageObjectFactoryModal: React.FC = () => { - const glyphs: Glyph[] = useAppSelector(glyphsDataSelector); const currentModelId = useAppSelector(currentModelIdSelector); const activeLayer = useAppSelector(layersActiveLayerSelector); const layerImageObjectFactoryState = useAppSelector(layerImageObjectFactoryStateSelector); const dispatch = useAppDispatch(); - const fileInputRef = useRef<HTMLInputElement>(null); const highestZIndex = useAppSelector(highestZIndexSelector); const { mapInstance } = useMapInstance(); const [selectedGlyph, setSelectedGlyph] = useState<number | null>(null); const [file, setFile] = useState<File | null>(null); const [isSending, setIsSending] = useState<boolean>(false); - const [previewUrl, setPreviewUrl] = useState<string | null>(null); - - const handleGlyphChange = (glyph: Glyph | null): void => { - const glyphId = glyph?.id || null; - setSelectedGlyph(glyphId); - if (!glyphId) { - return; - } - setFile(null); - setPreviewUrl(`${BASE_NEW_API_URL}${apiPath.getGlyphImage(glyphId)}`); - - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - }; - - const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => { - const uploadedFile = e.target.files?.[0] || null; - - setFile(uploadedFile); - if (!uploadedFile) { - return; - } - - setSelectedGlyph(null); - if (uploadedFile) { - const url = URL.createObjectURL(uploadedFile); - setPreviewUrl(url); - } else { - setPreviewUrl(null); - } - }; const handleSubmit = async (): Promise<void> => { if (!layerImageObjectFactoryState || !activeLayer) { @@ -130,57 +88,13 @@ export const LayerImageObjectFactoryModal: React.FC = () => { }; return ( - <div className="relative w-[800px] border border-t-[#E1E0E6] bg-white p-[24px]"> - {isSending && ( - <div className="c-layer-image-object-factory-loader"> - <LoadingIndicator width={44} height={44} /> - </div> - )} - <div className="grid grid-cols-2 gap-2"> - <div className="mb-4 flex flex-col gap-2"> - <span>Glyph:</span> - <Autocomplete<Glyph> - options={glyphs} - valueKey="id" - labelKey="filename" - onChange={handleGlyphChange} - /> - </div> - <div className="mb-4 flex flex-col gap-2"> - <span>File:</span> - <Input - ref={fileInputRef} - type="file" - accept="image/*" - onChange={handleFileChange} - data-testid="image-file-input" - className="w-full border border-[#ccc] bg-white p-2" - /> - </div> - </div> - - <div className="relative mb-4 flex h-[350px] w-full items-center justify-center overflow-hidden rounded border"> - {previewUrl ? ( - <Image - src={previewUrl} - alt="image preview" - fill - style={{ objectFit: 'contain' }} - className="rounded" - data-testid="layer-image-preview" - /> - ) : ( - <div className="text-gray-500">No Image</div> - )} - </div> - - <Button - type="button" - onClick={handleSubmit} - className="w-full justify-center text-base font-medium" - > - Submit - </Button> - </div> + <LayerImageObjectForm + file={file} + selectedGlyph={selectedGlyph} + isSending={isSending} + onSubmit={handleSubmit} + setFile={setFile} + setSelectedGlyph={setSelectedGlyph} + /> ); }; diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component.tsx b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..58a6efac76ce294539463ad527017ee1af187f2c --- /dev/null +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component.tsx @@ -0,0 +1,121 @@ +/* eslint-disable no-magic-numbers */ +import React, { useRef, useMemo } from 'react'; +import { glyphsDataSelector } from '@/redux/glyphs/glyphs.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { Button } from '@/shared/Button'; +import { BASE_NEW_API_URL } from '@/constants'; +import { apiPath } from '@/redux/apiPath'; +import { Input } from '@/shared/Input'; +import Image from 'next/image'; +import { Glyph } from '@/types/models'; +import { LoadingIndicator } from '@/shared/LoadingIndicator'; +import './LayerImageObjectForm.styles.css'; +import { Autocomplete } from '@/shared/Autocomplete'; + +type LayerImageObjectFormProps = { + onSubmit: () => void; + isSending: boolean; + selectedGlyph: number | null; + setSelectedGlyph: (glyphId: number | null) => void; + file: File | null; + setFile: (file: File | null) => void; +}; + +export const LayerImageObjectForm = ({ + onSubmit, + isSending, + selectedGlyph, + setSelectedGlyph, + file, + setFile, +}: LayerImageObjectFormProps): React.JSX.Element => { + const glyphs: Glyph[] = useAppSelector(glyphsDataSelector); + const fileInputRef = useRef<HTMLInputElement>(null); + const initialSelectedGlyph = glyphs.find(glyph => glyph.id === selectedGlyph); + + const previewUrl: string | null = useMemo(() => { + if (selectedGlyph) { + return `${BASE_NEW_API_URL}${apiPath.getGlyphImage(selectedGlyph)}`; + } + if (file) { + return URL.createObjectURL(file); + } + return null; + }, [file, selectedGlyph]); + + const handleGlyphChange = (glyph: Glyph | null): void => { + const glyphId = glyph?.id || null; + setSelectedGlyph(glyphId); + if (!glyphId) { + return; + } + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => { + const uploadedFile = e.target.files?.[0] || null; + setFile(uploadedFile); + if (!uploadedFile) { + return; + } + setSelectedGlyph(null); + }; + + return ( + <div className="relative w-[800px] border border-t-[#E1E0E6] bg-white p-[24px]"> + {isSending && ( + <div className="c-layer-image-object-factory-loader"> + <LoadingIndicator width={44} height={44} /> + </div> + )} + <div className="grid grid-cols-2 gap-2"> + <div className="mb-4 flex flex-col gap-2"> + <span>Glyph:</span> + <Autocomplete<Glyph> + options={glyphs} + initialValue={initialSelectedGlyph} + valueKey="id" + labelKey="filename" + onChange={handleGlyphChange} + /> + </div> + <div className="mb-4 flex flex-col gap-2"> + <span>File:</span> + <Input + ref={fileInputRef} + type="file" + accept="image/*" + onChange={handleFileChange} + data-testid="image-file-input" + className="w-full border border-[#ccc] bg-white p-2" + /> + </div> + </div> + + <div className="relative mb-4 flex h-[350px] w-full items-center justify-center overflow-hidden rounded border"> + {previewUrl ? ( + <Image + src={previewUrl} + alt="image preview" + fill + style={{ objectFit: 'contain' }} + className="rounded" + data-testid="layer-image-preview" + /> + ) : ( + <div className="text-gray-500">No Image</div> + )} + </div> + + <Button + type="button" + onClick={onSubmit} + className="w-full justify-center text-base font-medium" + > + Submit + </Button> + </div> + ); +}; diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.styles.css b/src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.styles.css similarity index 100% rename from src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.styles.css rename to src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.styles.css diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectModal/index.ts b/src/components/FunctionalArea/Modal/LayerImageObjectModal/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a3553301fa513994c27b4a121e26440028d9478 --- /dev/null +++ b/src/components/FunctionalArea/Modal/LayerImageObjectModal/index.ts @@ -0,0 +1,2 @@ +export { LayerImageObjectFactoryModal } from './LayerImageObjectFactoryModal.component'; +export { LayerImageObjectEditFactoryModal } from './LayerImageObjectEditFactoryModal.component'; diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx index feec78d941d8fb4fcee1bd192539a001907a46af..79a6ac8dd3fb1a8340c083b9aa0f41a67c9c4a07 100644 --- a/src/components/FunctionalArea/Modal/Modal.component.tsx +++ b/src/components/FunctionalArea/Modal/Modal.component.tsx @@ -5,7 +5,10 @@ import { AccessDeniedModal } from '@/components/FunctionalArea/Modal/AccessDenie import { AddCommentModal } from '@/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component'; import { LicenseModal } from '@/components/FunctionalArea/Modal/LicenseModal'; import { ToSModal } from '@/components/FunctionalArea/Modal/ToSModal/ToSModal.component'; -import { LayerImageObjectFactoryModal } from '@/components/FunctionalArea/Modal/LayerImageObjectFactoryModal'; +import { + LayerImageObjectEditFactoryModal, + LayerImageObjectFactoryModal, +} from '@/components/FunctionalArea/Modal/LayerImageObjectModal'; import { EditOverlayModal } from './EditOverlayModal'; import { LoginModal } from './LoginModal'; import { ErrorReportModal } from './ErrorReportModal'; @@ -91,6 +94,11 @@ export const Modal = (): React.ReactNode => { <LayerImageObjectFactoryModal /> </ModalLayout> )} + {isOpen && modalName === 'layer-image-object-edit-factory' && ( + <ModalLayout> + <LayerImageObjectEditFactoryModal /> + </ModalLayout> + )} </> ); }; diff --git a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx index 64816f0bda36c4261fc2735a9c397ba215b16590..56e3afbcc7e9fee6099345b56f5603c68de81a24 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-image-object-factory' && 'h-auto w-[800px]', + ['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/MapDrawActions/MapDrawActions.component.tsx b/src/components/Map/MapDrawActions/MapDrawActions.component.tsx index e7a0139374792c66da09cc71af07b038a0752be9..43b51bd237b61dc6e5cc3d6ecb56435280b9dcfd 100644 --- a/src/components/Map/MapDrawActions/MapDrawActions.component.tsx +++ b/src/components/Map/MapDrawActions/MapDrawActions.component.tsx @@ -5,6 +5,7 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { mapEditToolsActiveActionSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { MapDrawActionsButton } from '@/components/Map/MapDrawActions/MapDrawActionsButton.component'; +import { MapDrawEditActionsComponent } from '@/components/Map/MapDrawActions/MapDrawEditActions.component'; import { useMemo } from 'react'; import { layersForCurrentModelSelector, @@ -30,18 +31,16 @@ export const MapDrawActions = (): React.JSX.Element | null => { } return ( - <div className="absolute right-6 top-[calc(64px+40px+144px)] z-10 flex flex-col gap-4"> + <div className="absolute right-6 top-[calc(64px+40px+144px)] z-10 flex flex-col items-end gap-4"> <MapDrawActionsButton isActive={activeAction === MAP_EDIT_ACTIONS.DRAW_IMAGE} toggleMapEditAction={() => toggleMapEditAction(MAP_EDIT_ACTIONS.DRAW_IMAGE)} icon="image" title="Draw image" /> - <MapDrawActionsButton + <MapDrawEditActionsComponent isActive={activeAction === MAP_EDIT_ACTIONS.TRANSFORM_IMAGE} toggleMapEditAction={() => toggleMapEditAction(MAP_EDIT_ACTIONS.TRANSFORM_IMAGE)} - icon="resize-image" - title="Transform image" /> </div> ); diff --git a/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx b/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..153e93ad7fdf79e3fe66c9f2b64844cd206b80f3 --- /dev/null +++ b/src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx @@ -0,0 +1,50 @@ +/* eslint-disable no-magic-numbers */ +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { mapEditToolsLayerImageObjectSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { MapDrawActionsButton } from '@/components/Map/MapDrawActions/MapDrawActionsButton.component'; +import { openLayerImageObjectEditFactoryModal } from '@/redux/modal/modal.slice'; + +type MapDrawEditActionsComponentProps = { + toggleMapEditAction: () => void; + isActive: boolean; +}; + +export const MapDrawEditActionsComponent = ({ + toggleMapEditAction, + isActive, +}: MapDrawEditActionsComponentProps): React.JSX.Element => { + const layerImageObject = useAppSelector(mapEditToolsLayerImageObjectSelector); + const dispatch = useAppDispatch(); + + const editMapObject = (): void => { + dispatch(openLayerImageObjectEditFactoryModal()); + }; + + return ( + <div className="flex flex-row-reverse gap-4"> + <MapDrawActionsButton + isActive={isActive} + toggleMapEditAction={toggleMapEditAction} + icon="pencil" + title="Edit image" + /> + {layerImageObject && ( + <> + <MapDrawActionsButton + isActive={false} + toggleMapEditAction={() => editMapObject()} + icon="edit-image" + title="Edit image" + /> + <MapDrawActionsButton + isActive={false} + toggleMapEditAction={() => {}} + icon="trash" + title="Remove image" + /> + </> + )} + </div> + ); +}; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index ada5b18ea5048e2a90b37f74aae6c2cdcf19375b..04657abc49b57fedad69bc815c5c84c56e89020f 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -27,6 +27,7 @@ import { mapEditToolsActiveActionSelector } from '@/redux/mapEditTools/mapEditTo import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; import { Extent } from 'ol/extent'; import getTransformImageInteraction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction'; +import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; export const useOlMapAdditionalLayers = ( mapInstance: MapInstance, @@ -146,9 +147,10 @@ export const useOlMapAdditionalLayers = ( } mapInstance?.addInteraction(transformInteraction); return () => { + dispatch(mapEditToolsSetLayerObject(null)); mapInstance?.removeInteraction(transformInteraction); }; - }, [activeAction, activeLayer, mapInstance, transformInteraction, vectorRendering]); + }, [activeAction, activeLayer, dispatch, mapInstance, transformInteraction, vectorRendering]); useEffect(() => { if (!drawImageInteraction) { diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts index 53f71dc9c250d3ff04e593ece5a65b1b9440f115..f2046aebdf8fd863b8f7d0ef1120d5e6c7a2949c 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts @@ -155,30 +155,14 @@ export default class Glyph { this.feature.set('setCoordinates', this.setCoordinates.bind(this)); this.feature.set('getGlyphData', this.getGlyphData.bind(this)); this.feature.set('reset', this.reset.bind(this)); + this.feature.set('setGlyph', this.setGlyph.bind(this)); + this.feature.setId(this.elementId); this.feature.setStyle(this.getStyle.bind(this)); if (!this.glyphId) { return; } - const img = new Image(); - img.onload = (): void => { - this.imageWidth = img.naturalWidth; - this.imageHeight = img.naturalHeight; - const imagePoint1 = this.pointToProjection({ x: 0, y: 0 }); - const imagePoint2 = this.pointToProjection({ x: this.imageWidth, y: this.imageHeight }); - this.imageWidthOnMap = Math.abs(imagePoint2[0] - imagePoint1[0]); - this.imageHeightOnMap = Math.abs(imagePoint2[1] - imagePoint1[1]); - this.setImageScaleAndDimensions(this.heightOnMap, this.widthOnMap); - this.style = new Style({ - image: new Icon({ - anchor: [0, 0], - img, - size: [this.imageWidth, this.imageHeight], - }), - zIndex: this.zIndex, - }); - }; - img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(this.glyphId)}`; + this.setGlyph(this.glyphId); } private drawPolygon(): void { @@ -225,6 +209,29 @@ export default class Glyph { } } + private setGlyph(glyph: number): void { + const img = new Image(); + img.onload = (): void => { + this.imageWidth = img.naturalWidth; + this.imageHeight = img.naturalHeight; + const imagePoint1 = this.pointToProjection({ x: 0, y: 0 }); + const imagePoint2 = this.pointToProjection({ x: this.imageWidth, y: this.imageHeight }); + this.imageWidthOnMap = Math.abs(imagePoint2[0] - imagePoint1[0]); + this.imageHeightOnMap = Math.abs(imagePoint2[1] - imagePoint1[1]); + this.setImageScaleAndDimensions(this.heightOnMap, this.widthOnMap); + this.style = new Style({ + image: new Icon({ + anchor: [0, 0], + img, + size: [this.imageWidth, this.imageHeight], + }), + zIndex: this.zIndex, + }); + }; + img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(glyph)}`; + this.glyphId = glyph; + } + private getGlyphData(): LayerImage { return { id: this.elementId, diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts index 20041fa337ce2d8d6fe2c27215081cafbdfe4bfe..8bb46cfee9e6e18cbdaa3620648bd782e2207d78 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts @@ -10,6 +10,7 @@ import { layerUpdateImage } from '@/redux/layers/layers.slice'; import getBoundingBoxFromExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent'; import { MapSize } from '@/redux/map/map.types'; import { Extent } from 'ol/extent'; +import { mapEditToolsSetLayerObject } from '@/redux/mapEditTools/mapEditTools.slice'; export default function getTransformImageInteraction( dispatch: AppDispatch, @@ -88,6 +89,20 @@ export default function getTransformImageInteraction( } }); + transform.on('select', event => { + const transformEvent = event as unknown as { features: Collection<Feature> }; + const { features } = transformEvent; + if (!features.getLength()) { + dispatch(mapEditToolsSetLayerObject(null)); + return; + } + const getGlyphData = features.item(0).get('getGlyphData'); + if (getGlyphData && getGlyphData instanceof Function) { + const glyphData = getGlyphData(); + dispatch(mapEditToolsSetLayerObject(glyphData)); + } + }); + transform.on(['scaleend', 'translateend'], async (event: BaseEvent | Event): Promise<void> => { const transformEvent = event as unknown as { feature: Feature }; const { feature } = transformEvent; diff --git a/src/redux/mapEditTools/mapEditTools.mock.ts b/src/redux/mapEditTools/mapEditTools.mock.ts index 81dd081244074969ac072b418d32b62a2671e2b5..d6fe529ca573712859930af2879cf0591ddb7b50 100644 --- a/src/redux/mapEditTools/mapEditTools.mock.ts +++ b/src/redux/mapEditTools/mapEditTools.mock.ts @@ -2,4 +2,5 @@ import { MapEditToolsState } from '@/redux/mapEditTools/mapEditTools.types'; export const MAP_EDIT_TOOLS_STATE_INITIAL_MOCK: MapEditToolsState = { activeAction: null, + layerImageObject: null, }; diff --git a/src/redux/mapEditTools/mapEditTools.reducers.ts b/src/redux/mapEditTools/mapEditTools.reducers.ts index 01334ba6aa845df55bc2436490fda314bb8d211d..ce150602d346927bee17c9f8be85421ff5dbadc1 100644 --- a/src/redux/mapEditTools/mapEditTools.reducers.ts +++ b/src/redux/mapEditTools/mapEditTools.reducers.ts @@ -2,6 +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'; export const mapEditToolsSetActiveActionReducer = ( state: MapEditToolsState, @@ -13,3 +14,10 @@ export const mapEditToolsSetActiveActionReducer = ( state.activeAction = null; } }; + +export const mapEditToolsSetLayerObjectReducer = ( + state: MapEditToolsState, + action: PayloadAction<LayerImage | null>, +): void => { + state.layerImageObject = action.payload; +}; diff --git a/src/redux/mapEditTools/mapEditTools.selectors.ts b/src/redux/mapEditTools/mapEditTools.selectors.ts index ed51e7ab48c7c878d83fd6f2afb4617bb823d705..2b29bd35c204f8f06d6459d2fb7c89429e53a82a 100644 --- a/src/redux/mapEditTools/mapEditTools.selectors.ts +++ b/src/redux/mapEditTools/mapEditTools.selectors.ts @@ -9,6 +9,11 @@ export const mapEditToolsActiveActionSelector = createSelector( state => state.activeAction, ); +export const mapEditToolsLayerImageObjectSelector = createSelector( + mapEditToolsSelector, + state => state.layerImageObject, +); + export const isMapEditToolsActiveSelector = createSelector(mapEditToolsSelector, state => Boolean(state.activeAction), ); diff --git a/src/redux/mapEditTools/mapEditTools.slice.ts b/src/redux/mapEditTools/mapEditTools.slice.ts index bea57d9cda02e1cc7ca20039cb296f23700bb8dd..2db0001c938bc11f5469fb5addfc167d639680c3 100644 --- a/src/redux/mapEditTools/mapEditTools.slice.ts +++ b/src/redux/mapEditTools/mapEditTools.slice.ts @@ -1,15 +1,19 @@ import { createSlice } from '@reduxjs/toolkit'; import { MAP_EDIT_TOOLS_STATE_INITIAL_MOCK } from '@/redux/mapEditTools/mapEditTools.mock'; -import { mapEditToolsSetActiveActionReducer } from '@/redux/mapEditTools/mapEditTools.reducers'; +import { + mapEditToolsSetActiveActionReducer, + mapEditToolsSetLayerObjectReducer, +} from '@/redux/mapEditTools/mapEditTools.reducers'; export const layersSlice = createSlice({ name: 'layers', initialState: MAP_EDIT_TOOLS_STATE_INITIAL_MOCK, reducers: { mapEditToolsSetActiveAction: mapEditToolsSetActiveActionReducer, + mapEditToolsSetLayerObject: mapEditToolsSetLayerObjectReducer, }, }); -export const { mapEditToolsSetActiveAction } = layersSlice.actions; +export const { mapEditToolsSetActiveAction, mapEditToolsSetLayerObject } = layersSlice.actions; export default layersSlice.reducer; diff --git a/src/redux/mapEditTools/mapEditTools.types.ts b/src/redux/mapEditTools/mapEditTools.types.ts index 8a000d1d076a6d7e194eec4bfd1c22d36672d15f..e141e6b86f35356c891bf47320a1784b945ee525 100644 --- a/src/redux/mapEditTools/mapEditTools.types.ts +++ b/src/redux/mapEditTools/mapEditTools.types.ts @@ -1,5 +1,7 @@ import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; +import { LayerImage } from '@/types/models'; export type MapEditToolsState = { activeAction: keyof typeof MAP_EDIT_ACTIONS | null; + layerImageObject: LayerImage | null; }; diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts index f678ea91f3f7524626b107a413d7766c3bcd5a20..a7e1c77421c7d7fe376c096940294cf5add16591 100644 --- a/src/redux/modal/modal.reducers.ts +++ b/src/redux/modal/modal.reducers.ts @@ -153,3 +153,9 @@ export const openLayerImageObjectFactoryModalReducer = ( state.modalName = 'layer-image-object-factory'; state.modalTitle = 'Select glyph or upload file'; }; + +export const openLayerImageObjectEditFactoryModalReducer = (state: ModalState): void => { + state.isOpen = true; + state.modalName = 'layer-image-object-edit-factory'; + state.modalTitle = 'Edit layer image object'; +}; diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts index a9baf72a027e686a80e91491679078dace605c9f..82da41c6d082eab0891a5bc6099609dc9cf379aa 100644 --- a/src/redux/modal/modal.slice.ts +++ b/src/redux/modal/modal.slice.ts @@ -18,6 +18,7 @@ import { openToSModalReducer, openLayerFactoryModalReducer, openLayerImageObjectFactoryModalReducer, + openLayerImageObjectEditFactoryModalReducer, } from './modal.reducers'; const modalSlice = createSlice({ @@ -41,6 +42,7 @@ const modalSlice = createSlice({ openToSModal: openToSModalReducer, openLayerFactoryModal: openLayerFactoryModalReducer, openLayerImageObjectFactoryModal: openLayerImageObjectFactoryModalReducer, + openLayerImageObjectEditFactoryModal: openLayerImageObjectEditFactoryModalReducer, }, }); @@ -62,6 +64,7 @@ export const { openToSModal, openLayerFactoryModal, openLayerImageObjectFactoryModal, + openLayerImageObjectEditFactoryModal, } = modalSlice.actions; export default modalSlice.reducer; diff --git a/src/shared/Autocomplete/Autocomplete.component.tsx b/src/shared/Autocomplete/Autocomplete.component.tsx index 232dca1940bbb8eb595bfeeaf633054af91e38c8..aa602259d4cca15333e5cfc47edaad7f82b27a05 100644 --- a/src/shared/Autocomplete/Autocomplete.component.tsx +++ b/src/shared/Autocomplete/Autocomplete.component.tsx @@ -8,6 +8,7 @@ type AutocompleteProps<T> = { labelKey?: keyof T; placeholder?: string; onChange: (value: T | null) => void; + initialValue?: T | null; }; type OptionType<T> = { @@ -22,6 +23,7 @@ export const Autocomplete = <T,>({ labelKey = 'label' as keyof T, placeholder = 'Select...', onChange, + initialValue = null, }: AutocompleteProps<T>): React.JSX.Element => { const formattedOptions = options.map(option => ({ value: option[valueKey], @@ -29,13 +31,24 @@ export const Autocomplete = <T,>({ originalOption: option, })); + const initialFormattedValue = React.useMemo(() => { + if (!initialValue) { + return null; + } + return ( + formattedOptions.find(option => option.originalOption[valueKey] === initialValue[valueKey]) || + null + ); + }, [initialValue, valueKey, labelKey, formattedOptions]); + const handleChange = (selectedOption: SingleValue<OptionType<T>>): void => { onChange(selectedOption ? selectedOption.originalOption : null); }; return ( <div data-testid="autocomplete"> - <Select + <Select<OptionType<T>> + value={initialFormattedValue} options={formattedOptions} onChange={handleChange} placeholder={placeholder} diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx index 4d888fc27857a3659b614a2f777c9852b0e20901..e9cf537cf1761891f28c7ca443f757524c78e3a7 100644 --- a/src/shared/Icon/Icon.component.tsx +++ b/src/shared/Icon/Icon.component.tsx @@ -20,6 +20,9 @@ import type { IconComponentType, IconTypes } from '@/types/iconTypes'; import { DownloadIcon } from '@/shared/Icon/Icons/DownloadIcon'; import { ImageIcon } from '@/shared/Icon/Icons/ImageIcon'; import { ResizeImageIcon } from '@/shared/Icon/Icons/ResizeImageIcon'; +import { PencilIcon } from '@/shared/Icon/Icons/PencilIcon'; +import { EditImageIcon } from '@/shared/Icon/Icons/EditImageIcon'; +import { TrashIcon } from '@/shared/Icon/Icons/TrashIcon'; import { LocationIcon } from './Icons/LocationIcon'; import { MaginfierZoomInIcon } from './Icons/MagnifierZoomIn'; import { MaginfierZoomOutIcon } from './Icons/MagnifierZoomOut'; @@ -63,6 +66,9 @@ const icons: Record<IconTypes, IconComponentType> = { 'manage-user': ManageUserIcon, image: ImageIcon, 'resize-image': ResizeImageIcon, + 'edit-image': EditImageIcon, + trash: TrashIcon, + pencil: PencilIcon, } as const; export const Icon = ({ name, className = '', ...rest }: IconProps): JSX.Element => { diff --git a/src/shared/Icon/Icons/EditImageIcon.tsx b/src/shared/Icon/Icons/EditImageIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7cf8046996760387ef6167a4ed59fdba27ae1fc3 --- /dev/null +++ b/src/shared/Icon/Icons/EditImageIcon.tsx @@ -0,0 +1,29 @@ +interface EditImageIconProps { + className?: string; +} + +export const EditImageIcon = ({ className }: EditImageIconProps): JSX.Element => ( + <svg + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + className={className} + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M21.2 6.4L12 15.6C11.2 16.4 8.8 16.8 8.3 16.3C7.8 15.8 8.2 13.4 9 12.6L18.4 3.2C18.6 3 18.8 2.8 19.1 2.7C19.4 2.5 19.7 2.4 20.1 2.4C20.4 2.4 20.7 2.5 21 2.7C21.3 2.9 21.5 3.1 21.7 3.4C21.9 3.7 22 4 22 4.4C22 4.7 21.9 5 21.7 5.3C21.5 5.6 21.3 5.8 21.2 6L21.2 6.4Z" + stroke="currentColor" + strokeWidth="1.5" + strokeLinecap="square" + strokeLinejoin="miter" + /> + <path + d="M11 4H6C5 4 4.2 4.4 3.6 5C3 5.6 2.6 6.4 2.6 8V18C2.6 19 3 19.8 3.6 20.4C4.2 21 5 21.4 6 21.4H17C18.8 21.4 19.6 20.4 19.6 18V13" + stroke="currentColor" + strokeWidth="1.5" + strokeLinecap="square" + strokeLinejoin="miter" + /> + </svg> +); diff --git a/src/shared/Icon/Icons/PencilIcon.tsx b/src/shared/Icon/Icons/PencilIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7d86605f1bf5d8f8b11862775a7dfc445d43eb2a --- /dev/null +++ b/src/shared/Icon/Icons/PencilIcon.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +interface PencilIconProps { + className?: string; +} + +export const PencilIcon = ({ className }: PencilIconProps): JSX.Element => ( + <svg + className={className} + fill="currentColor" + height="24" + width="24" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 306.637 306.637" + aria-hidden="true" + role="img" + strokeWidth="2" + > + <g> + <path + d="M12.809,238.52L0,306.637l68.118-12.809l184.277-184.277l-55.309-55.309L12.809,238.52z M60.79,279.943l-41.992,7.896 + l7.896-41.992L197.086,75.455l34.096,34.096L60.79,279.943z" + /> + <path + d="M251.329,0l-41.507,41.507l55.308,55.308l41.507-41.507L251.329,0z M231.035,41.507l20.294-20.294l34.095,34.095 + L265.13,75.602L231.035,41.507z" + /> + </g> + </svg> +); diff --git a/src/shared/Icon/Icons/TrashIcon.tsx b/src/shared/Icon/Icons/TrashIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cf0b83b7965f17dbe37a1e20711dc3ef48d77a2c --- /dev/null +++ b/src/shared/Icon/Icons/TrashIcon.tsx @@ -0,0 +1,30 @@ +interface TrashIconProps { + className?: string; +} + +export const TrashIcon = ({ className }: TrashIconProps): JSX.Element => ( + <svg + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + className={className} + xmlns="http://www.w3.org/2000/svg" + > + <path d="M7 6H17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> + <path d="M9 4H15V6H9V4Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" /> + <rect + x="6" + y="6" + width="12" + height="14" + rx="1" + stroke="currentColor" + strokeWidth="1.5" + fill="none" + /> + <path d="M9 10V16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> + <path d="M12 10V16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> + <path d="M15 10V16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> + </svg> +); diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts index 469b7699a5d3afc72da33a27ad6959ae8cbc7281..dc1ee2b11f2cabe00fd84164c4ea43910f1ef9b0 100644 --- a/src/types/iconTypes.ts +++ b/src/types/iconTypes.ts @@ -26,6 +26,9 @@ export type IconTypes = | 'download' | 'question' | 'image' - | 'resize-image'; + | 'resize-image' + | 'edit-image' + | 'trash' + | 'pencil'; export type IconComponentType = ({ className }: { className: string }) => JSX.Element; diff --git a/src/types/modal.ts b/src/types/modal.ts index edf1c858843a5573baf94dd44b19eefe47743026..88d3dcd3ccdd278145bfafc4b5b7d68836ed48c8 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -13,4 +13,5 @@ export type ModalName = | 'terms-of-service' | 'logged-in-menu' | 'layer-factory' - | 'layer-image-object-factory'; + | 'layer-image-object-factory' + | 'layer-image-object-edit-factory';