Skip to content

feat: display child programs in program requirement sections#3064

Open
ChristopherChudzicki wants to merge 23 commits intomainfrom
cc/link-to-programs
Open

feat: display child programs in program requirement sections#3064
ChristopherChudzicki wants to merge 23 commits intomainfrom
cc/link-to-programs

Conversation

@ChristopherChudzicki
Copy link
Contributor

@ChristopherChudzicki ChristopherChudzicki commented Mar 18, 2026

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_tree were silently ignored, including course-like programs with display_mode="course".

  • Child program cards: 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 the req_tree is preserved; course cards still link to /courses/{readable_id}.
  • MitxOnlineResourceCard: Replaces MitxOnlineCourseCard with a unified component (resourceType: "course" | "program") supporting enrollment-based pricing and certificate_type passthrough for both resource types.
  • getBestRun: Moved to common/mitxonline as 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

Screenshot 2026-03-17 at 10 55 14 PM

How can this be tested?

Prerequisites: MITxOnline and MIT Learn integrated

  1. Ensure you have a program with several child courses in MITxOnline, or use this script: https://gist.github.com/ChristopherChudzicki/b902d6f93f7094ead888481d061f14ef The script creates a program program-v1:card-demo with:
    • A bunch of child courses
    • A bunch of child programs with display_mode="course" ("courselike programs")
    • A bunch of child regular programs with standard display mode
      • Note: The UI display for program with child standard-display-programs isn't perfect. The cards/links work fine, but requirement text is off. This is not a use case we currently need, I believe.
    • In each case, the children have different display_modes
    • In each case, the children have products (except free-only) and certificate pages
    • See structure at top of script for more details
  2. Visit your program's product page... http://open.odl.local:8062/programs/program-v1:card-demo if you used the script above
  3. Check the cards; compare screenshot above
    • paid-only courses should display price outside certificate badge
    • paid-or-free display price within certificate badge
    • If min/max price are set to different values on the wagtail page, that should display as a range
    • Links:
      • Course -> courses/:readable_id
      • Courselike Programs -> courses/p/:readable_id
      • Programs -> programs/:readable_id

Copilot AI review requested due to automatic review settings March 18, 2026 02:11
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (parseReqTreeitems, plus getIdsFromReqTree for recursive ID collection).
  • Updates ProgramPage and ProgramAsCoursePage to fetch/render child programs alongside courses.
  • Replaces the per-resource cards with a unified MitxOnlineResourceCard and 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.

ChristopherChudzicki and others added 20 commits March 18, 2026 16:31
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>
…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),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Comment on lines +80 to +88
// 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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a use case when program contains programs? Can't think of one

Copy link
Contributor Author

@ChristopherChudzicki ChristopherChudzicki Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@gumaerc gumaerc self-assigned this Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants