From 60f9daea8c988836db38cafbb25326d6a4440c84 Mon Sep 17 00:00:00 2001 From: Luca van der Wijngaart Date: Sat, 21 Mar 2026 13:48:01 +0100 Subject: [PATCH 1/2] Improve image edit form #1209 --- src/components/Exercises/Add/Step5Images.tsx | 136 ++----------- .../Detail/ExerciseDetailEdit.test.tsx | 186 +++++++++++++++++- .../Exercises/Detail/ExerciseDetailEdit.tsx | 105 ++++++++++ src/components/Exercises/forms/ImageCard.tsx | 14 +- src/components/Exercises/forms/ImageModal.tsx | 112 +++++++++++ src/components/Exercises/forms/ImageStyle.tsx | 14 +- src/components/Exercises/models/exercise.ts | 2 +- src/components/Exercises/models/image.ts | 19 +- src/components/Exercises/queries/index.ts | 15 +- src/services/image.ts | 41 ++++ 10 files changed, 511 insertions(+), 133 deletions(-) create mode 100644 src/components/Exercises/forms/ImageModal.tsx diff --git a/src/components/Exercises/Add/Step5Images.tsx b/src/components/Exercises/Add/Step5Images.tsx index e607543a6..e0e9846ce 100644 --- a/src/components/Exercises/Add/Step5Images.tsx +++ b/src/components/Exercises/Add/Step5Images.tsx @@ -1,31 +1,21 @@ import CameraAltIcon from '@mui/icons-material/CameraAlt'; import CollectionsIcon from '@mui/icons-material/Collections'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import InfoIcon from '@mui/icons-material/Info'; import { - Alert, Box, Button, IconButton, ImageListItem, ImageListItemBar, - Modal, Stack, Typography } from "@mui/material"; import Grid from '@mui/material/Grid'; import ImageList from '@mui/material/ImageList'; -import { LicenseAuthor } from "components/Common/forms/LicenseAuthor"; -import { LicenseAuthorUrl } from "components/Common/forms/LicenseAuthorUrl"; -import { LicenseDerivativeSourceUrl } from "components/Common/forms/LicenseDerivativeSourceUrl"; -import { LicenseObjectUrl } from "components/Common/forms/LicenseObjectUrl"; -import { LicenseTitle } from "components/Common/forms/LicenseTitle"; import { StepProps } from "components/Exercises/Add/AddExerciseStepper"; -import { ImageStyleToggle } from "components/Exercises/forms/ImageStyle"; +import { ImageFormModal } from "components/Exercises/forms/ImageModal"; import { ImageFormData } from "components/Exercises/models/exercise"; import { ImageStyle } from "components/Exercises/models/image"; -import { useProfileQuery } from "components/User/queries/profile"; -import { Form, Formik } from "formik"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useExerciseSubmissionStateValue } from "state"; @@ -33,14 +23,16 @@ import { setImages } from "state/exerciseSubmissionReducer"; export const Step5Images = ({ onContinue, onBack }: StepProps) => { const [t] = useTranslation(); - const profileQuery = useProfileQuery(); const [state, dispatch] = useExerciseSubmissionStateValue(); const [localImages, setLocalImages] = useState(state.images); - const [popupImage, setPopupImage] = useState(undefined); + const [selectedImage, setSelectedImage] = useState(null); - const [openModal, setOpenModal] = React.useState(false); - const handleCloseModal = () => setOpenModal(false); + const [openModal, setOpenModal] = useState(false); + const handleCloseModal = () => { + setOpenModal(false); + setSelectedImage(null); + }; useEffect(() => { dispatch(setImages(localImages)); @@ -50,15 +42,13 @@ export const Step5Images = ({ onContinue, onBack }: StepProps) => { if (!e.target.files?.length) { return; } + const [uploadedFile] = e.target.files; const objectURL = URL.createObjectURL(uploadedFile); - setOpenModal(true); - - setPopupImage({ + setSelectedImage({ url: objectURL, file: uploadedFile, - author: "", authorUrl: "", title: "", @@ -66,27 +56,11 @@ export const Step5Images = ({ onContinue, onBack }: StepProps) => { objectUrl: "", style: ImageStyle.PHOTO.toString() }); + setOpenModal(true); }; - const handleAddFullImage = (data: { - title: string, - objectUrl: string, - author: string, - authorUrl: string, - derivativeSourceUrl: string, - imageStyle: number - }) => { - setLocalImages(localImages.concat({ - url: popupImage?.url!, - file: popupImage?.file!, - - author: data.author, - authorUrl: data.authorUrl, - title: data.title, - derivativeSourceUrl: data.derivativeSourceUrl, - objectUrl: data.objectUrl, - style: data.imageStyle.toString() - })); + const handleAddFullImage = (data: ImageFormData) => { + setLocalImages(prevImages => prevImages.concat(data)); handleCloseModal(); }; @@ -99,89 +73,15 @@ export const Step5Images = ({ onContinue, onBack }: StepProps) => { onContinue!(); }; - const style = { - position: 'absolute' as const, - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 600, - bgcolor: 'background.paper', - //border: '2px solid #000', - boxShadow: 24, - p: 4, - }; - return ( (
- - - - - {t('exercises.imageDetails')} - - - - - {popupImage && } - - - { - console.log(values); - handleAddFullImage(values); - }} - > - {formik => { - return (
- - - - - - - - } severity="info"> - By submitting this image, you agree to release it under the CC - BY-SA 4.0 license. The image must be either your own work or the - author must have released in under - a license compatible with CC BY-SA 4.0. - - - - - - - -
); - }} -
-
-
-
-
+ image={selectedImage} + onSubmit={handleAddFullImage} + submitLabel={t('add')} + /> {t("exercises.compatibleImagesCC")} @@ -216,7 +116,7 @@ export const Step5Images = ({ onContinue, onBack }: StepProps) => { + style={{ maxHeight: "400px" }}> {localImages.map(imageEntry => ( (value: T): T => { + return Object.assign(Promise.resolve(value), value) as unknown as T; +}; + describe("Exercise translation edit tests", () => { const editTranslationMutateMock: jest.Mock = jest.fn(); @@ -60,6 +69,19 @@ describe("Exercise translation edit tests", () => { (editTranslation as jest.Mock).mockImplementation(() => Promise.resolve(testExerciseSquats.translations[1])); (useProfileQuery as jest.Mock).mockImplementation(() => Promise.resolve(testProfileDataVerified)); + (useEditExerciseImageQuery as jest.Mock).mockImplementation(() => ({ + isError: false, + isPending: false, + mutateAsync: jest.fn(), + })); + + (useAddExerciseImageQuery as jest.Mock).mockImplementation(() => ({ + isError: false, + isPending: false, + mutate: jest.fn(), + mutateAsync: jest.fn(), + })); + // @ts-ignore // addTranslation.mockImplementation(() => Promise.resolve( // new Translation( @@ -245,4 +267,162 @@ describe("Exercise translation edit tests", () => { ); */ }); -}); \ No newline at end of file +}); + +describe("Exercise image tests", () => { + const editImageMutateMock: jest.Mock = jest.fn(); + const addImageMutateMock: jest.Mock = jest.fn(); + const deleteImageMutateMock: jest.Mock = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + + editImageMutateMock.mockResolvedValue({}); + addImageMutateMock.mockResolvedValue({}); + deleteImageMutateMock.mockResolvedValue({}); + + (useAddTranslationQuery as jest.Mock).mockImplementation(() => ({ + isPending: false, + mutateAsync: jest.fn() + })); + (useEditTranslationQuery as jest.Mock).mockImplementation(() => ({ + isPending: false, + mutateAsync: jest.fn() + })); + + (useEditExerciseImageQuery as jest.Mock).mockImplementation(() => ({ + isError: false, + isPending: false, + mutateAsync: editImageMutateMock + })); + (useAddExerciseImageQuery as jest.Mock).mockImplementation(() => asPromiseHookResult({ + isError: false, + isPending: false, + mutate: addImageMutateMock, + mutateAsync: addImageMutateMock + })); + (useDeleteExerciseImageQuery as jest.Mock).mockImplementation(() => asPromiseHookResult({ + isError: false, + isPending: false, + mutate: deleteImageMutateMock, + mutateAsync: deleteImageMutateMock + })); + + (useMusclesQuery as jest.Mock).mockImplementation(() => asPromiseHookResult({ + isLoading: false, + isSuccess: true, + data: testMuscles + })); + (useProfileQuery as jest.Mock).mockImplementation(() => asPromiseHookResult({ + isLoading: false, + isSuccess: true, + data: testProfileDataVerified + })); + (usePermissionQuery as jest.Mock).mockImplementation((permission: string) => { + const imagePermission = + permission === WgerPermissions.ADD_IMAGE + || permission === WgerPermissions.DELETE_IMAGE; + + return asPromiseHookResult({ + isLoading: false, + isSuccess: true, + data: imagePermission + }); + }); + }); + + test("edits an existing image and submits patch mutation", async () => { + const user = userEvent.setup(); + + const image = new ExerciseImage( + 77, + "img-uuid-77", + "https://example.com/squat.jpg", + true, + "Old title", + "Old author", + "https://old-author.example", + "https://old-object.example", + "https://old-derivative.example", + 4 + ); + + const exerciseWithImage = new Exercise({ + ...testExerciseSquats, + images: [image] + }); + + (useExerciseQuery as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: exerciseWithImage, + })); + + render( + + + + ); + + // Open image edit modal + await user.click(screen.getByTestId("edit-image-77")); + + // change fields in the modal + const getModalInput = async (name: string) => { + return await waitFor(() => { + const el = document.querySelector(`input[name="${name}"]`) as HTMLInputElement | null; + expect(el).not.toBeNull(); + return el as HTMLInputElement; + }); + }; + + // Title + const titleInput = await getModalInput("title"); + await user.clear(titleInput); + await user.type(titleInput, "Updated title"); + // ObjectURL + const objectUrlInput = await getModalInput("objectUrl"); + await user.clear(objectUrlInput); + await user.type(objectUrlInput, "https://updatedObjecturl.com"); + // Author + const authorInput = await getModalInput("author"); + await user.clear(authorInput); + await user.type(authorInput, "Updated author"); + // Author URL + const authorURLInput = await getModalInput("authorUrl"); + await user.clear(authorURLInput); + await user.type(authorURLInput, "https://updatedAutorurl.com"); + // DerivativeSourceURL + const derivativeURLInput = await getModalInput("derivativeSourceUrl") + await user.clear(derivativeURLInput); + await user.type(derivativeURLInput, "https://updated-derivative.com"); + + const clickStyleByValue = async (value: string) => { + const styleGroup = await waitFor(() => screen.getByRole("group", { name: "text alignment" })); + const styleButton = styleGroup.querySelector(`button[value="${value}"]`) as HTMLButtonElement | null; + expect(styleButton).not.toBeNull(); + await user.click(styleButton!); + }; + + // Style + await clickStyleByValue("2"); + + // Submit modal + const submitButton = screen.getByTestId("submit-edit-image-form"); + await user.click(submitButton); + + expect(editImageMutateMock).toHaveBeenCalledWith({ + imageId: 77, + image: undefined, + imageData: expect.objectContaining({ + url: "https://example.com/squat.jpg", + title: "Updated title", + author: "Updated author", + authorUrl: "https://updatedAutorurl.com", + objectUrl: "https://updatedObjecturl.com", + derivativeSourceUrl: "https://updated-derivative.com", + style: 2, + }), + }); + }); +}); diff --git a/src/components/Exercises/Detail/ExerciseDetailEdit.tsx b/src/components/Exercises/Detail/ExerciseDetailEdit.tsx index 4d91ad2c7..17c9de770 100644 --- a/src/components/Exercises/Detail/ExerciseDetailEdit.tsx +++ b/src/components/Exercises/Detail/ExerciseDetailEdit.tsx @@ -19,7 +19,9 @@ import { import { Language } from "components/Exercises/models/language"; import { Translation } from "components/Exercises/models/translation"; import { + useAddExerciseImageQuery, useAddTranslationQuery, + useEditExerciseImageQuery, useEditTranslationQuery, useExerciseQuery, useMusclesQuery @@ -33,6 +35,43 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { deleteAlias, postAlias } from "services"; import * as yup from "yup"; +import { ImageFormModal } from '../forms/ImageModal'; +import { ExerciseImage } from '../models/image'; +import { ImageFormData } from '../models/exercise'; +import { FormQueryErrorsSnackbar } from 'components/Core/Widgets/FormError'; + +export const mapToImageFormData = ( + image: ExerciseImage | File | null +): ImageFormData | null => { + if (!image) return null; + + // Case 1: Editing an existing ExerciseImage + if (image instanceof ExerciseImage) { + // Get the image/file from url + return { + url: image.url, + file: undefined, + title: image.title || '', + author: image.author || '', + authorUrl: image.authorUrl || '', + objectUrl: image.objectUrl || '', + derivativeSourceUrl: image.derivativeSourceUrl || '', + style: image.style?.toString() || '1', // Default to Photo + }; + } + + // Case 2: Adding a new File + return { + url: URL.createObjectURL(image), + file: image, + title: '', + author: '', + authorUrl: '', + objectUrl: '', + derivativeSourceUrl: '', + style: '1', + }; +}; export interface ViewProps { exerciseId: number; @@ -58,6 +97,14 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => { const profileQuery = useProfileQuery(); const exercise = exerciseQuery.data!; + + const [openModal, setOpenModal] = React.useState(false); + const [imageGuardError, setImageGuardError] = useState(null); + + const [selectedImage, setSelectedImage] = useState(null); + const [editingImageId, setEditingImageId] = useState(null); + + const editImageMutation = useEditExerciseImageQuery(exerciseId); useEffect(() => { if (exerciseQuery.data !== undefined) { @@ -95,6 +142,42 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => { description: descriptionValidator() }); + // Called when clicking "Edit" on an existing card + // Inside handleEditClick in ExerciseDetailEdit.tsx + const handleEditClick = (image: ExerciseImage) => { + setSelectedImage({ + url: image.url, + file: undefined, + title: image.title || '', + author: image.author || '', + authorUrl: image.authorUrl || '', + objectUrl: image.objectUrl || '', + derivativeSourceUrl: image.derivativeSourceUrl || '', + style: image.style?.toString() || '1', + }); + setEditingImageId(image.id); + setOpenModal(true); + setImageGuardError(null); + }; + + const handleSaveImage = async (values: ImageFormData) => { + if (!editingImageId) { + setImageGuardError('Could not edit image: missing image id.'); + return; + } + + await editImageMutation.mutateAsync({ + imageId: editingImageId, + image: values.file, // This will be undefined if they didn't pick a new file + imageData: values + }); + + setOpenModal(false); + setEditingImageId(null); + setSelectedImage(null); + setImageGuardError(null); + }; + return <> { exerciseId={exercise.id!} image={img} canDelete={deleteImagePermissionQuery.data!} + onEdit={handleEditClick} /> ))} @@ -352,5 +436,26 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => { } + + setOpenModal(false)} + image={selectedImage} + onSubmit={handleSaveImage} + submitLabel={t('save')} + /> + + {editImageMutation.isError && ( + + )} + + {imageGuardError && ( + + )} ; }; diff --git a/src/components/Exercises/forms/ImageCard.tsx b/src/components/Exercises/forms/ImageCard.tsx index 4322f83a8..34809002a 100644 --- a/src/components/Exercises/forms/ImageCard.tsx +++ b/src/components/Exercises/forms/ImageCard.tsx @@ -11,9 +11,10 @@ type ImageCardProps = { exerciseId: number; image: ExerciseImage; canDelete: boolean + onEdit: (image: ExerciseImage) => void; }; -export const ImageEditCard = ({ exerciseId, image, canDelete }: ImageCardProps) => { +export const ImageEditCard = ({ exerciseId, image, canDelete, onEdit }: ImageCardProps) => { const [t] = useTranslation(); const deleteImageQuery = useDeleteExerciseImageQuery(exerciseId); @@ -24,7 +25,7 @@ export const ImageEditCard = ({ exerciseId, image, canDelete }: ImageCardProps) sx={{ height: 120 }} alt="" /> - + {canDelete && } + {canDelete && + + } ; }; diff --git a/src/components/Exercises/forms/ImageModal.tsx b/src/components/Exercises/forms/ImageModal.tsx new file mode 100644 index 000000000..9828d5e48 --- /dev/null +++ b/src/components/Exercises/forms/ImageModal.tsx @@ -0,0 +1,112 @@ +import { Alert, Box, Button, Grid, Modal, Stack, Typography } from "@mui/material"; +import InfoIcon from '@mui/icons-material/Info'; +import { LicenseAuthor } from "components/Common/forms/LicenseAuthor"; +import { LicenseAuthorUrl } from "components/Common/forms/LicenseAuthorUrl"; +import { LicenseDerivativeSourceUrl } from "components/Common/forms/LicenseDerivativeSourceUrl"; +import { LicenseObjectUrl } from "components/Common/forms/LicenseObjectUrl"; +import { LicenseTitle } from "components/Common/forms/LicenseTitle"; +import { Form, Formik } from "formik"; +import { ImageStyleToggle } from "./ImageStyle"; +import { useTranslation } from "react-i18next"; +import { useProfileQuery } from "components/User/queries/profile"; +import { ImageFormData } from "../models/exercise"; +import { ImageStyle } from "../models/image"; + +interface ImageFormModalProps { + open: boolean; + onClose: () => void; + // The image data to display and edit (can be a new upload or existing image) + image: ImageFormData | null; + // The specific action to take when the user clicks "Save/Add" + onSubmit: (values: ImageFormData) => void; + // Change the button text (e.g., "Add" vs "Save Changes") + submitLabel: string; +} + +const style = { + position: 'absolute' as const, + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 600, + bgcolor: 'background.paper', + //border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +export const ImageFormModal = ({ + open, + onClose, + image, + onSubmit, + submitLabel +}: ImageFormModalProps) => { + const { t } = useTranslation(); + const profileQuery = useProfileQuery(); + + // If no image is provided, don't render or show a loader + if (!image) return null; + + return ( + + + + {t('exercises.imageDetails')} + + + + + Preview + + + + {({ submitForm }) => ( +
+ + + + + + + + + } severity="info"> + By submitting this image, you agree to release it under the CC + BY-SA 4.0 license. The image must be either your own work or the + author must have released in under + a license compatible with CC BY-SA 4.0. + + + + + + +
+ )} +
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/Exercises/forms/ImageStyle.tsx b/src/components/Exercises/forms/ImageStyle.tsx index 60ab567e8..90b27f800 100644 --- a/src/components/Exercises/forms/ImageStyle.tsx +++ b/src/components/Exercises/forms/ImageStyle.tsx @@ -14,17 +14,21 @@ import { useTranslation } from "react-i18next"; export function ImageStyleToggle(props: { fieldName: string }) { const [t] = useTranslation(); - const [style, setStyle] = React.useState(ImageStyle.PHOTO); - - const [, , helpers] = useField(props.fieldName); - + const [field, , helpers] = useField(props.fieldName); + const selectedStyle = (field.value !== undefined && field.value !== null && field.value !== '') + ? Number(field.value) + : ImageStyle.PHOTO; + const [style, setStyle] = React.useState(selectedStyle); const handleAlignment = ( event: React.MouseEvent, newStyle: number | null, ) => { - setStyle(newStyle); + if (newStyle === null) { + return; + } helpers.setValue(newStyle); + setStyle(newStyle); }; return ( diff --git a/src/components/Exercises/models/exercise.ts b/src/components/Exercises/models/exercise.ts index 15a73b9b6..2e353d313 100644 --- a/src/components/Exercises/models/exercise.ts +++ b/src/components/Exercises/models/exercise.ts @@ -160,7 +160,7 @@ export class ExerciseAdapter implements Adapter { export type ImageFormData = { url: string; - file: File; + file?: File; // When editing an existing image, this is undefined author: string; authorUrl: string; title: string, diff --git a/src/components/Exercises/models/image.ts b/src/components/Exercises/models/image.ts index d116e9338..6ff639427 100644 --- a/src/components/Exercises/models/image.ts +++ b/src/components/Exercises/models/image.ts @@ -14,8 +14,15 @@ export class ExerciseImage { public id: number, public uuid: string, public url: string, - public isMain: boolean) { - } + public isMain: boolean, + + public title?: string, + public author?: string, + public authorUrl?: string, + public objectUrl?: string, + public derivativeSourceUrl?: string, + public style?: number + ) {} } export class ExerciseImageAdapter implements Adapter { @@ -25,7 +32,13 @@ export class ExerciseImageAdapter implements Adapter { item.id, item.uuid, item.image, - item.is_main + item.is_main, + item.license_title ?? '', + item.license_author ?? '', + item.license_author_url ?? '', + item.license_object_url ?? '', + item.license_derivative_source_url ?? '', + item.style ? Number(item.style) : undefined ); } diff --git a/src/components/Exercises/queries/index.ts b/src/components/Exercises/queries/index.ts index a9fbd79c0..a144eff3d 100644 --- a/src/components/Exercises/queries/index.ts +++ b/src/components/Exercises/queries/index.ts @@ -9,7 +9,7 @@ import { postExerciseImage, } from "services"; import { AddTranslationParams, EditTranslationParams } from "services/exerciseTranslation"; -import { deleteExerciseImage, PostExerciseImageParams } from "services/image"; +import { deleteExerciseImage, PostExerciseImageParams, patchExerciseImage, PatchExerciseImageParams } from "services/image"; import { deleteExerciseVideo, postExerciseVideo, PostExerciseVideoParams } from "services/video"; import { QueryKey } from "utils/consts"; @@ -63,6 +63,19 @@ export function useAddExerciseImageQuery(exerciseId: number) { }); } +export function useEditExerciseImageQuery(exerciseId: number) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: PatchExerciseImageParams) => patchExerciseImage(data), + onSuccess: () => { + // Invalidate the cache so the UI shows the updated title/author immediately + queryClient.invalidateQueries({ queryKey: [QueryKey.EXERCISES] }); + queryClient.invalidateQueries({ queryKey: [QueryKey.EXERCISE_DETAIL, exerciseId] }); + } + }); +} + /** * A query hook to add a new exercise video * @param exerciseId {number} - Exercise ID to which the uploaded video will be added to diff --git a/src/services/image.ts b/src/services/image.ts index 565ab6967..af457cc78 100644 --- a/src/services/image.ts +++ b/src/services/image.ts @@ -52,3 +52,44 @@ export const deleteExerciseImage = async (imageId: number): Promise => { return response.status; }; + +export type PatchExerciseImageParams = { + imageId: number; + image?: File; // Optional: only send if the user selected a NEW file + imageData: ImageFormData; +}; + +/** + * Edit an existing exercise image + */ +export const patchExerciseImage = async (data: PatchExerciseImageParams): Promise => { + const url = makeUrl(IMAGE_PATH, { id: data.imageId }); + const headers = makeHeader(); + headers['Content-Type'] = 'multipart/form-data'; + + const formData = new FormData(); + + // ONLY append if the file exists AND has content (size > 0) + // If we are editing existing metadata, we don't send the 'image' key at all + if (data.image && data.image.size > 0) { + formData.append('image', data.image); + } + + // Always append the metadata + formData.append('license_title', data.imageData.title); + formData.append('license_object_url', data.imageData.objectUrl); + formData.append('license_author', data.imageData.author); + formData.append('license_author_url', data.imageData.authorUrl); + formData.append('license_derivative_source_url', data.imageData.derivativeSourceUrl); + formData.append('style', data.imageData.style); + + try { + const response = await axios.patch(url, formData, { headers }); + return new ExerciseImageAdapter().fromJson(response.data); + } catch (error) { + if (axios.isAxiosError(error)) { + console.error(error.response?.status, error.response?.data); + } + throw error; + } +}; \ No newline at end of file From 10daf4df62fab2154c8a9d3c917a49054508f599 Mon Sep 17 00:00:00 2001 From: Luca van der Wijngaart Date: Mon, 23 Mar 2026 16:55:48 +0100 Subject: [PATCH 2/2] Fix lint errors and failing test cases. --- src/components/Exercises/Add/Step5Images.tsx | 2 +- .../Detail/ExerciseDetailEdit.test.tsx | 2 +- .../Exercises/Detail/ExerciseDetailEdit.tsx | 8 ++++---- src/components/Exercises/forms/ImageModal.tsx | 3 --- src/components/Exercises/forms/ImageStyle.tsx | 4 ++-- src/components/Exercises/models/exercise.ts | 2 +- src/components/Exercises/models/image.ts | 12 ++++++------ src/services/image.test.ts | 19 +++++++++++++++---- src/tests/responseApi.ts | 15 +++++++++++++-- 9 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/components/Exercises/Add/Step5Images.tsx b/src/components/Exercises/Add/Step5Images.tsx index e0e9846ce..0b605f183 100644 --- a/src/components/Exercises/Add/Step5Images.tsx +++ b/src/components/Exercises/Add/Step5Images.tsx @@ -54,7 +54,7 @@ export const Step5Images = ({ onContinue, onBack }: StepProps) => { title: "", derivativeSourceUrl: "", objectUrl: "", - style: ImageStyle.PHOTO.toString() + style: ImageStyle.PHOTO }); setOpenModal(true); }; diff --git a/src/components/Exercises/Detail/ExerciseDetailEdit.test.tsx b/src/components/Exercises/Detail/ExerciseDetailEdit.test.tsx index 847e5e5b9..7e71edb6b 100644 --- a/src/components/Exercises/Detail/ExerciseDetailEdit.test.tsx +++ b/src/components/Exercises/Detail/ExerciseDetailEdit.test.tsx @@ -393,7 +393,7 @@ describe("Exercise image tests", () => { await user.clear(authorURLInput); await user.type(authorURLInput, "https://updatedAutorurl.com"); // DerivativeSourceURL - const derivativeURLInput = await getModalInput("derivativeSourceUrl") + const derivativeURLInput = await getModalInput("derivativeSourceUrl"); await user.clear(derivativeURLInput); await user.type(derivativeURLInput, "https://updated-derivative.com"); diff --git a/src/components/Exercises/Detail/ExerciseDetailEdit.tsx b/src/components/Exercises/Detail/ExerciseDetailEdit.tsx index 17c9de770..b8fa07981 100644 --- a/src/components/Exercises/Detail/ExerciseDetailEdit.tsx +++ b/src/components/Exercises/Detail/ExerciseDetailEdit.tsx @@ -36,7 +36,7 @@ import { useTranslation } from "react-i18next"; import { deleteAlias, postAlias } from "services"; import * as yup from "yup"; import { ImageFormModal } from '../forms/ImageModal'; -import { ExerciseImage } from '../models/image'; +import { ExerciseImage, ImageStyle } from '../models/image'; import { ImageFormData } from '../models/exercise'; import { FormQueryErrorsSnackbar } from 'components/Core/Widgets/FormError'; @@ -56,7 +56,7 @@ export const mapToImageFormData = ( authorUrl: image.authorUrl || '', objectUrl: image.objectUrl || '', derivativeSourceUrl: image.derivativeSourceUrl || '', - style: image.style?.toString() || '1', // Default to Photo + style: image.style || ImageStyle.PHOTO, // Default to Photo }; } @@ -69,7 +69,7 @@ export const mapToImageFormData = ( authorUrl: '', objectUrl: '', derivativeSourceUrl: '', - style: '1', + style: ImageStyle.PHOTO, }; }; @@ -153,7 +153,7 @@ export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => { authorUrl: image.authorUrl || '', objectUrl: image.objectUrl || '', derivativeSourceUrl: image.derivativeSourceUrl || '', - style: image.style?.toString() || '1', + style: image.style || ImageStyle.PHOTO, }); setEditingImageId(image.id); setOpenModal(true); diff --git a/src/components/Exercises/forms/ImageModal.tsx b/src/components/Exercises/forms/ImageModal.tsx index 9828d5e48..63628601f 100644 --- a/src/components/Exercises/forms/ImageModal.tsx +++ b/src/components/Exercises/forms/ImageModal.tsx @@ -8,9 +8,7 @@ import { LicenseTitle } from "components/Common/forms/LicenseTitle"; import { Form, Formik } from "formik"; import { ImageStyleToggle } from "./ImageStyle"; import { useTranslation } from "react-i18next"; -import { useProfileQuery } from "components/User/queries/profile"; import { ImageFormData } from "../models/exercise"; -import { ImageStyle } from "../models/image"; interface ImageFormModalProps { open: boolean; @@ -43,7 +41,6 @@ export const ImageFormModal = ({ submitLabel }: ImageFormModalProps) => { const { t } = useTranslation(); - const profileQuery = useProfileQuery(); // If no image is provided, don't render or show a loader if (!image) return null; diff --git a/src/components/Exercises/forms/ImageStyle.tsx b/src/components/Exercises/forms/ImageStyle.tsx index 90b27f800..b1d840244 100644 --- a/src/components/Exercises/forms/ImageStyle.tsx +++ b/src/components/Exercises/forms/ImageStyle.tsx @@ -17,8 +17,8 @@ export function ImageStyleToggle(props: { fieldName: string }) { const [field, , helpers] = useField(props.fieldName); const selectedStyle = (field.value !== undefined && field.value !== null && field.value !== '') ? Number(field.value) - : ImageStyle.PHOTO; - const [style, setStyle] = React.useState(selectedStyle); + : undefined; + const [style, setStyle] = React.useState(selectedStyle); const handleAlignment = ( event: React.MouseEvent, diff --git a/src/components/Exercises/models/exercise.ts b/src/components/Exercises/models/exercise.ts index 2e353d313..c231fbc9a 100644 --- a/src/components/Exercises/models/exercise.ts +++ b/src/components/Exercises/models/exercise.ts @@ -166,6 +166,6 @@ export type ImageFormData = { title: string, objectUrl: string, derivativeSourceUrl: string; - style: string; + style: number; }; diff --git a/src/components/Exercises/models/image.ts b/src/components/Exercises/models/image.ts index 6ff639427..3b8036cf0 100644 --- a/src/components/Exercises/models/image.ts +++ b/src/components/Exercises/models/image.ts @@ -33,12 +33,12 @@ export class ExerciseImageAdapter implements Adapter { item.uuid, item.image, item.is_main, - item.license_title ?? '', - item.license_author ?? '', - item.license_author_url ?? '', - item.license_object_url ?? '', - item.license_derivative_source_url ?? '', - item.style ? Number(item.style) : undefined + item.license_title, + item.license_author, + item.license_author_url, + item.license_object_url, + item.license_derivative_source_url, + item.style, ); } diff --git a/src/services/image.test.ts b/src/services/image.test.ts index 648ec0101..801777efd 100644 --- a/src/services/image.test.ts +++ b/src/services/image.test.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import { ExerciseImage } from "components/Exercises/models/image"; +import { ExerciseImage, ImageStyle } from "components/Exercises/models/image"; import { postExerciseImage } from "services"; import { deleteExerciseImage } from "services/image"; @@ -19,13 +19,24 @@ describe("Image service API tests", () => { "image": "https://wger.de/media/exercise-images/1070/004bb79f-36bf-4c48-8c00-d863d724717c.jpg", "is_main": true, "status": "1", - "style": "4" + "style": ImageStyle.THREE_D, + "license_title": "image", + "license_object_url": "https://object-url.com", + "license_author": "Test user", + "license_author_url": "https://author-url.com", + "license_derivative_source_url": "https://derivative-source-url.com" }; const image = new ExerciseImage( 1, "004bb79f-36bf-4c48-8c00-d863d724717c", "https://wger.de/media/exercise-images/1070/004bb79f-36bf-4c48-8c00-d863d724717c.jpg", - true + true, + "image", + "Test user", + "https://author-url.com", + "https://object-url.com", + "https://derivative-source-url.com", + ImageStyle.THREE_D ); (axios.post as jest.Mock).mockImplementation(() => Promise.resolve({ data: response })); @@ -43,7 +54,7 @@ describe("Image service API tests", () => { title: "top title", objectUrl: "", derivativeSourceUrl: "", - style: "3d", + style: ImageStyle.THREE_D, } } ); diff --git a/src/tests/responseApi.ts b/src/tests/responseApi.ts index 1997eedc4..215bcf285 100644 --- a/src/tests/responseApi.ts +++ b/src/tests/responseApi.ts @@ -38,7 +38,13 @@ const image = new ExerciseImage( 7, "2fe5f04b-5c9d-448c-a973-3fad6ddd4f74", "http://localhost:8000/media/exercise-images/9/2fe5f04b-5c9d-448c-a973-3fad6ddd4f74.jpg", - true + true, + "image", + "Test user", + "https://author-url.com", + "https://object-url.com", + "https://derivative-source-url.com", + 4 ); export const testApiExercise1 = new Exercise({ @@ -129,7 +135,12 @@ export const responseApiExerciseInfo = { "image": "http://localhost:8000/media/exercise-images/9/2fe5f04b-5c9d-448c-a973-3fad6ddd4f74.jpg", "is_main": true, "status": "2", - "style": "4" + "style": 4, + "license_title": "image", + "license_object_url": "https://object-url.com", + "license_author": "Test user", + "license_author_url": "https://author-url.com", + "license_derivative_source_url": "https://derivative-source-url.com" }], "videos": [ {