Refactor TrainingUnitSectionsEditor to support new parallel stream functionality
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s

- Renamed functions and parameters to clarify the handling of sections within parallel streams, enhancing code readability.
- Updated drag-and-drop event handlers to accommodate additional parameters for managing sections in parallel streams.
- Introduced a new utility function to reorder sections as the first entry in a parallel stream, improving section management.
- Enhanced the visual representation of drop zones for sections, ensuring a better user experience during reordering operations.
This commit is contained in:
Lars 2026-05-15 12:10:44 +02:00
parent 72e8f31cff
commit 3005f1cb3e
2 changed files with 97 additions and 23 deletions

View File

@ -25,7 +25,7 @@ import {
swapAdjacentPhaseRuns,
reorderBlocksImmutableWithPlanLoc,
reorderSectionBeforeParallelRunAsWholeGroup,
reorderSectionAsFirstInParallelPhase,
reorderSectionAsFirstInParallelStream,
reorderBlockIntoParallelStreamEnd,
globalInsertBeforeIndexForParallelStreamEnd,
movePhaseRunUpByPhaseOrder,
@ -744,7 +744,7 @@ export default function TrainingUnitSectionsEditor({
setDropSectionBand({ slot: sectionToSlot, phaseAboveSplitPo: Number(po) || 0 })
}
const onPhaseBelowSplitDragOver = (e, po) => {
const onPhaseBelowSplitDragOver = (e, po, so) => {
if (!enableSectionDragReorder || !enableParallelPhaseControls) return
if (!dtHasType(e, DND_TU_SECTION)) return
e.preventDefault()
@ -754,7 +754,10 @@ export default function TrainingUnitSectionsEditor({
} catch {
/* ignore */
}
setDropSectionBand({ slot: sectionToSlot, phaseBelowSplitPo: Number(po) || 0 })
setDropSectionBand({
slot: sectionToSlot,
phaseBelowSplit: { po: Number(po) || 0, so: Number(so) || 0 },
})
}
const applyParsedSectionDrop = (data) => {
@ -824,7 +827,7 @@ export default function TrainingUnitSectionsEditor({
})
}
const onPhaseBelowSplitDrop = (e, po) => {
const onPhaseBelowSplitDrop = (e, po, so) => {
if (!enableSectionDragReorder || !enableParallelPhaseControls) return
e.preventDefault()
e.stopPropagation()
@ -843,6 +846,7 @@ export default function TrainingUnitSectionsEditor({
return
}
const targetPo = Number(po) || 0
const targetSo = Number(so) || 0
const parsed = applyParsedSectionDrop(data)
if (!parsed) return
@ -864,7 +868,7 @@ export default function TrainingUnitSectionsEditor({
const { fromSi } = parsed
patch((prev) => {
let next = reorderSectionAsFirstInParallelPhase(prev, fromSi, targetPo)
let next = reorderSectionAsFirstInParallelStream(prev, fromSi, targetPo, targetSo)
if (enableParallelPhaseControls) next = afterSectionReorderParallelGuard(prev, next)
return next
})
@ -1383,7 +1387,7 @@ export default function TrainingUnitSectionsEditor({
dropSectionBand.beforeIdx === bx &&
!dropSectionBand.streamDrop &&
dropSectionBand.phaseAboveSplitPo == null &&
dropSectionBand.phaseBelowSplitPo == null
!dropSectionBand.phaseBelowSplit
const streamChipDropActive = (po, so) =>
useStreamTagDropUx &&
@ -1398,7 +1402,8 @@ export default function TrainingUnitSectionsEditor({
const phaseBelowSplitDnd =
parallelPhaseOrder != null &&
dropSectionBand?.slot === sectionToSlot &&
dropSectionBand?.phaseBelowSplitPo === parallelPhaseOrder
dropSectionBand?.phaseBelowSplit?.po === parallelPhaseOrder &&
dropSectionBand?.phaseBelowSplit?.so === activeParallelStream
const streamVisual =
enableParallelPhaseControls && pl?.phaseKind === 'parallel'
@ -1622,22 +1627,6 @@ export default function TrainingUnitSectionsEditor({
+ Abschnitt in diesem Stream
</button>
</div>
{enableSectionDragReorder ? (
<div
className={
'tu-section-dropband tu-phase-drop--below-split tu-section-dropband--phase-parallel-slot' +
(phaseBelowSplitDnd ? ' tu-section-dropband--active' : '')
}
title="Erster Abschnitt dieser Split-Phase (vorne einfügen)"
aria-label="Dropzone erster Abschnitt unter dem Split-Kopf"
onDragOver={(e) => onPhaseBelowSplitDragOver(e, parallelPhaseOrder)}
onDragLeave={(e) => {
if (e.currentTarget.contains(e.relatedTarget)) return
clearSectionDnD()
}}
onDrop={(e) => onPhaseBelowSplitDrop(e, parallelPhaseOrder)}
/>
) : null}
<div
role="tablist"
aria-label={`Streams · Phase ${parallelPhaseOrder}`}
@ -1797,6 +1786,39 @@ export default function TrainingUnitSectionsEditor({
})}
</div>
</div>
{enableSectionDragReorder ? (
<div
className={
'tu-section-dropband tu-phase-drop--below-split tu-section-dropband--phase-parallel-slot' +
sectionDropBandRegionClass(
list,
firstVisibleIdxActiveStream ?? firstGlobalIdxThisPhase ?? sIdx,
enableParallelPhaseControls
) +
(phaseBelowSplitDnd ? ' tu-section-dropband--active' : '')
}
title="Erster Abschnitt in dieser Gruppe (hier vorne einfügen)"
aria-label="Dropzone: erster Slot der gewählten Splitgruppe unter dem Split-Kopf"
onDragOver={(e) =>
onPhaseBelowSplitDragOver(
e,
parallelPhaseOrder,
activeParallelStream ?? 0
)
}
onDragLeave={(e) => {
if (e.currentTarget.contains(e.relatedTarget)) return
clearSectionDnD()
}}
onDrop={(e) =>
onPhaseBelowSplitDrop(
e,
parallelPhaseOrder,
activeParallelStream ?? 0
)
}
/>
) : null}
</Fragment>
) : null}
{!hideParallelSection ? (

View File

@ -905,6 +905,58 @@ export function reorderSectionAsFirstInParallelPhase(prev, fromI, phaseOrderInde
return arr
}
/** Abschnitt als ersten Eintrag eines parallelen Streams setzen (planLoc wie erster Abschnitt dieses Streams, bzw. leerer Stream wie reorderBlockIntoParallelStreamEnd). */
export function reorderSectionAsFirstInParallelStream(prev, fromI, phaseOrderIndex, streamOrderIndex) {
const po = Number(phaseOrderIndex) || 0
const so = Number(streamOrderIndex) || 0
const len = prev?.length ?? 0
if (fromI < 0 || fromI >= len) return prev
const arr = [...prev]
const [moved] = arr.splice(fromI, 1)
const streamIdx = sectionIndicesForParallelStream(arr, po, so)
let insertAt
let headTpl
let skipFromIAdjust = false
if (streamIdx.length) {
const first = Math.min(...streamIdx)
headTpl = { ...arr[first].planLoc }
insertAt = first
} else {
const phaseIdx = indicesOfParallelPhase(arr, po)
if (!phaseIdx.length) {
const ml = moved?.planLoc
if (ml?.phaseKind !== 'parallel' || (ml.phaseOrderIndex ?? 0) !== po) return prev
headTpl = {
...ml,
parallelStreamOrderIndex: so,
streamTitle: null,
streamNotes: null,
streamAssignedTrainerProfileIds: null,
}
insertAt = Math.min(fromI, arr.length)
skipFromIAdjust = true
} else {
const ref = arr[phaseIdx[phaseIdx.length - 1]]
headTpl = {
...ref.planLoc,
parallelStreamOrderIndex: so,
streamTitle: null,
streamNotes: null,
streamAssignedTrainerProfileIds: null,
}
insertAt = phaseIdx[phaseIdx.length - 1] + 1
}
}
if (!skipFromIAdjust && fromI < insertAt) insertAt -= 1
insertAt = Math.max(0, Math.min(insertAt, arr.length))
arr.splice(insertAt, 0, { ...moved, planLoc: { ...headTpl } })
return arr
}
/**
* Abschnitt ans Ende eines parallelen Streams setzen (planLoc wie dieser Stream).
* Leerer Stream: Einfügen hinter den letzten Abschnitt der zugehörigen parallelen Phase, planLoc vom Referenz-Abschnitt mit angepasstem streamIndex.