From 39e03f9b712055d558efc2378c0f01253330bcad Mon Sep 17 00:00:00 2001 From: Siyuan Jiang <2203934+sjiang1@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:49:29 -0400 Subject: [PATCH] Decouple practice item from a single track (#4) Replace PracticeItem.trackId with trackChoices: string[], so an item can reference a pool of tracks (the foundation for the dice-picks-a-track feature, #5). A new primaryTrackId(item) helper in plans.ts is the single seam every consumer reads through: PracticeTasksList, teacher-data aggregation, plan-validation, and the editor (PlanEditor / PracticeItemRow / replace-item-track / NewPlanButton) all resolve their lone track via it, so single-element pools behave exactly as before. #5 swaps this helper for "the rolled track." actions.getPlans gains normalizePlans, a read-time upgrade that maps legacy {trackId} items (the shape still stored in prod Redis) to {trackChoices}, so deploying needs no manual Redis migration. data/plans.json and the test seed are rewritten to the array shape. Tests: trackId PracticeItem literals migrated to trackChoices across the suite (validation-error and TrackReport trackId fields are unchanged); adds primaryTrackId unit tests and getPlans legacy-normalization tests. Co-Authored-By: Claude Opus 4.8 --- data/plans.json | 4 +-- src/app/PracticeTasksList.tsx | 15 ++++----- src/app/actions.test.ts | 40 ++++++++++++++++++++++-- src/app/actions.ts | 19 ++++++++++- src/app/plans.test.ts | 15 ++++++++- src/app/plans.ts | 9 +++++- src/app/plans/NewPlanButton.tsx | 2 +- src/app/plans/PlanEditor.tsx | 20 ++++++------ src/app/plans/PracticeItemRow.tsx | 6 ++-- src/app/plans/plan-validation.test.ts | 18 +++++------ src/app/plans/plan-validation.ts | 7 +++-- src/app/plans/replace-item-track.test.ts | 10 +++--- src/app/plans/replace-item-track.ts | 5 ++- src/app/seed-data.test.ts | 6 ++-- src/app/teacher-data.test.ts | 12 +++---- src/app/teacher-data.ts | 11 ++++--- src/app/test-support/seed.ts | 4 +-- 17 files changed, 143 insertions(+), 60 deletions(-) diff --git a/data/plans.json b/data/plans.json index a812161..9844288 100644 --- a/data/plans.json +++ b/data/plans.json @@ -5,12 +5,12 @@ "description": "Welcome! This is a sample practice plan — edit it, or create your own from the Plans tab.", "items": [ { - "trackId": "demo-song", + "trackChoices": ["demo-song"], "tasks": [{ "type": "playWithTrack", "count": 2 }], "dice": false }, { - "trackId": "a-major-scale", + "trackChoices": ["a-major-scale"], "tasks": [{ "type": "playWithoutTrack", "count": 1 }], "dice": true } diff --git a/src/app/PracticeTasksList.tsx b/src/app/PracticeTasksList.tsx index 90195c8..3ec291e 100644 --- a/src/app/PracticeTasksList.tsx +++ b/src/app/PracticeTasksList.tsx @@ -3,7 +3,7 @@ import DiceRoller from "./DiceRoller"; import TaskList, { TaskTypeDef } from "./TaskList"; import TrackNote from "./TrackNote"; -import type { PracticeItem, Track } from "./plans"; +import { primaryTrackId, type PracticeItem, type Track } from "./plans"; import type { Student } from "./students"; interface Props { @@ -19,11 +19,12 @@ export default function PracticeTasksList({ items, tracks, taskTypes, doneTasks, return (