Enhance Exercise Form Functionality with Variant Management Improvements
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
- Introduced snapshot and dirty check functions for variant payloads, enabling better tracking of unsaved changes. - Implemented synchronization of saved variant snapshots to improve data integrity during edits. - Enhanced the variant saving process with validation for variant names and automatic saving of changes. - Updated the UI to reflect changes in variant management, ensuring a smoother user experience when editing exercise variants.
This commit is contained in:
parent
9b3f594007
commit
7f62b6ceee
|
|
@ -190,6 +190,26 @@ function buildVariantPayloadFromRow(row) {
|
|||
}
|
||||
}
|
||||
|
||||
function snapshotVariantPayload(row) {
|
||||
return JSON.stringify(buildVariantPayloadFromRow(row))
|
||||
}
|
||||
|
||||
function variantDraftHasContent(draft) {
|
||||
if (!draft) return false
|
||||
const p = buildVariantPayloadFromRow(draft)
|
||||
return (
|
||||
p.variant_name.length > 0 ||
|
||||
Boolean(p.description) ||
|
||||
Boolean(p.execution_changes) ||
|
||||
p.duration_min != null ||
|
||||
p.duration_max != null ||
|
||||
(Array.isArray(p.equipment_changes) && p.equipment_changes.length > 0) ||
|
||||
Boolean(p.difficulty_adjustment) ||
|
||||
(p.progression_level != null && p.progression_level !== 1) ||
|
||||
p.prerequisite_variant_id != null
|
||||
)
|
||||
}
|
||||
|
||||
/** Gemeinsame Felder für „Variante bearbeiten“ und „Neue Variante“. */
|
||||
function ExerciseVariantFields({
|
||||
row,
|
||||
|
|
@ -480,6 +500,24 @@ function ExerciseFormPageRoot() {
|
|||
const [variantBusy, setVariantBusy] = useState(false)
|
||||
const [variantEditSelection, setVariantEditSelection] = useState(null)
|
||||
const variantsDetailsRef = useRef(null)
|
||||
const variantsSavedSnapshotRef = useRef({})
|
||||
|
||||
const syncVariantsSavedSnapshot = useCallback((rows) => {
|
||||
const snap = {}
|
||||
for (const v of rows || []) {
|
||||
if (v?.id != null) snap[v.id] = snapshotVariantPayload(v)
|
||||
}
|
||||
variantsSavedSnapshotRef.current = snap
|
||||
}, [])
|
||||
|
||||
const getDirtyVariantRows = useCallback((rows) => {
|
||||
return (rows || []).filter((v) => {
|
||||
if (v?.id == null) return false
|
||||
const saved = variantsSavedSnapshotRef.current[v.id]
|
||||
if (saved == null) return true
|
||||
return snapshotVariantPayload(v) !== saved
|
||||
})
|
||||
}, [])
|
||||
|
||||
const [mediaFields, setMediaFields] = useState({})
|
||||
const [mediaSavingId, setMediaSavingId] = useState(null)
|
||||
|
|
@ -588,9 +626,11 @@ function ExerciseFormPageRoot() {
|
|||
try {
|
||||
const exercise = await api.getExercise(exerciseId)
|
||||
if (cancelled) return
|
||||
const variantRows = (exercise.variants || []).map(apiVariantToRow)
|
||||
setFormData(detailToForm(exercise))
|
||||
setMediaList(exercise.media || [])
|
||||
setVariants((exercise.variants || []).map(apiVariantToRow))
|
||||
setVariants(variantRows)
|
||||
syncVariantsSavedSnapshot(variantRows)
|
||||
setVariantDraft(emptyVariantDraft())
|
||||
setVariantEditSelection(null)
|
||||
setFormDirty(false)
|
||||
|
|
@ -774,12 +814,73 @@ function ExerciseFormPageRoot() {
|
|||
)
|
||||
}
|
||||
|
||||
const refreshVariants = useCallback(async () => {
|
||||
if (!exerciseId) return
|
||||
const ex = await api.getExercise(exerciseId)
|
||||
const rows = (ex.variants || []).map(apiVariantToRow)
|
||||
syncVariantsSavedSnapshot(rows)
|
||||
setVariants(rows)
|
||||
}, [exerciseId, syncVariantsSavedSnapshot])
|
||||
|
||||
const persistPendingVariantChanges = useCallback(async () => {
|
||||
if (!exerciseId) return true
|
||||
|
||||
const dirtyRows = getDirtyVariantRows(variants)
|
||||
if (dirtyRows.length > 0) {
|
||||
setVariantBusy(true)
|
||||
try {
|
||||
for (const row of dirtyRows) {
|
||||
const payload = buildVariantPayloadFromRow(row)
|
||||
if (payload.variant_name.length < 3) {
|
||||
toast.error(`Variante „${row.variant_name || `#${row.id}`}“: Name mindestens 3 Zeichen`)
|
||||
return false
|
||||
}
|
||||
setVariantSavingId(row.id)
|
||||
await api.updateExerciseVariant(exerciseId, row.id, payload)
|
||||
}
|
||||
await refreshVariants()
|
||||
} catch (e) {
|
||||
toast.error(e.message || String(e))
|
||||
return false
|
||||
} finally {
|
||||
setVariantSavingId(null)
|
||||
setVariantBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (variantDraftHasContent(variantDraft)) {
|
||||
const payload = buildVariantPayloadFromRow(variantDraft)
|
||||
if (payload.variant_name.length < 3) {
|
||||
toast.error('Variantenentwurf: Name mindestens 3 Zeichen, sonst Felder verwerfen oder ausfüllen.')
|
||||
return false
|
||||
}
|
||||
setVariantBusy(true)
|
||||
try {
|
||||
const created = await api.createExerciseVariant(exerciseId, payload)
|
||||
setVariantDraft(emptyVariantDraft())
|
||||
if (created?.id != null) setVariantEditSelection(created.id)
|
||||
await refreshVariants()
|
||||
} catch (e) {
|
||||
toast.error(e.message || String(e))
|
||||
return false
|
||||
} finally {
|
||||
setVariantBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}, [exerciseId, variantDraft, variants, getDirtyVariantRows, refreshVariants, toast])
|
||||
|
||||
const performSaveAttempt = useCallback(
|
||||
async ({ fromUnsavedDialog = false, closeAfter = false } = {}) => {
|
||||
if (!formData.title || formData.title.trim().length < 3) {
|
||||
toast.error('Titel mindestens 3 Zeichen')
|
||||
return false
|
||||
}
|
||||
if (isEdit && exerciseId) {
|
||||
const variantsOk = await persistPendingVariantChanges()
|
||||
if (!variantsOk) return false
|
||||
}
|
||||
const payloadBase = {
|
||||
...formData,
|
||||
equipment:
|
||||
|
|
@ -876,7 +977,9 @@ function ExerciseFormPageRoot() {
|
|||
}
|
||||
const ex = await api.getExercise(exerciseId)
|
||||
setMediaList(ex.media || [])
|
||||
setVariants((ex.variants || []).map(apiVariantToRow))
|
||||
const variantRows = (ex.variants || []).map(apiVariantToRow)
|
||||
setVariants(variantRows)
|
||||
syncVariantsSavedSnapshot(variantRows)
|
||||
setFormDirty(false)
|
||||
toast.success('Gespeichert.')
|
||||
if (closeAfter) goBack()
|
||||
|
|
@ -898,7 +1001,7 @@ function ExerciseFormPageRoot() {
|
|||
setSaving(false)
|
||||
}
|
||||
},
|
||||
[exerciseId, formData, isEdit, navigate, location, toast, goBack],
|
||||
[exerciseId, formData, isEdit, navigate, location, toast, goBack, persistPendingVariantChanges, syncVariantsSavedSnapshot],
|
||||
)
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
|
|
@ -1028,12 +1131,6 @@ function ExerciseFormPageRoot() {
|
|||
}
|
||||
}
|
||||
|
||||
const refreshVariants = async () => {
|
||||
if (!exerciseId) return
|
||||
const ex = await api.getExercise(exerciseId)
|
||||
setVariants((ex.variants || []).map(apiVariantToRow))
|
||||
}
|
||||
|
||||
const updateVariantField = (id, patch) => {
|
||||
setFormDirty(true)
|
||||
setVariants((prev) => prev.map((v) => (v.id === id ? { ...v, ...patch } : v)))
|
||||
|
|
@ -1863,7 +1960,8 @@ function ExerciseFormPageRoot() {
|
|||
<div className="exercise-variants-details__body">
|
||||
<p className="exercise-variants-hint">
|
||||
Pro Durchgang nur eine Variante bearbeiten – weniger Scrollen. Reihenfolge entspricht Planung und Auswahl im
|
||||
Training; „Voraussetzung“ nutzt ihr später für Progressions-Serien.
|
||||
Training; „Voraussetzung“ nutzt ihr später für Progressions-Serien. Offene Varianten-Änderungen werden mit
|
||||
„Speichern“ in der Aktionsleiste (oder in der Sicherheitsabfrage beim Verlassen) automatisch mitgespeichert.
|
||||
</p>
|
||||
|
||||
{variants.length > 0 && (
|
||||
|
|
@ -1973,12 +2071,13 @@ function ExerciseFormPageRoot() {
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
className="btn btn-secondary"
|
||||
style={{ marginLeft: 'auto', fontSize: '12px' }}
|
||||
disabled={variantSavingId === selectedVariantForEdit.id || variantBusy}
|
||||
onClick={() => saveVariantRow(selectedVariantForEdit)}
|
||||
title="Optional — Änderungen werden auch über die Aktionsleiste gespeichert"
|
||||
>
|
||||
{variantSavingId === selectedVariantForEdit.id ? 'Speichern…' : 'Speichern'}
|
||||
{variantSavingId === selectedVariantForEdit.id ? 'Speichern…' : 'Variante jetzt speichern'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user