import { PaddingBox } from "@/components/Exercises/screens/Detail/ExerciseDetails"; import { EditExerciseCategory } from "@/components/Exercises/forms/Category"; import { EditExerciseEquipment } from "@/components/Exercises/forms/Equipment"; import { ExerciseAliases } from "@/components/Exercises/forms/ExerciseAliases"; import { ExerciseName } from "@/components/Exercises/forms/ExerciseName"; import { AddImageCard, ImageEditCard } from "@/components/Exercises/forms/ImageCard"; import { EditExerciseMuscle } from "@/components/Exercises/forms/Muscle"; import { EditExerciseVariation } from "@/components/Exercises/forms/Variation"; import { AddVideoCard, VideoEditCard } from "@/components/Exercises/forms/VideoCard"; import { alternativeNameValidator, descriptionValidator, nameValidator } from "@/components/Exercises/forms/yupValidators"; import { Language } from "@/components/Exercises/models/language"; import { Note } from "@/components/Exercises/models/note"; import { Translation } from "@/components/Exercises/models/translation"; import { useAddNoteQuery, useAddTranslationQuery, useDeleteAliasQuery, useDeleteNoteQuery, useEditExerciseImageQuery, useEditNoteQuery, useEditTranslationQuery, useExerciseQuery, useMusclesQuery, usePostAliasQuery } from "@/components/Exercises/queries"; import { MuscleOverview } from "@/components/Muscles/MuscleOverview"; import { usePermissionQuery, useProfileQuery, WgerPermissions } from "@/components/User"; import { MarkdownEditor } from "@/core/forms/MarkdownEditor"; import { LoadingWidget } from "@/core/ui/LoadingWidget/LoadingWidget"; import { FormQueryErrorsSnackbar } from '@/core/ui/Widgets/FormError'; import AddIcon from '@mui/icons-material/Add'; import CloseIcon from '@mui/icons-material/Close'; import DeleteIcon from '@mui/icons-material/Delete'; import SaveIcon from '@mui/icons-material/Save'; import { Alert, Box, Button, IconButton, InputAdornment, TextField, Typography } from "@mui/material"; import Grid from '@mui/material/Grid'; import { Form, Formik } from "formik"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import * as yup from "yup"; import { ImageFormModal } from '../../forms/ImageModal'; import { ImageFormData } from '../../models/exercise'; import { ExerciseImage, ImageStyle } from '../../models/image'; export interface ViewProps { exerciseId: number; language: Language; } export const ExerciseDetailEdit = ({ exerciseId, language }: ViewProps) => { const [t] = useTranslation(); const [alertIsVisible, setAlertIsVisible] = useState(false); const [mainMuscles, setMainMuscles] = useState([]); const [secondaryMuscles, setSecondaryMuscles] = useState([]); const exerciseQuery = useExerciseQuery(exerciseId); const addTranslationQuery = useAddTranslationQuery(exerciseId); const editTranslationQuery = useEditTranslationQuery(exerciseId); const postAliasQuery = usePostAliasQuery(exerciseId); const deleteAliasQuery = useDeleteAliasQuery(exerciseId); const addImagePermissionQuery = usePermissionQuery(WgerPermissions.ADD_IMAGE); const deleteImagePermissionQuery = usePermissionQuery(WgerPermissions.DELETE_IMAGE); const addVideoPermissionQuery = usePermissionQuery(WgerPermissions.ADD_VIDEO); const deleteVideoPermissionQuery = usePermissionQuery(WgerPermissions.DELETE_VIDEO); const editExercisePermissionQuery = usePermissionQuery(WgerPermissions.EDIT_EXERCISE); const musclesQuery = useMusclesQuery(); 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); const addNoteMutation = useAddNoteQuery(exerciseId); const editNoteMutation = useEditNoteQuery(exerciseId); const deleteNoteMutation = useDeleteNoteQuery(exerciseId); const [newNoteValue, setNewNoteValue] = useState(''); const [editingNoteId, setEditingNoteId] = useState(null); const [editingNoteValue, setEditingNoteValue] = useState(''); useEffect(() => { if (exerciseQuery.data !== undefined) { setMainMuscles(exerciseQuery.data.muscles.map((m) => m.id)); setSecondaryMuscles(exerciseQuery.data.musclesSecondary.map((m) => m.id)); } }, [exerciseQuery.data]); if ( exerciseQuery.isLoading || musclesQuery.isLoading || exerciseQuery.isLoading || profileQuery.isLoading || addImagePermissionQuery.isLoading || deleteImagePermissionQuery.isLoading || addVideoPermissionQuery.isLoading || deleteVideoPermissionQuery.isLoading || editExercisePermissionQuery.isLoading ) { return ; } const translationFromBase = exercise.getTranslation(language); const isNewTranslation = language.id !== translationFromBase?.language; const exerciseTranslation = isNewTranslation ? new Translation({ id: null, uuid: null, name: '', description: '', language: language.id }) : translationFromBase; const exerciseEnglish = exercise.getTranslation(); const validationSchema = yup.object({ name: nameValidator(), alternativeNames: alternativeNameValidator(), 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 || ImageStyle.PHOTO, isAi: image.isAi, }); 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 <> ({ id: a.id, alias: a.alias })), description: exerciseTranslation.descriptionSource, }} enableReinitialize validationSchema={validationSchema} onSubmit={async values => { // Exercise translation const payload = { exerciseId: exercise.id!, languageId: language.id, name: values.name, descriptionSource: values.description, author: '' }; const translation = exerciseTranslation.id ? await editTranslationQuery.mutateAsync({ ...payload, id: exerciseTranslation.id }) : await addTranslationQuery.mutateAsync({ ...payload, author: profileQuery.data!.username }); // Alias handling const aliasOrig = (exerciseTranslation.aliases).map(a => ({ id: a.id, alias: a.alias })); const aliasNew = values.alternativeNames ?? []; const aliasToCreate = aliasNew.filter(n => !aliasOrig.some(o => o.alias === n.alias)); const aliasToDelete = aliasOrig.filter(o => !aliasNew.some(n => n.alias === o.alias)); // Create new aliases for (const a of aliasToCreate) { await postAliasQuery.mutateAsync({ translationId: translation.id!, alias: a.alias }); } // Delete removed aliases for (const a of aliasToDelete) { if (a.id) { await deleteAliasQuery.mutateAsync(a.id); } } // Notify the user setAlertIsVisible(true); }} > {({ values, touched, errors, setFieldValue }) => (
{alertIsVisible && { setAlertIsVisible(false); }} > } > {t('exercises.successfullyUpdated')} } {t('translation')} {t('English')} {language.nameLong} ({language.nameShort}) {t('name')} {exerciseEnglish.name}
    {exerciseEnglish.aliases.map((alias) => (
  • {alias.alias}
  • ))}
{t('exercises.description')} {/* English/Base Description (Read Only) */} English Description (Reference)
{/* Markdown Editor */} setFieldValue('description', val)} error={touched.description && Boolean(errors.description)} // FIXED: Use ternary to ensure we return undefined instead of false helperText={touched.description ? errors.description : undefined} /> {editExercisePermissionQuery.data && <> {t('nutrition.others')} e.id)} /> {t('exercises.variations')} } {t('exercises.notes')}
    {exerciseEnglish.notes.map((note: Note) => (
  • {note.note}
  • ))}
{exerciseTranslation.notes.map((note: Note) => ( { if (editingNoteId !== note.id) { setEditingNoteId(note.id); setEditingNoteValue(e.target.value); } else { setEditingNoteValue(e.target.value); } }} onFocus={() => { if (editingNoteId !== note.id) { setEditingNoteId(note.id); setEditingNoteValue(note.note); } }} sx={{ mb: 1 }} variant="standard" slotProps={{ input: { endAdornment: ( {editingNoteId === note.id && editingNoteValue !== note.note && ( { await editNoteMutation.mutateAsync( new Note(note.id, note.translation, editingNoteValue) ); setEditingNoteId(null); setEditingNoteValue(''); }} disabled={editNoteMutation.isPending} > )} deleteNoteMutation.mutate(note.id!)} disabled={deleteNoteMutation.isPending} > ), } }} /> ))} {exerciseTranslation.id && ( setNewNoteValue(e.target.value)} helperText={t('exercises.notesHelpText')} slotProps={{ input: { endAdornment: ( { if (newNoteValue.trim()) { await addNoteMutation.mutateAsync( new Note(null, exerciseTranslation.id!, newNoteValue) ); setNewNoteValue(''); } }} disabled={addNoteMutation.isPending || !newNoteValue.trim()} > ), } }} /> )} )} {/* Images */} {t('images')} {addImagePermissionQuery.data && } {exercise.images.map(img => ( ))} {/* Videos */} {t('videos')} {addVideoPermissionQuery.data && } {exercise.videos.map(video => ( ))} {/* Base data */} {editExercisePermissionQuery.data && <> {t('exercises.muscles')} musclesQuery.data!.find(mq => mq.id === m)!)} secondaryMuscles={secondaryMuscles.map(m => musclesQuery.data!.find(mq => mq.id === m)!)} isFront={true} /> musclesQuery.data!.find(mq => mq.id === m)!)} secondaryMuscles={secondaryMuscles.map(m => musclesQuery.data!.find(mq => mq.id === m)!)} isFront={false} /> } setOpenModal(false)} image={selectedImage} onSubmit={handleSaveImage} submitLabel={t('save')} /> {editImageMutation.isError && ( )} {imageGuardError && ( )} ; };