feat: enhance TrainingUnitSectionsEditor with heading accessory support
Some checks failed
Deploy Development / deploy (push) Successful in 33s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 39s

- Added a new `headingAccessory` prop to TrainingUnitSectionsEditor for customizable header content.
- Updated the component to conditionally render the heading and accessory in a flexible toolbar layout.
- Refactored TrainingFrameworkProgramEditPage and TrainingPlanningPage to utilize the new heading accessory feature, improving user interaction and layout consistency.
This commit is contained in:
Lars 2026-05-05 14:47:38 +02:00
parent 2bfe67879f
commit 4080088d42
4 changed files with 72 additions and 70 deletions

View File

@ -46,6 +46,7 @@ export default function TrainingUnitSectionsEditor({
showExecutionExtras = false,
heading = 'Abschnitte & Übungen',
hideHeading = false,
headingAccessory = null,
wideExerciseGrid = false,
enableItemDragReorder = true,
enableSectionDragReorder = true,
@ -353,8 +354,39 @@ export default function TrainingUnitSectionsEditor({
(wideExerciseGrid ? ' training-unit-sections-editor--wide' : '')
}
>
{!hideHeading ? (
<h3 style={{ margin: '0 0 0.75rem', fontSize: '1rem' }}>{heading}</h3>
{(!hideHeading || headingAccessory) ? (
<div
className="tu-editor-heading-toolbar"
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-between',
alignItems: 'center',
gap: '10px',
marginBottom: '0.75rem',
}}
>
{!hideHeading ? (
<h3 style={{ margin: 0, fontSize: '1rem', flex: '1 1 200px', minWidth: 0 }}>
{heading}
</h3>
) : headingAccessory ? (
<span style={{ flex: '1 1 auto', minWidth: 0 }} />
) : null}
{headingAccessory ? (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
{headingAccessory}
</div>
) : null}
</div>
) : null}
{list.map((sec, sIdx) => {
const planMin = sectionPlannedMinutes(sec)

View File

@ -6,10 +6,10 @@ import ExercisePeekModal from '../components/ExercisePeekModal'
import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor'
import {
defaultSection,
exerciseRow,
normalizeUnitToForm,
enrichSectionsWithVariants,
buildSectionsPayload,
hydrateExercisePlanningRow,
} from '../utils/trainingUnitSectionsForm'
const DND_FW_SLOT = 'application/x-shinkan-framework-slot'
@ -52,27 +52,6 @@ async function enrichFrameworkSlotSections(slots) {
return out
}
async function hydrateExerciseForSlotRow(exercise) {
let variants = Array.isArray(exercise?.variants) ? exercise.variants : []
let title = exercise?.title || ''
const id = exercise?.id
if (!id) return null
if (!variants.length) {
try {
const full = await api.getExercise(id)
variants = Array.isArray(full?.variants) ? full.variants : []
title = full?.title || title
} catch {
variants = []
}
}
const row = exerciseRow()
row.exercise_id = id
row.exercise_variant_id = ''
row.exercise_title = title
row.variants = variants
return row
}
function goalHoverText(g) {
const t = (g.title || '').trim() || 'Ohne Titel'
const n = (g.notes || '').trim()
@ -1138,7 +1117,7 @@ export default function TrainingFrameworkProgramEditPage() {
const { slotIdx, sectionIndex: sIdx } = sectionPickerCtx
const rows = []
for (const ex of picked) {
const row = await hydrateExerciseForSlotRow(ex)
const row = await hydrateExercisePlanningRow(ex)
if (row) rows.push(row)
}
if (!rows.length) return
@ -1166,7 +1145,7 @@ export default function TrainingFrameworkProgramEditPage() {
if (sectionPickerCtx.multi) return
if (typeof sectionPickerCtx.itemIndex !== 'number') return
const { slotIdx, sectionIndex: sIdx, itemIndex: iIdx } = sectionPickerCtx
const row = await hydrateExerciseForSlotRow(exercise)
const row = await hydrateExercisePlanningRow(exercise)
if (!row) return
setForm((prev) => ({
...prev,

View File

@ -7,34 +7,12 @@ import ExercisePeekModal from '../components/ExercisePeekModal'
import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor'
import {
defaultSection,
exerciseRow,
normalizeUnitToForm,
enrichSectionsWithVariants,
buildSectionsPayload,
hydrateExercisePlanningRow,
} from '../utils/trainingUnitSectionsForm'
async function hydrateExerciseForPickerRow(exercise) {
let variants = Array.isArray(exercise?.variants) ? exercise.variants : []
let title = exercise?.title || ''
const id = exercise?.id
if (!id) return null
if (!variants.length) {
try {
const full = await api.getExercise(id)
variants = Array.isArray(full?.variants) ? full.variants : []
title = full?.title || title
} catch {
variants = []
}
}
const row = exerciseRow()
row.exercise_id = id
row.exercise_variant_id = ''
row.exercise_title = title
row.variants = variants
return row
}
function TrainingPlanningPage() {
const { user } = useAuth()
const [groups, setGroups] = useState([])
@ -634,7 +612,7 @@ function TrainingPlanningPage() {
background: 'var(--surface)',
borderRadius: '12px',
padding: 'clamp(12px, 3vw, 2rem)',
maxWidth: 'min(960px, 100%)',
maxWidth: 'min(1100px, 100%)',
width: '100%',
maxHeight: '92vh',
overflowY: 'auto',
@ -715,26 +693,16 @@ function TrainingPlanningPage() {
/>
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '2rem',
marginBottom: '0.75rem',
flexWrap: 'wrap',
gap: '0.5rem'
}}
>
<h3 style={{ margin: 0 }}>Abschnitte & Übungen</h3>
<button type="button" className="btn btn-secondary" onClick={handleSaveAsTemplate}>
Vorlage aus Aufbau speichern
</button>
</div>
<div style={{ marginTop: '2rem' }}>
<TrainingUnitSectionsEditor
hideHeading
heading="Abschnitte & Übungen"
headingAccessory={
<button type="button" className="btn btn-secondary" onClick={handleSaveAsTemplate}>
Vorlage aus Aufbau speichern
</button>
}
sections={formData.sections}
wideExerciseGrid
onSectionsChange={(updater) =>
setFormData((prev) => ({
...prev,
@ -754,6 +722,7 @@ function TrainingPlanningPage() {
}
showExecutionExtras={!!editingUnit}
/>
</div>
<div style={{ marginBottom: '1.75rem' }} />
@ -867,7 +836,7 @@ function TrainingPlanningPage() {
const { sIdx } = exercisePickerTarget
const rows = []
for (const ex of picked) {
const row = await hydrateExerciseForPickerRow(ex)
const row = await hydrateExercisePlanningRow(ex)
if (row) rows.push(row)
}
if (!rows.length) return
@ -884,7 +853,7 @@ function TrainingPlanningPage() {
}
onSelectExercise={async (ex) => {
if (!exercisePickerTarget || exercisePickerTarget.multi) return
const row = await hydrateExerciseForPickerRow(ex)
const row = await hydrateExercisePlanningRow(ex)
if (!row) return
const { sIdx, iIdx } = exercisePickerTarget
if (typeof iIdx !== 'number') return

View File

@ -18,6 +18,28 @@ export function exerciseRow() {
}
}
export async function hydrateExercisePlanningRow(exercise) {
let variants = Array.isArray(exercise?.variants) ? exercise.variants : []
let title = exercise?.title || ''
const id = exercise?.id
if (!id) return null
if (!variants.length) {
try {
const full = await api.getExercise(id)
variants = Array.isArray(full?.variants) ? full.variants : []
title = full?.title || title
} catch {
variants = []
}
}
const row = exerciseRow()
row.exercise_id = id
row.exercise_variant_id = ''
row.exercise_title = title
row.variants = variants
return row
}
export function noteRow() {
return { item_type: 'note', note_body: '' }
}