Skip to content

Commit 0bca7e4

Browse files
authored
multiple enrollment display (#3534)
* add sibling enrollments to data model and add desktop multiple enrollmetns view * add expired run indicator * separate out sibling runs accordion into its own component, implement basic mobile design * add multiple enrollments display to dashboard home * add tests * fix typecheck * handle compact ui * add course id matching to sibling run filtering * fix accordion onchange * change upcoming run icon color to orange * fix isloading check on home enrollments display * add accessible label to view content link * filter out b2b runs * add aria-hidden to non-remix icon * add hover effect to runs count * fix rebase issue * add missing aria tags
1 parent 66a9718 commit 0bca7e4

11 files changed

Lines changed: 1229 additions & 37 deletions

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/CoursewareCard.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
* - "program-enrollment" → a program enrollment.
1313
*/
1414
import React from "react"
15-
import { type DashboardCourseEntry } from "./model/dashboardViewModel"
15+
import {
16+
type DashboardCourseEntry,
17+
filterVariantSiblings,
18+
} from "./model/dashboardViewModel"
1619
import type {
1720
CourseRunEnrollmentV3,
1821
V3UserProgramEnrollment,
@@ -57,6 +60,7 @@ type CoursewareCardEnrollmentProps = StyledComponentBaseProps &
5760
CourseDisplayProps & {
5861
kind: "enrollment"
5962
enrollment: CourseRunEnrollmentV3
63+
siblingEnrollments?: CourseRunEnrollmentV3[]
6064
}
6165

6266
/** A program enrollment. */
@@ -93,6 +97,7 @@ const CoursewareCard: React.FC<CoursewareCardProps> = (props) => {
9397
return (
9498
<EnrolledCourseCard
9599
enrollment={props.enrollment}
100+
siblingEnrollments={props.siblingEnrollments}
96101
layout={layout}
97102
headingLevel={headingLevel}
98103
onUpgradeError={onUpgradeError}
@@ -108,6 +113,10 @@ const CoursewareCard: React.FC<CoursewareCardProps> = (props) => {
108113
// Will likely diverge with multiple enrollment display.
109114
<EnrolledCourseCard
110115
enrollment={entry.displayedEnrollment}
116+
siblingEnrollments={filterVariantSiblings(
117+
entry.enrollments,
118+
entry.displayedEnrollment,
119+
)}
111120
layout={layout}
112121
headingLevel={headingLevel}
113122
onUpgradeError={onUpgradeError}

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrolledCourseCard.test.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ describe.each([
379379
const enrollment = mitxonline.factories.enrollment.courseEnrollment({
380380
enrollment_mode: enrollmentMode,
381381
b2b_contract_id: b2bContractId,
382+
certificate: null,
382383
run,
383384
})
384385
renderWithProviders(<EnrolledCourseCard enrollment={enrollment} />)
@@ -454,6 +455,7 @@ describe.each([
454455
const enrollment = mitxonline.factories.enrollment.courseEnrollment({
455456
enrollment_mode: EnrollmentMode.Audit,
456457
b2b_contract_id: null,
458+
certificate: null,
457459
run: {
458460
is_upgradable: true,
459461
upgrade_deadline: deadline,
@@ -474,6 +476,7 @@ describe.each([
474476
const enrollment = mitxonline.factories.enrollment.courseEnrollment({
475477
enrollment_mode: EnrollmentMode.Audit,
476478
b2b_contract_id: null,
479+
certificate: null,
477480
run: {
478481
is_upgradable: true,
479482
upgrade_deadline: null,
@@ -496,6 +499,7 @@ describe.each([
496499
const enrollment = mitxonline.factories.enrollment.courseEnrollment({
497500
enrollment_mode: EnrollmentMode.Audit,
498501
b2b_contract_id: null,
502+
certificate: null,
499503
run: {
500504
is_upgradable: true,
501505
upgrade_deadline: faker.date.future().toISOString(),
@@ -529,6 +533,7 @@ describe.each([
529533
const enrollment = mitxonline.factories.enrollment.courseEnrollment({
530534
enrollment_mode: EnrollmentMode.Audit,
531535
b2b_contract_id: null,
536+
certificate: null,
532537
run: {
533538
is_upgradable: true,
534539
upgrade_deadline: faker.date.future().toISOString(),
@@ -734,3 +739,72 @@ describe.each([
734739
windowOpenSpy.mockRestore()
735740
})
736741
})
742+
743+
describe("EnrolledCourseCard — multiple enrollment runs", () => {
744+
setupLocationMock()
745+
746+
test("shows accordion on desktop when siblingEnrollments is non-empty", () => {
747+
setupUserApis()
748+
const enrollment = mitxonline.factories.enrollment.courseEnrollment({
749+
run: currentRunDates,
750+
})
751+
const siblings = [
752+
mitxonline.factories.enrollment.courseEnrollment(),
753+
mitxonline.factories.enrollment.courseEnrollment(),
754+
]
755+
renderWithProviders(
756+
<EnrolledCourseCard
757+
enrollment={enrollment}
758+
siblingEnrollments={siblings}
759+
/>,
760+
)
761+
const desktopCard = screen.getByTestId("enrollment-card-desktop")
762+
// 2 siblings + 1 current = 3
763+
expect(within(desktopCard).getByText("Course runs (3)")).toBeInTheDocument()
764+
})
765+
766+
test("does not show accordion on desktop when siblingEnrollments is empty", () => {
767+
setupUserApis()
768+
const enrollment = mitxonline.factories.enrollment.courseEnrollment()
769+
renderWithProviders(
770+
<EnrolledCourseCard enrollment={enrollment} siblingEnrollments={[]} />,
771+
)
772+
expect(screen.queryByText(/Course runs/)).not.toBeInTheDocument()
773+
})
774+
775+
test("does not show accordion when siblingEnrollments is omitted", () => {
776+
setupUserApis()
777+
const enrollment = mitxonline.factories.enrollment.courseEnrollment()
778+
renderWithProviders(<EnrolledCourseCard enrollment={enrollment} />)
779+
expect(screen.queryByText(/Course runs/)).not.toBeInTheDocument()
780+
})
781+
782+
test("shows accordion on mobile when siblingEnrollments is non-empty", () => {
783+
setupUserApis()
784+
const enrollment = mitxonline.factories.enrollment.courseEnrollment()
785+
const sibling = mitxonline.factories.enrollment.courseEnrollment()
786+
renderWithProviders(
787+
<EnrolledCourseCard
788+
enrollment={enrollment}
789+
siblingEnrollments={[sibling]}
790+
/>,
791+
)
792+
const mobileCard = screen.getByTestId("enrollment-card-mobile")
793+
expect(within(mobileCard).getByText("Course runs (2)")).toBeInTheDocument()
794+
})
795+
796+
test("accordion count reflects total runs (siblings + current)", () => {
797+
setupUserApis()
798+
const enrollment = mitxonline.factories.enrollment.courseEnrollment()
799+
const siblings = Array.from({ length: 4 }, () =>
800+
mitxonline.factories.enrollment.courseEnrollment(),
801+
)
802+
renderWithProviders(
803+
<EnrolledCourseCard
804+
enrollment={enrollment}
805+
siblingEnrollments={siblings}
806+
/>,
807+
)
808+
expect(screen.getAllByText("Course runs (5)").length).toBeGreaterThan(0)
809+
})
810+
})

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrolledCourseCard.tsx

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react"
2-
import { SimpleMenu, Stack } from "ol-components"
2+
import { SimpleMenu, Stack, styled } from "ol-components"
33
import {
44
CardRoot,
55
CardTypeText,
@@ -27,6 +27,7 @@ import { isVerifiedEnrollmentMode } from "@/common/mitxonline"
2727
import { RiArrowUpCircleLine, RiAwardLine, RiMore2Line } from "@remixicon/react"
2828
import { useReplaceBasketItem } from "@/common/mitxonline/useReplaceBasketItem"
2929
import { isInPast, calendarDaysUntil, NoSSR } from "ol-utilities"
30+
import { SiblingRunsAccordion } from "./SiblingRunsAccordion"
3031
import { EnrollmentStatusIndicator } from "./EnrollmentStatusIndicator"
3132
import { mitxUserQueries } from "api/mitxonline-hooks/user"
3233
import { useQuery } from "@tanstack/react-query"
@@ -109,8 +110,45 @@ const UpgradeBanner: React.FC<
109110
)
110111
}
111112

113+
const EnrolledCardShell = styled.div(({ theme }) => ({
114+
border: `1px solid ${theme.custom.colors.lightGray2}`,
115+
borderRadius: "8px",
116+
overflow: "hidden",
117+
backgroundColor: theme.custom.colors.white,
118+
[theme.breakpoints.down("md")]: {
119+
display: "none",
120+
},
121+
'&[data-layout="compact"]': {
122+
border: "none",
123+
borderBottom: `1px solid ${theme.custom.colors.lightGray2}`,
124+
borderRadius: "0px !important",
125+
boxShadow: "none",
126+
"&:first-of-type": {
127+
borderTopLeftRadius: "8px !important",
128+
borderTopRightRadius: "8px !important",
129+
},
130+
"&:last-of-type": {
131+
borderBottomLeftRadius: "8px !important",
132+
borderBottomRightRadius: "8px !important",
133+
borderBottom: "none",
134+
},
135+
},
136+
}))
137+
138+
const CardHeaderContent = styled.div({
139+
padding: "16px 16px 0 16px",
140+
display: "flex",
141+
gap: "8px",
142+
alignItems: "center",
143+
})
144+
145+
const MobileAccordionWrapper = styled.div({
146+
width: "100%",
147+
})
148+
112149
type EnrolledCourseCardProps = {
113150
enrollment: CourseRunEnrollmentV3
151+
siblingEnrollments?: CourseRunEnrollmentV3[]
114152
layout?: "default" | "compact"
115153
headingLevel?: "h2" | "h3" | "h4" | "h5" | "h6"
116154
onUpgradeError?: (error: string) => void
@@ -120,6 +158,7 @@ type EnrolledCourseCardProps = {
120158

121159
export const EnrolledCourseCard = ({
122160
enrollment,
161+
siblingEnrollments,
123162
layout = "default",
124163
headingLevel,
125164
onUpgradeError,
@@ -165,7 +204,12 @@ export const EnrolledCourseCard = ({
165204
})
166205
const upgradedAndIncomplete =
167206
!isContractPageResource && isVerifiedEnrollmentMode(enrollmentMode)
168-
const certStatus = showUpgradeBanner ? (
207+
const certStatus = certificateLink ? (
208+
<SubtitleLink href={certificateLink} layout={layout}>
209+
<RiAwardLine size="16px" />
210+
{isCompact ? "Certificate" : "View Certificate"}
211+
</SubtitleLink>
212+
) : showUpgradeBanner ? (
169213
<UpgradeBanner
170214
data-testid="upgrade-root"
171215
canUpgrade={showUpgradeBanner}
@@ -179,11 +223,6 @@ export const EnrolledCourseCard = ({
179223
)
180224
}}
181225
/>
182-
) : certificateLink ? (
183-
<SubtitleLink href={certificateLink} layout={layout}>
184-
<RiAwardLine size="16px" />
185-
{isCompact ? "Certificate" : "View Certificate"}
186-
</SubtitleLink>
187226
) : upgradedAndIncomplete ? (
188227
<UpgradedBanner />
189228
) : null
@@ -347,25 +386,50 @@ export const EnrolledCourseCard = ({
347386
/>
348387
)
349388

389+
const hasMultipleRuns = (siblingEnrollments?.length ?? 0) > 0
390+
350391
return (
351392
<>
352-
<CardRoot
353-
screenSize="desktop"
354-
data-testid="enrollment-card-desktop"
355-
as={Component}
356-
className={className}
357-
layout={layout}
358-
>
359-
<Stack justifyContent="start" alignItems="stretch" flex={1}>
360-
<CardTypeText>Course</CardTypeText>
361-
{titleSection}
362-
</Stack>
363-
<Stack direction="row" gap="8px" alignItems="center">
364-
{buttonSection}
365-
{contextMenu}
366-
</Stack>
367-
</CardRoot>
368-
393+
{hasMultipleRuns ? (
394+
<EnrolledCardShell
395+
data-testid="enrollment-card-desktop"
396+
data-layout={layout}
397+
className={className}
398+
as={Component}
399+
>
400+
<CardHeaderContent>
401+
<Stack justifyContent="start" alignItems="stretch" flex={1}>
402+
<CardTypeText>Course</CardTypeText>
403+
{titleSection}
404+
</Stack>
405+
<Stack direction="row" gap="8px" alignItems="center">
406+
{buttonSection}
407+
{contextMenu}
408+
</Stack>
409+
</CardHeaderContent>
410+
<SiblingRunsAccordion
411+
enrollment={enrollment}
412+
siblingEnrollments={siblingEnrollments ?? []}
413+
/>
414+
</EnrolledCardShell>
415+
) : (
416+
<CardRoot
417+
screenSize="desktop"
418+
data-testid="enrollment-card-desktop"
419+
as={Component}
420+
className={className}
421+
layout={layout}
422+
>
423+
<Stack justifyContent="start" alignItems="stretch" flex={1}>
424+
<CardTypeText>Course</CardTypeText>
425+
{titleSection}
426+
</Stack>
427+
<Stack direction="row" gap="8px" alignItems="center">
428+
{buttonSection}
429+
{contextMenu}
430+
</Stack>
431+
</CardRoot>
432+
)}
369433
<CardRoot
370434
screenSize="mobile"
371435
data-testid="enrollment-card-mobile"
@@ -394,6 +458,14 @@ export const EnrolledCourseCard = ({
394458
>
395459
{buttonSection}
396460
</Stack>
461+
{hasMultipleRuns && (
462+
<MobileAccordionWrapper>
463+
<SiblingRunsAccordion
464+
enrollment={enrollment}
465+
siblingEnrollments={siblingEnrollments ?? []}
466+
/>
467+
</MobileAccordionWrapper>
468+
)}
397469
</CardRoot>
398470
</>
399471
)

0 commit comments

Comments
 (0)