Conversation
There was a problem hiding this comment.
Pull request overview
Updates the MITx Online dashboard/product UI to support “program as course” presentation and splits the previous combined enrollment display into more focused components, including new program-specific card and progress badge UI.
Changes:
- Replace the old
EnrollmentDisplaywrapper withAllEnrollmentsDisplay(home) andProgramEnrollmentDisplay(program page). - Add new “program as course” UI via
ProgramAsCourseCardandProgressBadge, and route program enrollment cards based ondisplay_mode. - Refactor helpers/tests to distinguish course-run vs program enrollment status.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| frontends/main/src/app-pages/ProductPages/MitxOnlineCourseCard.tsx | Removes now-unneeded comment around image fallback behavior. |
| frontends/main/src/app-pages/DashboardPage/ProgramContent.tsx | Switches program dashboard content to ProgramEnrollmentDisplay. |
| frontends/main/src/app-pages/DashboardPage/ProgramContent.test.tsx | Updates mocks/assertions for ProgramEnrollmentDisplay. |
| frontends/main/src/app-pages/DashboardPage/HomeContent.tsx | Uses AllEnrollmentsDisplay for the “My Learning” section. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/helpers.ts | Renames course-run status helper and adds program enrollment status helper. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/helpers.test.tsx | Updates tests for renamed course-run status helper. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgressBadge.tsx | Adds a new badge component for enrollment/progress state. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramEnrollmentDisplay.tsx | Extracts program requirements/enrollment UI into a dedicated component. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramAsCourseCard.tsx | Adds the “program as course” card UI (modules list + dates popover). |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx | Removes the old combined component implementation. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.test.tsx | Updates dashboard dialog tests to render AllEnrollmentsDisplay. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx | Refactors card layout; routes program cards to ProgramAsCourseCard when display_mode is “Course”; updates CTA label logic. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.test.tsx | Updates CTA text expectations after noun removal. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/AllEnrollmentsDisplay.tsx | Introduces standalone “My Learning” (all enrollments) display component. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/AllEnrollmentsDisplay.test.tsx | Updates tests to use AllEnrollmentsDisplay + ProgramEnrollmentDisplay directly. |
| frontends/main/src/app-pages/DashboardPage/ContractContent.tsx | Removes now-deprecated noun="Module" usage on DashboardCard. |
|
|
||
| // Build sections from requirement tree | ||
| const requirementSections = | ||
| program?.req_tree |
| {} as Record<number, typeof rawEnrollments>, | ||
| ) | ||
|
|
||
| const firstRequirementSection = program?.req_tree.find( |
| <LinkButton | ||
| onClick={(event) => setPopoverAnchorEl(event.currentTarget)} | ||
| title="" | ||
| tooltip="" | ||
| aria-label="" | ||
| > |
| {startDatePopoverString && endDatePopoverString && daysTillEnd && ( | ||
| <> | ||
| <HorizontalSeparator /> | ||
| <Popover | ||
| anchorEl={popoverAnchorEl} | ||
| open={!!popoverAnchorEl} | ||
| onClose={() => setPopoverAnchorEl(null)} | ||
| > | ||
| <DatePopoverContent> | ||
| <Stack direction="column" gap="4px"> | ||
| <Typography | ||
| variant="subtitle3" | ||
| color={theme.custom.colors.black} | ||
| > | ||
| Important Dates: | ||
| </Typography> | ||
| <Typography | ||
| variant="body3" | ||
| color={theme.custom.colors.black} | ||
| > | ||
| This course{" "} | ||
| <Typography variant="subtitle3" component="span"> | ||
| started | ||
| </Typography>{" "} | ||
| on {startDatePopoverString} | ||
| </Typography> | ||
| </Stack> | ||
| <Typography | ||
| variant="body3" | ||
| color={theme.custom.colors.black} | ||
| > | ||
| This course will{" "} | ||
| <Typography variant="subtitle3" component="span"> | ||
| end | ||
| </Typography>{" "} | ||
| in {daysTillEnd} days on {endDatePopoverString} | ||
| </Typography> | ||
| </DatePopoverContent> | ||
| </Popover> | ||
| <LinkButton | ||
| onClick={(event) => setPopoverAnchorEl(event.currentTarget)} | ||
| title="" | ||
| tooltip="" | ||
| aria-label="" | ||
| > | ||
| <Typography | ||
| variant="body2" | ||
| color={theme.custom.colors.silverGrayDark} | ||
| > | ||
| Ends {linkButtonEndDate} | ||
| </Typography> | ||
| </LinkButton> | ||
| </> | ||
| )} |
| const getProgramEnrollmentStatus = ( | ||
| programEnrollment: V3UserProgramEnrollment | undefined, | ||
| enrolledCourseCount: number, | ||
| ): EnrollmentStatus => { | ||
| if (!programEnrollment) { | ||
| return EnrollmentStatus.NotEnrolled | ||
| } | ||
|
|
||
| if (programEnrollment.certificate) { | ||
| return EnrollmentStatus.Completed | ||
| } | ||
|
|
||
| if (enrolledCourseCount > 0) { | ||
| return EnrollmentStatus.Enrolled | ||
| } | ||
|
|
||
| return EnrollmentStatus.NotEnrolled | ||
| } |
| const isCourseDisplayMode = | ||
| resource.data.program.display_mode === DisplayModeEnum.Course | ||
|
|
||
| if (isCourseDisplayMode) { | ||
| return ( | ||
| <ProgramAsCourseCard | ||
| programId={resource.data.program.id} | ||
| Component={Component} | ||
| className={className} | ||
| variant={variant} | ||
| /> |
| const extractCoursesFromNode = (node: V2ProgramRequirement): number[] => { | ||
| const courses: number[] = [] | ||
|
|
||
| if (node.data.node_type === "course" && node.data.course) { | ||
| courses.push(node.data.course) | ||
| } | ||
|
|
||
| if (node.children) { | ||
| node.children.forEach((child) => { | ||
| courses.push(...extractCoursesFromNode(child)) | ||
| }) | ||
| } | ||
|
|
||
| return courses | ||
| } |
| const ProgressBadge: React.FC<ProgressBadgeProps> = ({ enrollmentStatus }) => { | ||
| const label = | ||
| enrollmentStatus === EnrollmentStatus.Completed | ||
| ? "Completed" | ||
| : enrollmentStatus === EnrollmentStatus.Enrolled | ||
| ? "In Progress" | ||
| : "Not Started" | ||
|
|
4202381 to
abe114f
Compare
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramAsCourseCard.tsx
Show resolved
Hide resolved
eef7e2e to
4e18a95
Compare
| if (!enrolledInProgram) { | ||
| return <NotFoundPage /> | ||
| } |
There was a problem hiding this comment.
Bug: The component renders a NotFoundPage on API failure because it doesn't handle the isError state from the programEnrollments query, treating an error as a 'not enrolled' case.
Severity: MEDIUM
Suggested Fix
Check the isError status from the useQuery hook for programEnrollments. If isError is true, render an appropriate error message or a null state instead of the NotFoundPage. This will distinguish between a failed API request and a user who is genuinely not enrolled.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramEnrollmentDisplay.tsx#L224-L226
Potential issue: In the `ProgramEnrollmentDisplay` component, if the API call to fetch
`programEnrollments` fails, the `useQuery` hook returns `undefined` data. Due to
optional chaining, the `enrolledInProgram` variable also becomes `undefined`. The
component logic only checks for the `isLoading` state and not the `isError` state.
Consequently, after loading completes, the condition `!enrolledInProgram` evaluates to
true, causing the component to render a `NotFoundPage`. This misleads the user by
presenting a 'not found' error instead of indicating a temporary API or network issue.
…ogram / "program as course" and actual courses
…ngs to be more consistent with what they are and how they're used
…ase issues that discarded changes in files that have been renamed / moved
…ests for the whole component
…s own card and not use the stacked display
4e18a95 to
0a25023
Compare
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx
Show resolved
Hide resolved
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramEnrollmentDisplay.tsx
Show resolved
Hide resolved
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramEnrollmentDisplay.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
This PR updates the MITx Online dashboard “program as course” experience by splitting the old EnrollmentDisplay into focused components, adding new UI for program-as-course cards, and refactoring dashboard cards/CTAs to support the new designs.
Changes:
- Replace the monolithic
EnrollmentDisplaywithAllEnrollmentsDisplay(dashboard home) andProgramEnrollmentDisplay(program detail). - Add “program as course” UI pieces (
ProgramAsCourseCard,ProgressBadge) and extend helpers for program enrollment status. - Refactor
DashboardCardCTA logic/styling and update/replace related tests.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| frontends/main/src/app-pages/ProductPages/MitxOnlineCourseCard.tsx | Removes a now-unneeded comment around feature image fallback behavior. |
| frontends/main/src/app-pages/DashboardPage/ProgramContent.tsx | Switches program dashboard content to use ProgramEnrollmentDisplay. |
| frontends/main/src/app-pages/DashboardPage/ProgramContent.test.tsx | Updates component mocking/tests to reflect ProgramEnrollmentDisplay. |
| frontends/main/src/app-pages/DashboardPage/HomeContent.tsx | Uses AllEnrollmentsDisplay for “My Learning” on the home dashboard. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/helpers.ts | Renames course-run status helper and adds program enrollment status helper. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/helpers.test.tsx | Updates tests for renamed course-run enrollment status helper. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgressBadge.tsx | Adds a new badge component for “Not Started / In Progress / Completed”. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramEnrollmentDisplay.tsx | Introduces program requirement tree display (courses + program-as-course items). |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramEnrollmentDisplay.test.tsx | Adds test coverage for ProgramEnrollmentDisplay behaviors and ordering. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramAsCourseCard.tsx | Adds a stacked “program as course” card with progress + date popover. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramAsCourseCard.test.tsx | Adds tests for program-as-course card UI and date popover logic. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentStatusIndicator.tsx | Deletes the old status indicator component (replaced by new UI). |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx | Deletes the old combined enrollment display component. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.test.tsx | Removes tests for the deleted EnrollmentDisplay. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.test.tsx | Updates dialog tests to render AllEnrollmentsDisplay instead of removed component. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx | Refactors card layout and CTA rendering; adds program-as-course branching. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.test.tsx | Updates CTA expectations and adds tests for CTA style mapping helper. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/AllEnrollmentsDisplay.tsx | New component implementing the old “My Learning” list behavior. |
| frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/AllEnrollmentsDisplay.test.tsx | Adds test coverage for AllEnrollmentsDisplay. |
| frontends/main/src/app-pages/DashboardPage/ContractContent.tsx | Removes deprecated noun prop usage from DashboardCard calls. |
| frontends/main/src/app-pages/DashboardPage/ContractContent.test.tsx | Removes assertions tied to deleted enrollment status indicator UI. |
| const modules = (programCourses?.results || []).filter((course) => | ||
| moduleIds.includes(course.id), | ||
| ) | ||
|
|
|
|
||
| const programEnrollmentStatus = getProgramEnrollmentStatus( | ||
| programEnrollment, | ||
| enrolledCount, |
| title="" | ||
| tooltip="" | ||
| aria-label="" |
| return EnrollmentStatus.Enrolled | ||
| } | ||
|
|
||
| return EnrollmentStatus.NotEnrolled |
| @@ -412,15 +436,13 @@ const CoursewareButton = styled( | |||
| href, | |||
| disabled, | |||
| className, | |||
| noun, | |||
| isProgram, | |||
| onClick, | |||
| isPending, | |||
| ...others | |||
| }: CoursewareButtonProps) => { | |||
| const coursewareText = getCoursewareTextAndIcon({ | |||
| const coursewareText = getCoursewareButtonStyle({ | |||
| endDate, | |||
| noun, | |||
| enrollmentStatus, | |||
| isProgram, | |||
| }) | |||
| @@ -430,16 +452,15 @@ const CoursewareButton = styled( | |||
| // Programs or enrolled courses with started runs: show link | |||
| if ((isProgram || hasEnrolled) && (hasStarted || !startDate) && href) { | |||
| return ( | |||
| <ButtonLink | |||
| <StyledCoursewareButtonLink | |||
| size="small" | |||
| variant="primary" | |||
| endIcon={coursewareText.endIcon} | |||
| variant={coursewareText.variant} | |||
| href={href} | |||
| className={className} | |||
| {...others} | |||
| > | |||
| {coursewareText.text} | |||
| </ButtonLink> | |||
| </StyledCoursewareButtonLink> | |||
| ) | |||
| } | |||
|
|
|||
| @@ -451,26 +472,22 @@ const CoursewareButton = styled( | |||
| ) | |||
| EnrollmentStatus.Completed | ||
| ) | ||
| } else { | ||
| return false |
| You have completed | ||
| <Typography component="span" variant="subtitle2"> | ||
| {" "} | ||
| {completedCount} of {totalCount} courses{" "} |
| const sectionCompletedCount = section.items.filter((item) => { | ||
| if (item.resourceType === "course") { | ||
| const bestEnrollment = selectBestEnrollment( | ||
| item.data, | ||
| enrollmentsByCourseId[item.data.id] || [], | ||
| ) | ||
| return ( | ||
| getCourseRunEnrollmentStatus(bestEnrollment) === | ||
| EnrollmentStatus.Completed | ||
| ) | ||
| } else { | ||
| return false | ||
| } | ||
| }).length |
What are the relevant tickets?
Closes https://github.com/mitodl/hq/issues/10521
Description (What does it do?)
This PR implements some design changes shown in the wireframes in the issue above. Support for displaying programs with
display_mode: "course"was added, and some styles onDashboardCardwere updated across the board based on the new designs. Several files were also renamed / restructured.Screenshots (if appropriate):
How can this be tested?
display_mode: "courseDashboardCardin any conceivable scenario, checking what's rendered against the Figma wireframes in the issue above