From 7d9603f3b9c55c70b212e818869ca7b769181a79 Mon Sep 17 00:00:00 2001 From: Anikesh Suresh Date: Wed, 11 Dec 2024 10:09:04 +0000 Subject: [PATCH 01/14] Implemented Edit Dialog for Images TODO: merge develop, and implement testing #1075 --- src/api/api.types.tsx | 8 +- src/api/images.tsx | 74 +++++++ .../images/editImageDialog.component.tsx | 191 ++++++++++++++++++ src/form.schemas.tsx | 11 + 4 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 src/api/images.tsx create mode 100644 src/common/images/editImageDialog.component.tsx diff --git a/src/api/api.types.tsx b/src/api/api.types.tsx index d137c843c..2cd5ebad2 100644 --- a/src/api/api.types.tsx +++ b/src/api/api.types.tsx @@ -250,10 +250,16 @@ export interface ImagePost { description?: string | null; } -export interface Image +export type ImagePatch = Partial; + +export interface APIImage extends Required>, CreatedModifiedMixin { id: string; primary: boolean; thumbnail_base64: string; } + +export interface APIImageWithURL extends APIImage { + url: string; +} diff --git a/src/api/images.tsx b/src/api/images.tsx new file mode 100644 index 000000000..da18381e9 --- /dev/null +++ b/src/api/images.tsx @@ -0,0 +1,74 @@ +import { + useMutation, + UseMutationResult, + useQuery, + useQueryClient, + UseQueryResult, +} from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { storageApi } from './api'; +import { APIImage, APIImageWithURL, ImagePatch } from './api.types'; + +export const getImage = async (id: string): Promise => { + return storageApi.get(`/images/${id}`).then((response) => { + return response.data; + }); +}; + +export const useGetImage = ( + id: string +): UseQueryResult => { + return useQuery({ + queryKey: ['Image', id], + queryFn: () => getImage(id), + }); +}; + +const getImages = async ( + entityId: string, + primary?: boolean +): Promise => { + const queryParams = new URLSearchParams(); + queryParams.append('entity_id', entityId); + + if (primary !== undefined) queryParams.append('primary', String(primary)); + return storageApi + .get(`/images`, { + params: queryParams, + }) + .then((response) => response.data); +}; + +export const useGetImages = ( + entityId?: string, + primary?: boolean +): UseQueryResult => { + return useQuery({ + queryKey: ['Images', entityId, primary], + queryFn: () => getImages(entityId ?? '', primary), + enabled: !!entityId, + }); +}; + +const patchImage = async (id: string, image: ImagePatch): Promise => { + return storageApi + .patch(`/images/${id}`, image) + .then((response) => response.data); +}; + +export const usePatchImage = (): UseMutationResult< + APIImage, + AxiosError, + { id: string; image: ImagePatch } +> => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, image }) => patchImage(id, image), + onSuccess: (updatedImage: APIImage) => { + queryClient.invalidateQueries({ queryKey: ['Images'] }); + queryClient.invalidateQueries({ + queryKey: ['Image', updatedImage.id], + }); + }, + }); +}; diff --git a/src/common/images/editImageDialog.component.tsx b/src/common/images/editImageDialog.component.tsx new file mode 100644 index 000000000..10b850418 --- /dev/null +++ b/src/common/images/editImageDialog.component.tsx @@ -0,0 +1,191 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { + Box, + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormHelperText, + Grid, + TextField, +} from '@mui/material'; +import { useForm } from 'react-hook-form'; + +import React from 'react'; + +import { AxiosError } from 'axios'; +import { APIImage, ImagePatch } from '../../api/api.types'; +import { usePatchImage } from '../../api/images'; +import { ImagesSchema } from '../../form.schemas'; +import handleIMS_APIError from '../../handleIMS_APIError'; + +export interface ImageDialogProps { + open: boolean; + onClose: () => void; + selectedImage: APIImage; +} + +const editImageDialog = (props: ImageDialogProps) => { + const { open, onClose, selectedImage } = props; + + const { mutateAsync: patchImage, isPending: isEditPending } = usePatchImage(); + + const initalImage: ImagePatch = React.useMemo( + () => selectedImage, + [selectedImage] + ); + + const { + handleSubmit, + register, + formState: { errors }, + watch, + setError, + clearErrors, + reset, + } = useForm({ + resolver: zodResolver(ImagesSchema('patch')), + defaultValues: initalImage, + }); + + // Load the values for editing + React.useEffect(() => { + reset(initalImage); + }, [initalImage, reset]); + + // Clears form errors when a value has been changed + React.useEffect(() => { + if (errors.root?.formError) { + const subscription = watch(() => clearErrors('root.formError')); + return () => subscription.unsubscribe(); + } + }, [clearErrors, errors, selectedImage, watch]); + + const handleClose = React.useCallback(() => { + reset(); + clearErrors(); + onClose(); + }, [clearErrors, onClose, reset]); + + const handleEditImage = React.useCallback( + (imageData: ImagePatch) => { + if (selectedImage) { + const isFileNameUpdated = + imageData.file_name !== selectedImage.file_name; + + const isDescriptionUpdated = + imageData.description !== selectedImage.description; + + const isTitleUpdated = imageData.title !== selectedImage.title; + + let imageToEdit: ImagePatch = {}; + + if (isFileNameUpdated) imageToEdit.file_name = imageData.file_name; + if (isDescriptionUpdated) + imageToEdit.description = imageData.description; + if (isTitleUpdated) imageToEdit.title = imageData.title; + + if (isFileNameUpdated || isDescriptionUpdated || isTitleUpdated) { + patchImage({ + id: selectedImage.id, + image: imageToEdit, + }) + .then(() => handleClose()) + .catch((error: AxiosError) => { + handleIMS_APIError(error); + }); + } else { + setError('root.formError', { + message: + "There have been no changes made. Please change a field's value or press Cancel to exit.", + }); + } + } + }, + [selectedImage, patchImage, handleClose, setError] + ); + + const onSubmit = (data: ImagePatch) => { + handleEditImage(data); + }; + + return ( + + {`Edit Image`} + + + + + + + + + + + + + + + + + + + + {errors.root?.formError && ( + + {errors.root?.formError.message} + + )} + + + ); +}; + +export default editImageDialog; diff --git a/src/form.schemas.tsx b/src/form.schemas.tsx index 9efc6c3a5..f88281ca3 100644 --- a/src/form.schemas.tsx +++ b/src/form.schemas.tsx @@ -614,3 +614,14 @@ export const ItemDetailsStepSchema = (requestType: RequestType) => { notes: OptionalOrNullableStringSchema({ requestType }), }); }; + +// ------------------------------------ IMAGES ------------------------------------ + +export const ImagesSchema = (requestType: RequestType) => + z.object({ + fileName: MandatoryStringSchema({ + errorMessage: 'Please enter a file name.', + }), + title: OptionalOrNullableStringSchema({ requestType }), + description: OptionalOrNullableStringSchema({ requestType }), + }); From 157ed0fcb5ca775d0e69fc68ec70444c1b64b8d9 Mon Sep 17 00:00:00 2001 From: Anikesh Suresh Date: Wed, 11 Dec 2024 14:33:19 +0000 Subject: [PATCH 02/14] connect edit dialog to menuitem TODO testing #1075 --- .../images/editImageDialog.component.tsx | 10 +++----- src/common/images/imageGallery.component.tsx | 23 +++++++++++++++---- src/form.schemas.tsx | 2 +- src/mocks/handlers.ts | 22 ++++++++++++++++++ 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/common/images/editImageDialog.component.tsx b/src/common/images/editImageDialog.component.tsx index 10b850418..627a02486 100644 --- a/src/common/images/editImageDialog.component.tsx +++ b/src/common/images/editImageDialog.component.tsx @@ -27,7 +27,7 @@ export interface ImageDialogProps { selectedImage: APIImage; } -const editImageDialog = (props: ImageDialogProps) => { +const EditImageDialog = (props: ImageDialogProps) => { const { open, onClose, selectedImage } = props; const { mutateAsync: patchImage, isPending: isEditPending } = usePatchImage(); @@ -107,10 +107,6 @@ const editImageDialog = (props: ImageDialogProps) => { [selectedImage, patchImage, handleClose, setError] ); - const onSubmit = (data: ImagePatch) => { - handleEditImage(data); - }; - return ( {`Edit Image`} @@ -171,7 +167,7 @@ const editImageDialog = (props: ImageDialogProps) => {