Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions frontend/src/lib/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -804,18 +804,21 @@ export function streakWeeks(S) {
}

/**
* Cascade an explicit load edit through later inherited rows in the same phase.
* Cascade an explicit work-set load edit through the later inherited work sets.
*
* Missing `weightOrigin` is inherited for compatibility with existing sessions. A row or side
* marked `manual` is an explicit exception, so it stays put even when it is heavier or lighter.
* `side` narrows a per-side edit to one limb; without it both limbs are eligible independently.
* Completed rows (and completed limbs) never get rewritten. Clearing an inherited load removes
* its `w` key just like a direct edit.
*
* Only a work-set edit cascades, and only onto work sets. Warm-ups are a ramp (buildSets), each
* rung its own load, so editing one rung leaves the rungs after it where they were (setting
* warm-up 1 of a 60/90/105 ramp to 65 used to turn it into 65/65/65).
*/
export function cascadeWeight(rows, from, value, side) {
const source = rows[from]
if (!source) return rows.slice()
const warm = isWarmupRow(source)
if (!source || isWarmupRow(source)) return rows.slice()
const sides = isSideSet(source) ? (side ? [side] : ['L', 'R']) : null
const next = rows.slice()
const setWeight = row => {
Expand All @@ -832,7 +835,7 @@ export function cascadeWeight(rows, from, value, side) {
}
for (let j = from + 1; j < next.length; j++) {
const row = next[j]
if (isWarmupRow(row) !== warm) continue
if (isWarmupRow(row)) continue
if (sides) {
if (!isSideSet(row)) continue
let out = row
Expand Down
30 changes: 28 additions & 2 deletions frontend/src/lib/history.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,32 @@ describe('session row helpers', () => {
expect(next[1].w).toBe(20) // different flag (warm-up) untouched
})

// Editing warm-up 1 of an incline bench ramp used to copy the new load onto the later
// warm-ups, so each one had to be put back by hand. Each rung keeps its own load.
it('cascadeWeight leaves the later warm-up rungs on their ramp loads', () => {
const S = { unit: 'lb', exWeights: {}, workouts: [] }
const rows = buildSets(S, { id: '0047', mode: 'reps', sets: 4, reps: 8, weight: 125, warmupSets: 3 }, { step: 5 })
expect(rows.map(r => r.w)).toEqual([60, 90, 105, 125, 125, 125, 125])
const next = cascadeWeight(rows, 0, 70) // the caller writes the edited row itself
expect(next.map(r => r.w)).toEqual([60, 90, 105, 125, 125, 125, 125])
const later = cascadeWeight(rows, 1, 100)
expect(later.map(r => r.w)).toEqual([60, 90, 105, 125, 125, 125, 125])
// a work-set edit still carries through the work sets and never reaches back into the ramp
expect(cascadeWeight(rows, 3, 130).map(r => r.w)).toEqual([60, 90, 105, 125, 130, 130, 130])
})

it('cascadeWeight leaves later one-side warm-up rungs alone too', () => {
const rows = [
{ ...makeSideSet({ w: 10, r: 16 }), phase: 'warmup' },
{ ...makeSideSet({ w: 15, r: 16 }), phase: 'warmup' },
makeSideSet({ w: 25, r: 16 }),
]
const next = cascadeWeight(rows, 0, 5, 'L')
expect(next[1].sides.L.w).toBe(15)
expect(next[1].sides.R.w).toBe(15)
expect(next[2].sides.L.w).toBe(25)
})

it('cascadeWeight deleting the weight removes the key from following undone rows only', () => {
const rows = [
{ w: 60, done: true },
Expand Down Expand Up @@ -1070,14 +1096,14 @@ describe('warm-up rows identified by phase alone', () => {
expect(workSetsDone({ entries: [{ sets: [imported, work] }] })).toBe(1)
})

it('cascadeWeight keeps phase-only warm-ups in their own lane', () => {
it('cascadeWeight treats a phase-only warm-up as a warm-up: the edit stays on its row', () => {
const rows = [
{ w: 40, r: 10, phase: 'warmup' },
{ w: 45, r: 10, phase: 'warmup' },
{ w: 100, r: 5 },
]
const next = cascadeWeight(rows, 0, 50)
expect(next[1].w).toBe(50)
expect(next[1].w).toBe(45)
expect(next[2].w).toBe(100)
})
})
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/views/Workout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1080,8 +1080,9 @@ function ActiveWorkout() {
// hold put aside must not outrank what you just typed, or the field would read 45 and the ▶
// would still hold the 30 the row was asking for before.
if (field === 'sec') delete e.sets[i].planSec
// Changing a weight cascades to following inherited sets of the same phase, so a correction
// carries through without retyping every row while explicit manual exceptions stay put.
// Changing a work set's weight cascades to the following inherited work sets, so a correction
// carries through without retyping every row while explicit manual exceptions stay put. A
// warm-up edit stays on its own row, and the later rungs keep their ramp loads.
if (field === 'w') {
e.sets[i].weightOrigin = WEIGHT_ORIGIN_MANUAL
e.sets = cascadeWeight(e.sets, i, v)
Expand Down
Loading