-
Notifications
You must be signed in to change notification settings - Fork 3
individual program display #2721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
c4b6ac7 to
49f608d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements support for displaying program enrollments in the "My Learning" section and adds a dedicated program detail page to the dashboard. It updates the API client to version 2025.11.24 to support the new V2 program enrollment endpoints and adds the ability to view program requirements with course enrollment status.
Key Changes:
- Added program enrollment display with cards linking to individual program pages
- Created a dedicated program detail view showing requirement sections (core/electives) with completion tracking
- Refactored
DashboardCardto support both course and program resources with type guards - Filters out B2B program enrollments from personal "My Learning" section
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Updates @mitodl/mitxonline-api-axios to version 2025.11.24 with exact version pinning |
| frontends/main/src/common/urls.ts | Adds PROGRAM_VIEW route constants and programView helper function |
| frontends/main/src/app/dashboard/program/[id]/page.tsx | New Next.js page component for program detail route with ID validation |
| frontends/main/src/app-pages/ProductPages/ProgramPage.tsx | Returns Skeleton during loading instead of null for better UX |
| frontends/main/src/app-pages/ProductPages/ProductSummary.test.tsx | Updates test factories to match new API schema with readable_id fields |
| frontends/main/src/app-pages/DashboardPage/ProgramContent.tsx | New component that wraps EnrollmentDisplay with programId prop |
| frontends/main/src/app-pages/DashboardPage/ProgramContent.test.tsx | Comprehensive tests for ProgramContent component with mocked dependencies |
| frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx | Updates to use V2 program enrollment types and standardized certificate URLs |
| frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx | Adds mocks for V2 program enrollment endpoints and updates certificate URL expectations |
| frontends/main/src/app-pages/DashboardPage/HomeContent.test.tsx | Adds V2 program enrollments mock for test setup |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/types.ts | Adds DashboardProgram types with enrollment, requirement tree, and readableId support |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.ts | Implements program enrollment transformations, B2B filtering, and requirement tree parsing |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.test.tsx | Extensive test coverage for new transformation functions with edge cases |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts | Adds dashboardProgram factory and V2 program enrollment mock setup |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx | Adds ProgramEnrollmentDisplay component with requirement sections and stacked card container |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.test.tsx | Comprehensive tests for program filtering, B2B exclusion, and requirement display |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.test.tsx | Updates tests to include B2B contract IDs in course enrollment mocks |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx | Major refactor to support programs with type guards, stacked variant, and improved B2B handling |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.test.tsx | Extensive new tests for program cards, stacked variant, and B2B enrollment flows |
| frontends/main/package.json | Pins @mitodl/mitxonline-api-axios to exact version 2025.11.24 |
| frontends/jest-shared-setup.ts | Adds NEXT_PUBLIC_MITX_ONLINE_DOMAIN environment variable for tests |
| frontends/api/src/mitxonline/test-utils/urls.ts | Adds enrollmentsListV2 URL helper for program enrollments |
| frontends/api/src/mitxonline/test-utils/factories/programs.ts | Updates program factory with readable_id fields in requirements |
| frontends/api/src/mitxonline/test-utils/factories/pages.ts | Updates page item factory with readable_id fields in course requirements |
| frontends/api/src/mitxonline/test-utils/factories/enrollment.ts | Adds programEnrollmentV2 factory for V2 program enrollment structure |
| frontends/api/src/mitxonline/test-utils/factories/contracts.ts | Adds programs field to contract factory |
| frontends/api/src/mitxonline/hooks/enrollment/queries.ts | Updates programEnrollmentsList to use V2 endpoint and types |
| frontends/api/package.json | Pins @mitodl/mitxonline-api-axios to exact version 2025.11.24 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx
Outdated
Show resolved
Hide resolved
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx
Show resolved
Hide resolved
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx
Show resolved
Hide resolved
211a770 to
172c0f2
Compare
ChristopherChudzicki
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good. I did notice some issues with elective display.
I also think it would be great if we could better share the req_tree analysis between this and product pages.
| } | ||
| } | ||
|
|
||
| const transformProgramEnrollmentToDashboard = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this just camelcase things? If yes, IMO we should just leave it snakey.
Edit: this comment was supposed to be about transformProgramRequirement
frontends/jest-shared-setup.ts
Outdated
| "http://api.test.learn.odl.local:8063" | ||
| process.env.NEXT_PUBLIC_MITX_ONLINE_BASE_URL = | ||
| "http://api.test.mitxonline.odl.local:8053" | ||
| process.env.NEXT_PUBLIC_MITX_ONLINE_DOMAIN = "mitxonline.mit.edu" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to use the actual domain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clarifying... IMO, better to use something fake lest we actually hit the real API unintentionally.
(I think jest disables network calls? but i'm not really sure.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wasn't even supposed to be here. At one point I had functionality in here to do the redirect thing we talked about but nixed it in favor of the no-op that I'm assuming is the correct move here.
| useQuery(coursesQueries.coursesList({ id: program?.courseIds })) | ||
|
|
||
| // Build sections from requirement tree | ||
| const requirementSections = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. In #2728, I have it hardcoded to at most 1 required courses subsection and 1 elective courses subsection, copied from mitxonline behavior. That's silly though, your approach is better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, now I remember. Other portions of the API response that I am using (Namely, the requiremenets object) are hard coded to expect at most 1 required and 1 elective section. https://mitxonline.mit.edu/api/v2/programs/5/
| ) | ||
| const program = rawProgram ? mitxonlineProgram(rawProgram) : undefined | ||
| const { data: rawProgramCourses, isLoading: programCoursesLoading } = | ||
| useQuery(coursesQueries.coursesList({ id: program?.courseIds })) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now when I load http://open.odl.local:8062/dashboard/program/1, I see
- an api call to http://api.open.odl.local:8065/mitxonline/api/v2/courses/
- then another one to http://api.open.odl.local:8065/mitxonline/api/v2/courses/?id=11%2C12%2C13%2C4%2C1%2C2
I think (1) is coming from this query when program?.courseIds is undefined.
Suggestion: Add enabled: !!program?.courseIds?.length.
| variant="body2" | ||
| color={theme.custom.colors.silverGrayDark} | ||
| > | ||
| Completed {sectionCompletedCount} of {section.courses.length} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this display is quite right for electives nodes.
My program has:
{
"data": {
"node_type": "operator",
"operator": "min_number_of",
"operator_value": "1",
"program": 1,
"course": null,
"required_program": null,
"title": "Electives!",
"elective_flag": true
},
"id": 18,
"children": [/* two child course nodes */]
}
I think it should display like this (note "0 of 1" not "0 of 2", since you only need to take 1 elective).
| data: V2UserProgramEnrollmentDetail[], | ||
| ): DashboardProgram[] => { | ||
| // Filter out program enrollments where any course enrollment is tied to a B2B contract | ||
| const nonB2BProgramEnrollments = data.filter((programEnrollment) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment: The following is a scenario we do not handle well. I am not sure how it should behave, or if it is just something we say "Don't set up the data this way".
- Program 1 contains Courses A, B, C
- Program 2 contains Courses X, Y, and A
If a user is enrolled in Program 1 and an org that has Program 2, then as soon as they enroll in Course A via the org, then Program 1 disappears from their main dashboard.
Question: contracts can contain programs. Should we base this on "is program in contract" rather than "does program have course that is in contract"?
Not sure if this entirely solves the issue above, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory, if you're creating your B2B programs using the management commands in MITx Online, separate course runs / enrollments are created for that specific program tied to the B2B contract. So, not only is this a "don't set up the data that way" situation, MITx Online doesn't really do that unless you are a developer that is manually wiring up test data and taking shortcuts. It would take someone manually adding a B2B course run to their non-B2B program.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MITx Online doesn't really do that unless you are a developer that is manually wiring up test data and taking shortcuts.
Based on my testing, I don't think this is accurate. It's true that separate runs are created for b2b courses, but in this scenario
- Program 1 contains Courses A, B, C ... enrolled in Program 1 normally
- Program 2 contains Courses X, Y, and A ... enrolled in Course A b2b run via org
the api/v2/program_enrollments response includes b2b runs under both programs:
[
{
program: { title: "Program 1" },
enrollments: [
{ run: { title: 'Course B' }, b2b_contract_id: null }, // normal enrollment
{ run: { title: 'Course A' }, b2b_contract_id: 1 } // b2b enrollment via Program 2
]
},
{
program: { title: "Program 2" },
enrollments: [
{ run: { title: 'Course X' }, b2b_contract_id: 1 }, // b2b enrollment via Program 2
{ run: { title: 'Course A' }, b2b_contract_id: 1 } // b2b enrollment via Program 2
]
}
]
You can see the relevant code here; it just checks that "run is in program's course".
Suggestion: I do think the above situation is handled better by deciding b2b vs non-b2b at the program level:
- Program is b2b: shows up under relevant contract page
- "program is b2b" = belongs to one of your org contracts, I guess
- probably only show b2b courserun enrollments for that contract
- Program is non-b2b: shows up under dashboard home.
- probably only show non-b2b enrollments?
|
|
||
| test("transforms requirement tree correctly", () => { | ||
| const program = factories.programs.program({ | ||
| req_tree: [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * Build req_tree data for tests: |
…ith a configurable click action
…and off in dashboard home when clicking program card CTA's
…program contents are being displayed
dc317fd to
9523114
Compare
ChristopherChudzicki
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| await act(async () => { | ||
| await new Promise((resolve) => setTimeout(resolve, 100)) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wish we had a better abstraction for this. For testing the presence of something, findBy is better than waiting for queries to resolve. But I don't see any way to test for absence other than what you've done.
If you wanted to avoid the explicit numeric timeout, I think
await waitFor(()= > { expect(queryClient.isFetching()).toBe(0) })
would work, though I haven't tried it. (queryClient.isFetching() returns the number of queries that are currently fetching; queryClient is returned by renderWithProviders.)
| await waitFor( | ||
| () => { | ||
| const coreCourses = screen.queryByText("Core Courses") | ||
| expect(coreCourses).toBeInTheDocument() | ||
| }, | ||
| { timeout: 2000 }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be await screen.findByText("Core Courses", {}, { timeout: 2000 }) (The second arg is options related to the selector, the third arg waitForOptions.
| if ( | ||
| status === 403 && | ||
| err.response?.data?.detail === | ||
| "Authentication credentials were not provided." | ||
| ) { | ||
| // For now, we don't want to throw an error if the user is not authenticated. | ||
| return false | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this happening to you on RC/prod/local?
User is authenticated on MIT Learn, but not on MITxOnline?
Must be a race condition?
…t in a b2b contract, not the presence of a b2b_contract_id on one of the enrollments
ChristopherChudzicki
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
noticed two more accessibility things
| <Typography variant="h3" paddingBottom="32px"> | ||
| {program?.title} | ||
| </Typography> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Request: I think this should be component="h1".
| <Typography variant="subtitle2" color={theme.custom.colors.red}> | ||
| {section.title} | ||
| </Typography> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Request: Let's make these component="h2".
| : section.courses.length | ||
|
|
||
| return ( | ||
| <React.Fragment key={index}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sure it doesn't matter much, but I'd add a key: id prop to the requirementSections (from the req_tree node id) and use that instead.

What are the relevant tickets?
Closes https://github.com/mitodl/hq/issues/9106
Description (What does it do?)
This PR adds support for listing program enrollments in the "My Learning" section of the dashboard home page. A program detail view (ProgramContent.tsx) was added to the dashboard to handle displaying the courses from an individual program as a page in the dashboard. The
EnrollmentDisplaycomponent was modified to accept aprogramIdargument. If this is passed, the component will render the courses / enrollments from that program, rather than the user's base enrollments which is what it displays by default. The program cards in the My Learning section link to this program dashboard page. The course cards on this program page currently just display an alert saying "Non-B2B course enrollment is not yet implemented." This is because the non-B2B ecommerce checkout dialogs are not ready to go here in MIT Learn, and this functionality is still feature flagged.Screenshots (if appropriate):
How can this be tested?
Programwith some courses added to it as requirementsProgramEnrollmentobject tied to said program and your user/dashboard/program/1or whatever number your program is