Skip to content

Commit

Permalink
generalise edit image dialog for attachemnts #1075
Browse files Browse the repository at this point in the history
  • Loading branch information
asuresh-code committed Dec 16, 2024
1 parent 9fccbfa commit 5d3f5a7
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 65 deletions.
6 changes: 5 additions & 1 deletion src/api/api.types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,11 @@ export interface ImagePost {
description?: string | null;
}

export type ImagePatch = Partial<ImagePost>;
export interface ObjectFilePatch {
file_name?: string;
title?: string | null;
description?: string | null;
}

export interface APIImage
extends Required<Omit<ImagePost, 'upload_file'>>,
Expand Down
4 changes: 2 additions & 2 deletions src/api/images.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { renderHook, waitFor } from '@testing-library/react';
import ImagesJSON from '../mocks/Images.json';
import { hooksWrapperWithProviders } from '../testUtils';
import { ImagePatch } from './api.types';
import { ObjectFilePatch } from './api.types';
import { useGetImage, useGetImages, usePatchImage } from './images';

describe('images api functions', () => {
Expand Down Expand Up @@ -65,7 +65,7 @@ describe('images api functions', () => {
});

describe('usePatchImage', () => {
let mockDataPatch: ImagePatch;
let mockDataPatch: ObjectFilePatch;
beforeEach(() => {
mockDataPatch = {
file_name: 'edited_image.jpeg',
Expand Down
13 changes: 8 additions & 5 deletions src/api/images.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { storageApi } from './api';
import { APIImage, APIImageWithURL, ImagePatch } from './api.types';
import { APIImage, APIImageWithURL, ObjectFilePatch } from './api.types';

export const getImage = async (id: string): Promise<APIImageWithURL> => {
return storageApi.get(`/images/${id}`).then((response) => {
Expand Down Expand Up @@ -50,20 +50,23 @@ export const useGetImages = (
});
};

const patchImage = async (id: string, image: ImagePatch): Promise<APIImage> => {
const patchImage = async (
id: string,
fileMetadata: ObjectFilePatch
): Promise<APIImage> => {
return storageApi
.patch<APIImage>(`/images/${id}`, image)
.patch<APIImage>(`/images/${id}`, fileMetadata)
.then((response) => response.data);
};

export const usePatchImage = (): UseMutationResult<
APIImage,
AxiosError,
{ id: string; image: ImagePatch }
{ id: string; fileMetadata: ObjectFilePatch }
> => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, image }) => patchImage(id, image),
mutationFn: ({ id, fileMetadata }) => patchImage(id, fileMetadata),
onSuccess: (updatedImage: APIImage) => {
queryClient.invalidateQueries({ queryKey: ['Images'] });
queryClient.invalidateQueries({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,37 @@ import userEvent, { UserEvent } from '@testing-library/user-event';
import { http } from 'msw';
import { MockInstance } from 'vitest';
import { storageApi } from '../../api/api';
import { usePatchImage } from '../../api/images';
import handleIMS_APIError from '../../handleIMS_APIError';
import ImagesJSON from '../../mocks/Images.json';
import { server } from '../../mocks/server';
import { renderComponentWithRouterProvider } from '../../testUtils';
import EditImageDialog, { ImageDialogProps } from './editImageDialog.component';
import EditFileDialog, { FileDialogProps } from './editFileDialog.component';

vi.mock('../../handleIMS_APIError');

describe('Edit image dialog', () => {
describe('Edit file dialog', () => {
const onClose = vi.fn();
let props: ImageDialogProps;
let props: FileDialogProps;
let user: UserEvent;
const createView = () => {
return renderComponentWithRouterProvider(<EditImageDialog {...props} />);
return renderComponentWithRouterProvider(<EditFileDialog {...props} />);
};

beforeEach(() => {
props = {
open: true,
onClose: onClose,
fileType: 'Image',
usePatchFile: usePatchImage,
};
user = userEvent.setup();
});

afterEach(() => {
vi.clearAllMocks();
});
const modifyImageValues = (values: {
const modifyFileValues = (values: {
file_name?: string;
title?: string;
description?: string;
Expand All @@ -56,7 +59,7 @@ describe('Edit image dialog', () => {
beforeEach(() => {
props = {
...props,
selectedImage: ImagesJSON[0],
selectedFile: ImagesJSON[0],
};

axiosPatchSpy = vi.spyOn(storageApi, 'patch');
Expand All @@ -71,7 +74,7 @@ describe('Edit image dialog', () => {

createView();

modifyImageValues({
modifyFileValues({
file_name: 'Image A',
});

Expand All @@ -84,7 +87,7 @@ describe('Edit image dialog', () => {

it('Edits an image correctly', async () => {
createView();
modifyImageValues({
modifyFileValues({
file_name: 'test_file_name.jpeg',
title: 'Test Title',
description: 'Test Description',
Expand Down Expand Up @@ -119,7 +122,7 @@ describe('Edit image dialog', () => {

it('Required fields show error if they are whitespace or current value just removed', async () => {
createView();
modifyImageValues({
modifyFileValues({
file_name: '',
});
const saveButton = screen.getByRole('button', { name: 'Save' });
Expand All @@ -132,7 +135,7 @@ describe('Edit image dialog', () => {

it('CatchAllError request works correctly and displays refresh page message', async () => {
createView();
modifyImageValues({
modifyFileValues({
file_name: 'Error 500',
});
const saveButton = screen.getByRole('button', { name: 'Save' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,37 @@ import { useForm } from 'react-hook-form';

import React from 'react';

import { UseMutationResult } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { APIImage, ImagePatch } from '../../api/api.types';
import { usePatchImage } from '../../api/images';
import { ImagesSchema } from '../../form.schemas';
import { APIImage, ObjectFilePatch } from '../../api/api.types';
import { FileSchema } from '../../form.schemas';
import handleIMS_APIError from '../../handleIMS_APIError';

export interface ImageDialogProps {
export interface FileDialogProps {
open: boolean;
onClose: () => void;
selectedImage?: APIImage;
selectedFile?: APIImage;
fileType: 'Image' | 'Attachment';
usePatchFile: () => UseMutationResult<
APIImage,
AxiosError,
{ id: string; fileMetadata: ObjectFilePatch }
>;
}

const EditImageDialog = (props: ImageDialogProps) => {
const { open, onClose, selectedImage } = props;
const EditFileDialog = (props: FileDialogProps) => {
const { open, onClose, selectedFile, fileType, usePatchFile } = props;

const { mutateAsync: patchImage, isPending: isEditPending } = usePatchImage();
const { mutateAsync: patchFile, isPending: isEditPending } = usePatchFile();

const initalImage: ImagePatch = React.useMemo(
const initialFile: ObjectFilePatch = React.useMemo(
() =>
selectedImage ?? {
selectedFile ?? {
file_name: '',
title: '',
description: '',
},
[selectedImage]
[selectedFile]
);

const {
Expand All @@ -50,52 +56,50 @@ const EditImageDialog = (props: ImageDialogProps) => {
setError,
clearErrors,
reset,
} = useForm<ImagePatch>({
resolver: zodResolver(ImagesSchema('patch')),
defaultValues: initalImage,
} = useForm<ObjectFilePatch>({
resolver: zodResolver(FileSchema('patch')),
defaultValues: initialFile,
});

// Load the values for editing
React.useEffect(() => {
reset(initalImage);
}, [initalImage, reset]);
reset(initialFile);
}, [initialFile, 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]);
}, [clearErrors, errors, selectedFile, 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 handleEditFile = React.useCallback(
(fileData: ObjectFilePatch) => {
if (selectedFile) {
const isFileNameUpdated = fileData.file_name !== selectedFile.file_name;

const isDescriptionUpdated =
imageData.description !== selectedImage.description;
fileData.description !== selectedFile.description;

const isTitleUpdated = imageData.title !== selectedImage.title;
const isTitleUpdated = fileData.title !== selectedFile.title;

let imageToEdit: ImagePatch = {};
let fileToEdit: ObjectFilePatch = {};

if (isFileNameUpdated) imageToEdit.file_name = imageData.file_name;
if (isDescriptionUpdated)
imageToEdit.description = imageData.description;
if (isTitleUpdated) imageToEdit.title = imageData.title;
if (isFileNameUpdated) fileToEdit.file_name = fileData.file_name;
if (isDescriptionUpdated) fileToEdit.description = fileData.description;
if (isTitleUpdated) fileToEdit.title = fileData.title;

if (isFileNameUpdated || isDescriptionUpdated || isTitleUpdated) {
patchImage({
id: selectedImage.id,
image: imageToEdit,
patchFile({
id: selectedFile.id,
fileMetadata: fileToEdit,
})
.then(() => handleClose())
.catch((error: AxiosError) => {
Expand All @@ -109,17 +113,17 @@ const EditImageDialog = (props: ImageDialogProps) => {
}
}
},
[selectedImage, patchImage, handleClose, setError]
[selectedFile, patchFile, handleClose, setError]
);

return (
<Dialog open={open} maxWidth="lg" fullWidth>
<DialogTitle>{`Edit Image`}</DialogTitle>
<DialogTitle>{`Edit ${fileType}`}</DialogTitle>
<DialogContent>
<Grid container direction="column" spacing={1} component="form">
<Grid item sx={{ mt: 1 }}>
<TextField
id="image-file-name-input"
id="object-file-name-input"
label="File Name"
required
{...register('file_name')}
Expand All @@ -130,7 +134,7 @@ const EditImageDialog = (props: ImageDialogProps) => {
</Grid>
<Grid item>
<TextField
id="image-description-input"
id="object-description-input"
label="Description"
{...register('description')}
error={!!errors.description}
Expand All @@ -140,7 +144,7 @@ const EditImageDialog = (props: ImageDialogProps) => {
</Grid>
<Grid item>
<TextField
id="image-title-input"
id="object-title-input"
label="Title"
{...register('title')}
error={!!errors.title}
Expand Down Expand Up @@ -172,7 +176,7 @@ const EditImageDialog = (props: ImageDialogProps) => {
<Button
variant="outlined"
sx={{ width: '50%', mx: 1 }}
onClick={handleSubmit(handleEditImage)}
onClick={handleSubmit(handleEditFile)}
disabled={Object.values(errors).length !== 0 || isEditPending}
endIcon={isEditPending ? <CircularProgress size={20} /> : null}
>
Expand All @@ -189,4 +193,4 @@ const EditImageDialog = (props: ImageDialogProps) => {
);
};

export default EditImageDialog;
export default EditFileDialog;
10 changes: 6 additions & 4 deletions src/common/images/imageGallery.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import {
import { MRT_Localization_EN } from 'material-react-table/locales/en';
import React from 'react';
import { APIImage } from '../../api/api.types';
import { useGetImages } from '../../api/images';
import { useGetImages, usePatchImage } from '../../api/images';
import { displayTableRowCountText, OverflowTip } from '../../utils';
import CardViewFilters from '../cardView/cardViewFilters.component';
import { usePreservedTableState } from '../preservedTableState.component';
import EditImageDialog from './editImageDialog.component';
import EditFileDialog from './editFileDialog.component';
import GalleryLightBox from './galleryLightbox.component';
import ImageInformationDialog from './imageInformationDialog.component';
import ThumbnailImage from './thumbnailImage.component';
Expand Down Expand Up @@ -437,10 +437,12 @@ const ImageGallery = (props: ImageGalleryProps) => {
onClose={() => setOpenMenuDialog(false)}
image={selectedImage}
/>
<EditImageDialog
<EditFileDialog
open={openMenuDialog === 'edit'}
onClose={() => setOpenMenuDialog(false)}
selectedImage={selectedImage}
fileType="Image"
usePatchFile={usePatchImage}
selectedFile={selectedImage}
/>
</>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/form.schemas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -615,9 +615,9 @@ export const ItemDetailsStepSchema = (requestType: RequestType) => {
});
};

// ------------------------------------ IMAGES ------------------------------------
// ------------------------------------ FILES ------------------------------------

export const ImagesSchema = (requestType: RequestType) =>
export const FileSchema = (requestType: RequestType) =>
z.object({
file_name: MandatoryStringSchema({
errorMessage: 'Please enter a file name.',
Expand Down
4 changes: 2 additions & 2 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import {
CatalogueItem,
CatalogueItemPatch,
CatalogueItemPost,
ImagePatch,
Item,
ItemPatch,
ItemPost,
Manufacturer,
ManufacturerPatch,
ManufacturerPost,
ObjectFilePatch,
System,
SystemPatch,
SystemPost,
Expand Down Expand Up @@ -1074,7 +1074,7 @@ export const handlers = [
}
}),

http.patch<{ id: string }, ImagePatch, APIImage | ErrorResponse>(
http.patch<{ id: string }, ObjectFilePatch, APIImage | ErrorResponse>(
'/images/:id',
async ({ request, params }) => {
const { id } = params;
Expand Down

0 comments on commit 5d3f5a7

Please sign in to comment.