From c264f607e31ad3b7d70e6fba5aaf333a8a42540b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Grocholewski?= <m.grocholewski@atcomp.pl> Date: Thu, 16 Jan 2025 12:44:45 +0100 Subject: [PATCH] feat(layer-image): add glyph editing for layer image objects --- .../LayerImageObjectFactoryModal/index.ts | 1 - ...eObjectEditFactoryModal.component.test.tsx | 173 ++++++++++++++++++ ...rImageObjectEditFactoryModal.component.tsx | 98 ++++++++++ ...ImageObjectFactoryModal.component.test.tsx | 0 ...LayerImageObjectFactoryModal.component.tsx | 108 ++--------- .../LayerImageObjectForm.component.tsx | 121 ++++++++++++ .../LayerImageObjectForm.styles.css} | 0 .../Modal/LayerImageObjectModal/index.ts | 2 + .../FunctionalArea/Modal/Modal.component.tsx | 10 +- .../ModalLayout/ModalLayout.component.tsx | 3 +- .../MapDrawActions.component.tsx | 7 +- .../MapDrawEditActions.component.tsx | 50 +++++ .../useOlMapAdditionalLayers.ts | 4 +- .../utils/shapes/elements/Glyph.ts | 45 +++-- .../layer/getTransformImageInteraction.ts | 15 ++ src/redux/mapEditTools/mapEditTools.mock.ts | 1 + .../mapEditTools/mapEditTools.reducers.ts | 8 + .../mapEditTools/mapEditTools.selectors.ts | 5 + src/redux/mapEditTools/mapEditTools.slice.ts | 8 +- src/redux/mapEditTools/mapEditTools.types.ts | 2 + src/redux/modal/modal.reducers.ts | 6 + src/redux/modal/modal.slice.ts | 3 + .../Autocomplete/Autocomplete.component.tsx | 15 +- src/shared/Icon/Icon.component.tsx | 6 + src/shared/Icon/Icons/EditImageIcon.tsx | 29 +++ src/shared/Icon/Icons/PencilIcon.tsx | 30 +++ src/shared/Icon/Icons/TrashIcon.tsx | 30 +++ src/types/iconTypes.ts | 5 +- src/types/modal.ts | 3 +- 29 files changed, 659 insertions(+), 129 deletions(-) delete mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.test.tsx create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectEditFactoryModal.component.tsx rename src/components/FunctionalArea/Modal/{LayerImageObjectFactoryModal => LayerImageObjectModal}/LayerImageObjectFactoryModal.component.test.tsx (100%) rename src/components/FunctionalArea/Modal/{LayerImageObjectFactoryModal => LayerImageObjectModal}/LayerImageObjectFactoryModal.component.tsx (51%) create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectModal/LayerImageObjectForm.component.tsx rename src/components/FunctionalArea/Modal/{LayerImageObjectFactoryModal/LayerImageObjectFactoryModal.styles.css => LayerImageObjectModal/LayerImageObjectForm.styles.css} (100%) create mode 100644 src/components/FunctionalArea/Modal/LayerImageObjectModal/index.ts create mode 100644 src/components/Map/MapDrawActions/MapDrawEditActions.component.tsx create mode 100644 src/shared/Icon/Icons/EditImageIcon.tsx create mode 100644 src/shared/Icon/Icons/PencilIcon.tsx create mode 100644 src/shared/Icon/Icons/TrashIcon.tsx diff --git a/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts b/src/components/FunctionalArea/Modal/LayerImageObjectFactoryModal/index.ts deleted file mode 100644 index 11947806..00000000 --- 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 00000000..8095d60e --- /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 00000000..589b3a14 --- /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 b750d061..98bc5fc5 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 00000000..58a6efac --- /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 00000000..7a355330 --- /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 feec78d9..79a6ac8d 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 64816f0b..56e3afbc 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 e7a01393..43b51bd2 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 00000000..153e93ad --- /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 ada5b18e..04657abc 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 53f71dc9..f2046aeb 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 20041fa3..8bb46cfe 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 81dd0812..d6fe529c 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 01334ba6..ce150602 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 ed51e7ab..2b29bd35 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 bea57d9c..2db0001c 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 8a000d1d..e141e6b8 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 f678ea91..a7e1c774 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 a9baf72a..82da41c6 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 232dca19..aa602259 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 4d888fc2..e9cf537c 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 00000000..7cf80469 --- /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 00000000..7d86605f --- /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 00000000..cf0b83b7 --- /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 469b7699..dc1ee2b1 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 edf1c858..88d3dcd3 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'; -- GitLab