feat: display child programs in program requirement sections#3064
feat: display child programs in program requirement sections#3064ChristopherChudzicki wants to merge 23 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for rendering child program nodes from a program’s req_tree in the product-page requirements/modules UI, aligning MITx Online program pages with how mixed course/program requirement trees are actually structured.
Changes:
- Refactors requirement parsing/extraction to preserve ordered course/program items (
parseReqTree→items, plusgetIdsFromReqTreefor recursive ID collection). - Updates
ProgramPageandProgramAsCoursePageto fetch/render child programs alongside courses. - Replaces the per-resource cards with a unified
MitxOnlineResourceCardand updates pricing display logic based on enrollment modes.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| frontends/ol-components/src/components/BaseLearningResourceCard/BaseLearningResourceCard.tsx | Documents new coursePrice / certificatePrice semantics used by MITx Online cards. |
| frontends/main/src/common/mitxonline.ts | Replaces course-only req_tree ID extraction with getIdsFromReqTree (course + program, recursive). |
| frontends/main/src/common/mitxonline.test.ts | Updates tests to cover getIdsFromReqTree for mixed/nested trees. |
| frontends/main/src/app-pages/ProductPages/util.ts | Refactors parseReqTree to emit ordered requirement items (course/program). |
| frontends/main/src/app-pages/ProductPages/util.test.ts | Adds tests for parseReqTree items and ordering. |
| frontends/main/src/app-pages/ProductPages/ProgramPage.tsx | Renders requirement cards for both courses and child programs; adds child program query. |
| frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx | Extends coverage for mixed course/program requirement rendering and child-program link behavior. |
| frontends/main/src/app-pages/ProductPages/ProgramBundleUpsell.tsx | Switches required-count calculation to the new requiredCount field. |
| frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.tsx | Renders module lists containing both courses and child programs; adds child program query. |
| frontends/main/src/app-pages/ProductPages/ProgramAsCoursePage.test.tsx | Adds coverage for mixed module lists including child programs. |
| frontends/main/src/app-pages/ProductPages/ProductSummary.tsx | Switches required-count calculation to the new requiredCount field. |
| frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx | Introduces unified card for course/program with enrollment-mode-based pricing. |
| frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.test.tsx | Adds coverage for unified card rendering and pricing rules. |
| frontends/main/src/app-pages/ProductPages/MitxOnlineCourseCard.tsx | Removes legacy course-only card in favor of unified card. |
You can also share your feedback on Copilot code review. Take the survey.
Program pages now render child programs alongside courses in the requirements section, preserving tree order. Child programs with display_mode="course" show as course cards linking to /courses/p/, while others show as program cards linking to /programs/. Completion text uses "courses/programs" when items are mixed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ypos
Resurrect the req_tree ID extraction as getIdsFromReqTree in mitxonline.ts,
returning { courseIds, programIds } from a single traversal. Remove duplicated
getCourseIdsFromReqTree/getProgramIdsFromReqTree from both test files. Fix
typos ("Collasping", "noFound") and remove dead assertion code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show certificate price or course price on program cards, mirroring the MitxOnlineCourseCard pattern. Uses min_price/max_price for price display and certificate_type to determine which price slot to use. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Course cards now prioritize page.current_price over min/max price range. Program cards use products[0].price instead of min/max price, matching how ProductSummary displays program pricing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use bestRun.products[0].price instead of page.current_price, which is the same value (the price of the run matching next_run_id). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Price display now follows enrollment mode rules: - free-only: show "Free" as course price, no certificate price - paid-only: show product price as course price - both: show "Free" as course price, product price as certificate price - neither: show nothing Course cards get price from the next run's product. Program cards get price from program.products[0].price. Also documents coursePrice/certificatePrice props on BaseLearningResourceCard. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace separate course and program card components with a single MitxOnlineResourceCard that accepts a discriminated union prop (resourceType: "course" | "program"). Eliminates duplicated enrollment type, pricing, and rendering logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use coursePageView() helper instead of raw template string for course hrefs, and handle undefined course gracefully - Rename misleading totalCourses to totalRequired in ProgramBundleUpsell - Tighten formatPrice type from unknown to string | number | null - Remove redundant factory overrides in tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
getItemNoun now returns { singular, plural } so that
"course/program" correctly pluralizes to "courses/programs"
instead of the broken "course/programs".
Also adds a test for mixed completion text and wraps async
assertions in waitFor to prevent flaky timing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch from raw render + ThemeProvider to the standard renderWithProviders helper for consistency with project conventions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tSummary Rename for clarity. ProductSummary RequirementsRow now uses getRequirementItemNoun so the label says "Courses/Programs" when a program has mixed requirement types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove getRequirementItemNoun — correctly classifying child programs by display_mode requires data that isn't always available (child program details must be fetched). Since child programs with display_mode="course" should count as courses anyway, always saying "courses" is the simplest correct approach. Added comments explaining this limitation in both ProgramPage and ProductSummary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use min_price/max_price from the resource (course or program) as the price source, showing a range when they differ. This replaces the previous approach of reading from bestRun.products[0].price for courses and program.products[0].price for programs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use formatPrice from mitxonline.ts instead of local formatCurrency - Assert child program hrefs in mixed requirements test - Add course-specific pricing test for MitxOnlineResourceCard - Scope ProgramAsCoursePage mixed test assertions to Modules region Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Display the actual certificate type (e.g., "program_certificate") instead of the generic "Certificate" label. Also removes a stray console.log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use findByRole instead of waitFor + getByRole for async assertions, per project testing conventions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When min_price and max_price are equal, use the product price (bestRun.products[0].price for courses, program.products[0].price for programs) as a more precise value. When they differ, show the range. This mirrors the original formatCoursePrice logic (which used page.current_price as the product-based fallback). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Programs now show start date (or "Anytime") like courses do - "Starts:" label shown for anytime or medium-sized cards; suppressed on small cards with a real date - Fix startLabel logic: show label when date exists, not when it doesn't Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract inline return type of extractCardData into a named CardData type - Replace waitFor+multiple-getBy blocks with findBy+synchronous-getBy pattern to satisfy testing-library/no-wait-for-multiple-assertions lint rule - Assert interleaved module order (c1, p1, c2) via listitem positions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0426c0f to
3d69822
Compare
…line
The dashboard helper and the product-page card had duplicate logic for
selecting the best run from a course. Unified into getBestRun in
common/mitxonline with an opts.enrollableOnly flag (default false) so
the dashboard can opt into enrollable-only filtering and the card gets
all runs. Dashboard callsite updated to { enrollableOnly: true, contractId }.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| title: course.title, | ||
| displayType: "Course", | ||
| imageSrc: course.page?.feature_image_src || DEFAULT_RESOURCE_IMG, | ||
| productPrice: formatResourcePrice(course, bestRun?.products[0]?.price), |
There was a problem hiding this comment.
This is using bestRun now; previously it was using current_price (https://github.com/mitodl/mit-learn/pull/3064/changes#diff-5bd17441d6dcec45afaedd98e767121812724d2156bae50e490cc5a67fc57295L47)
They are functionally equivalent. I decided to remove current_price since
- this was the only place in both mitxonline legacy frontend and learn frontend that used it...avoiding it means we could potentially use get rid of current_price
- Functionally, best_run's price and current_price are equivalent
Render function is now purely structural: loading skeleton or card. All business logic (enrollment type → coursePrice/certificatePrice) lives in extractCardData alongside the rest of the data extraction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| // Programs can have child courses and child programs. Child programs with | ||
| // display_mode="Course" are shown as courses here and on their own product pages. | ||
| // | ||
| // This text always says "courses" even when some children are programs with | ||
| // null display_mode. Correctly classifying child programs by display_mode would | ||
| // require waiting for the child programs query to resolve, causing flicker. The | ||
| // important use cases are course and course-like program children only; getting | ||
| // text like "courses/programs" right across the requirements display, summary, | ||
| // and bundle upsell would need nontrivial refactoring and design/product input. |
There was a problem hiding this comment.
@pdpinch I wanted to mention this to you. As I've currently written this code, a program with:
- 3 child courses
- 2 child Program(display_mode="course")
- 2 child programs(normal display mode)
Will say "7 required courses"...despite the fact that 2 child programs are "true" programs.
I'm not sure if "true" programs being children of other programs is something we plan on having soon. If so, we can/should change this.
I briefly considered saying "courses/programs" when some children are "true" programs, but this was difficult to get consistent between the InfoBox, the main column, and the bundle upsells, all of which use "Course" language right now.
There was a problem hiding this comment.
Do we have a use case when program contains programs? Can't think of one
There was a problem hiding this comment.
I don't think so / I agree. I wanted to mention it because our backend does support it pretty well, I believe, but the frontend won't (here, for example). (The backend does, e.g., for certificate creation. But that's necessary for display_mode="course" programs).
There was a problem hiding this comment.
Do we have a use case when program contains programs? Can't think of one
We created this feature specifically for UAI verticals, where the learner is supposed to get a distinct certificate for having completed all the foundational modules and a vertical.
This use case is all about certificates, and not very relevant to Product Pages, or even the dashboard.
What are the relevant tickets?
Closes https://github.com/mitodl/hq/issues/10537
Description (What does it do?)
Program product pages now display child programs alongside child courses in the requirements section. Previously, program nodes in the
req_treewere silently ignored, including course-like programs withdisplay_mode="course".display_mode="Course"programs display like course cards and link to/courses/p/{readable_id}; others display with a "Program" label and link to/programs/{readable_id}. Order from thereq_treeis preserved; course cards still link to/courses/{readable_id}.MitxOnlineResourceCard: ReplacesMitxOnlineCourseCardwith a unified component (resourceType: "course" | "program") supporting enrollment-based pricing andcertificate_typepassthrough for both resource types.getBestRun: Moved tocommon/mitxonlineas a shared utility (used by both the card and the dashboard); accepts{ enrollableOnly, contractId }opts so the dashboard can filter to enrollable runs without affecting card display.Known limitation
Completion text (e.g., "5 required courses") always says "courses" even when some children are programs. Correctly classifying them requires fetching child program details, which would cause flicker. The important use cases — course children and course-like program children — are correctly labeled. Getting mixed-noun text right across the requirements display, summary, and bundle upsell would need nontrivial refactoring and design/product input.
Screenshots
How can this be tested?
Prerequisites: MITxOnline and MIT Learn integrated
program-v1:card-demowith:courses/:readable_idcourses/p/:readable_idprograms/:readable_id