Skip to content

Commit f1baed0

Browse files
authored
Add support for ProgramCollection (#2369)
* temporarily use unreleased version of mitxonline-api-axios and implement hooks / transform for ProgramCollections * formatting or something? * Get program collections working in the most basic sense * Update api client and OrganizationContent to use the collections property of Program to filter * For now assume that ProgramCard always displays the first Course in the Program, and use the correct noun * add loading skeletons * org_id needs to be sent * fix most of the tests * move back to published package now that the API changes are released * fix test * reorganize program collection cards so queries are more efficient and add a test * add another test to ensure programs that are part of a collection are not rendered on their own * fix yarn lock again * remove unnecessary itemSpacing args
1 parent 42d0069 commit f1baed0

File tree

17 files changed

+401
-96
lines changed

17 files changed

+401
-96
lines changed

frontends/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"ol-test-utilities": "0.0.0"
3131
},
3232
"dependencies": {
33-
"@mitodl/mitxonline-api-axios": "^2025.6.23",
33+
"@mitodl/mitxonline-api-axios": "2025.7.28",
3434
"@tanstack/react-query": "^5.66.0",
3535
"axios": "^1.6.3"
3636
}

frontends/api/src/mitxonline/clients.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
B2bApi,
33
CoursesApi,
44
EnrollmentsApi,
5+
ProgramCollectionsApi,
56
ProgramsApi,
67
UsersApi,
78
} from "@mitodl/mitxonline-api-axios/v1"
@@ -23,13 +24,19 @@ const usersApi = new UsersApi(undefined, BASE_PATH, axiosInstance)
2324
const b2bApi = new B2bApi(undefined, BASE_PATH, axiosInstance)
2425
const enrollmentsApi = new EnrollmentsApi(undefined, BASE_PATH, axiosInstance)
2526
const programsApi = new ProgramsApi(undefined, BASE_PATH, axiosInstance)
27+
const programCollectionsApi = new ProgramCollectionsApi(
28+
undefined,
29+
BASE_PATH,
30+
axiosInstance,
31+
)
2632
const coursesApi = new CoursesApi(undefined, BASE_PATH, axiosInstance)
2733

2834
export {
2935
usersApi,
3036
b2bApi,
3137
enrollmentsApi,
3238
programsApi,
39+
programCollectionsApi,
3340
coursesApi,
3441
axiosInstance,
3542
}

frontends/api/src/mitxonline/hooks/courses/queries.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { queryOptions } from "@tanstack/react-query"
22
import type {
33
CoursesApiApiV2CoursesListRequest,
4-
PaginatedCourseWithCourseRunsList,
4+
PaginatedV2CourseWithCourseRunsList,
55
} from "@mitodl/mitxonline-api-axios/v1"
66
import { coursesApi } from "../../clients"
77

@@ -18,7 +18,7 @@ const coursesQueries = {
1818
coursesList: (opts?: CoursesApiApiV2CoursesListRequest) =>
1919
queryOptions({
2020
queryKey: coursesKeys.coursesList(opts),
21-
queryFn: async (): Promise<PaginatedCourseWithCourseRunsList> => {
21+
queryFn: async (): Promise<PaginatedV2CourseWithCourseRunsList> => {
2222
return coursesApi.apiV2CoursesList(opts).then((res) => res.data)
2323
},
2424
}),
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import { programsQueries } from "./queries"
1+
import { programsQueries, programCollectionQueries } from "./queries"
22

3-
export { programsQueries }
3+
export { programsQueries, programCollectionQueries }
Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,39 @@
11
import { queryOptions } from "@tanstack/react-query"
22
import type {
3+
PaginatedV2ProgramCollectionList,
34
PaginatedV2ProgramList,
5+
ProgramCollectionsApiProgramCollectionsListRequest,
46
ProgramsApiProgramsListV2Request,
7+
ProgramsApiProgramsRetrieveV2Request,
8+
V2Program,
59
} from "@mitodl/mitxonline-api-axios/v1"
6-
import { programsApi } from "../../clients"
10+
import { programCollectionsApi, programsApi } from "../../clients"
711

812
const programsKeys = {
913
root: ["mitxonline", "programs"],
14+
programDetail: (opts: ProgramsApiProgramsRetrieveV2Request) => [
15+
...programsKeys.root,
16+
"detail",
17+
opts,
18+
],
1019
programsList: (opts?: ProgramsApiProgramsListV2Request) => [
1120
...programsKeys.root,
1221
"list",
1322
opts,
1423
],
24+
programCollectionsList: (
25+
opts?: ProgramCollectionsApiProgramCollectionsListRequest,
26+
) => [...programsKeys.root, "collections", "list", opts],
1527
}
1628

1729
const programsQueries = {
30+
programDetail: (opts: ProgramsApiProgramsRetrieveV2Request) =>
31+
queryOptions({
32+
queryKey: programsKeys.programDetail(opts),
33+
queryFn: async (): Promise<V2Program> => {
34+
return programsApi.programsRetrieveV2(opts).then((res) => res.data)
35+
},
36+
}),
1837
programsList: (opts: ProgramsApiProgramsListV2Request) =>
1938
queryOptions({
2039
queryKey: programsKeys.programsList(opts),
@@ -24,4 +43,18 @@ const programsQueries = {
2443
}),
2544
}
2645

27-
export { programsQueries, programsKeys }
46+
const programCollectionQueries = {
47+
programCollectionsList: (
48+
opts: ProgramCollectionsApiProgramCollectionsListRequest,
49+
) =>
50+
queryOptions({
51+
queryKey: programsKeys.programCollectionsList(opts),
52+
queryFn: async (): Promise<PaginatedV2ProgramCollectionList> => {
53+
return programCollectionsApi
54+
.programCollectionsList(opts)
55+
.then((res) => res.data)
56+
},
57+
}),
58+
}
59+
60+
export { programsQueries, programCollectionQueries, programsKeys }

frontends/api/src/mitxonline/test-utils/factories/courses.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { mergeOverrides, makePaginatedFactory } from "ol-test-utilities"
22
import type { PartialFactory } from "ol-test-utilities"
3-
import type { CourseWithCourseRuns } from "@mitodl/mitxonline-api-axios/v1"
3+
import type { V2CourseWithCourseRuns } from "@mitodl/mitxonline-api-axios/v1"
44
import { faker } from "@faker-js/faker/locale/en"
55
import { UniqueEnforcer } from "enforce-unique"
66

77
const uniqueCourseId = new UniqueEnforcer()
88
const uniqueCourseRunId = new UniqueEnforcer()
99

10-
const course: PartialFactory<CourseWithCourseRuns> = (overrides = {}) => {
11-
const defaults: CourseWithCourseRuns = {
10+
const course: PartialFactory<V2CourseWithCourseRuns> = (overrides = {}) => {
11+
const defaults: V2CourseWithCourseRuns = {
1212
id: uniqueCourseId.enforce(() => faker.number.int()),
1313
title: faker.lorem.words(3),
1414
readable_id: faker.lorem.slug(),
@@ -80,9 +80,13 @@ const course: PartialFactory<CourseWithCourseRuns> = (overrides = {}) => {
8080
approved_flexible_price_exists: faker.datatype.boolean(),
8181
},
8282
],
83+
min_price: faker.number.int({ min: 0, max: 1000 }),
84+
max_price: faker.number.int({ min: 1000, max: 2000 }),
85+
include_in_learn_catalog: faker.datatype.boolean(),
86+
ingest_content_files_for_ai: faker.datatype.boolean(),
8387
}
8488

85-
return mergeOverrides<CourseWithCourseRuns>(defaults, overrides)
89+
return mergeOverrides<V2CourseWithCourseRuns>(defaults, overrides)
8690
}
8791

8892
const courses = makePaginatedFactory(course)

frontends/api/src/mitxonline/test-utils/factories/programs.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { mergeOverrides, makePaginatedFactory } from "ol-test-utilities"
22
import type { PartialFactory } from "ol-test-utilities"
3-
import type { V2Program } from "@mitodl/mitxonline-api-axios/v1"
3+
import type {
4+
V2Program,
5+
V2ProgramCollection,
6+
} from "@mitodl/mitxonline-api-axios/v1"
47
import { faker } from "@faker-js/faker/locale/en"
58
import { UniqueEnforcer } from "enforce-unique"
69

@@ -33,10 +36,17 @@ const program: PartialFactory<V2Program> = (overrides = {}) => {
3336
],
3437
live: faker.datatype.boolean(),
3538
courses: [],
39+
collections: [],
3640
req_tree: [],
3741
requirements: {
38-
required: [faker.number.int()],
39-
electives: [faker.number.int()],
42+
courses: {
43+
required: [faker.number.int()],
44+
electives: [faker.number.int()],
45+
},
46+
programs: {
47+
required: [faker.number.int()],
48+
electives: [faker.number.int()],
49+
},
4050
},
4151
certificate_type: faker.lorem.word(),
4252
topics: [
@@ -52,11 +62,27 @@ const program: PartialFactory<V2Program> = (overrides = {}) => {
5262
availability: faker.helpers.arrayElement(["anytime", "dated"]),
5363
min_weekly_hours: `${faker.number.int({ min: 1, max: 5 })} hours`,
5464
max_weekly_hours: `${faker.number.int({ min: 6, max: 10 })} hours`,
65+
start_date: faker.date.past().toISOString(),
5566
}
5667

5768
return mergeOverrides<V2Program>(defaults, overrides)
5869
}
5970

6071
const programs = makePaginatedFactory(program)
6172

62-
export { program, programs }
73+
const programCollection: PartialFactory<V2ProgramCollection> = (
74+
overrides = {},
75+
) => {
76+
const defaults: V2ProgramCollection = {
77+
id: uniqueProgramId.enforce(() => faker.number.int()),
78+
description: faker.lorem.paragraph(),
79+
programs: programs({ count: 2 }).results.map((p) => p.id),
80+
title: faker.lorem.words(3),
81+
created_on: faker.date.past().toISOString(),
82+
updated_on: faker.date.recent().toISOString(),
83+
}
84+
85+
return mergeOverrides<V2ProgramCollection>(defaults, overrides)
86+
}
87+
88+
export { program, programs, programCollection }

frontends/api/src/mitxonline/test-utils/urls.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
CoursesApiApiV2CoursesListRequest,
3+
ProgramCollectionsApiProgramCollectionsListRequest,
34
ProgramsApiProgramsListV2Request,
45
} from "@mitodl/mitxonline-api-axios/v1"
56
import { RawAxiosRequestConfig } from "axios"
@@ -13,6 +14,7 @@ const currentUser = {
1314
}
1415

1516
const enrollment = {
17+
enrollmentsList: () => `${API_BASE_URL}/api/v1/enrollments/`,
1618
courseEnrollment: (id?: number) =>
1719
`${API_BASE_URL}/api/v1/enrollments/${id ? `${id}/` : ""}`,
1820
}
@@ -25,6 +27,13 @@ const b2b = {
2527
const programs = {
2628
programsList: (opts?: ProgramsApiProgramsListV2Request) =>
2729
`${API_BASE_URL}/api/v2/programs/${queryify(opts)}`,
30+
programDetail: (id: number) => `${API_BASE_URL}/api/v2/programs/${id}/`,
31+
}
32+
33+
const programCollections = {
34+
programCollectionsList: (
35+
opts?: ProgramCollectionsApiProgramCollectionsListRequest,
36+
) => `${API_BASE_URL}/api/v2/program-collections/${queryify(opts)}`,
2837
}
2938

3039
const courses = {
@@ -37,4 +46,12 @@ const organization = {
3746
`${API_BASE_URL}/api/v0/b2b/organizations/${organizationSlug}/`,
3847
}
3948

40-
export { b2b, currentUser, enrollment, programs, courses, organization }
49+
export {
50+
b2b,
51+
currentUser,
52+
enrollment,
53+
programs,
54+
programCollections,
55+
courses,
56+
organization,
57+
}

frontends/main/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"@emotion/cache": "^11.13.1",
1515
"@emotion/styled": "^11.11.0",
1616
"@mitodl/course-search-utils": "3.3.2",
17-
"@mitodl/mitxonline-api-axios": "^2025.6.23",
17+
"@mitodl/mitxonline-api-axios": "2025.7.28",
1818
"@mitodl/smoot-design": "^6.10.0",
1919
"@next/bundle-analyzer": "^14.2.15",
2020
"@remixicon/react": "^4.2.0",

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {
1010
} from "ol-components"
1111
import NextLink from "next/link"
1212
import { EnrollmentStatus, EnrollmentMode } from "./types"
13-
import type { DashboardCourse, DashboardCourseEnrollment } from "./types"
13+
import type {
14+
DashboardResource,
15+
DashboardCourse,
16+
DashboardCourseEnrollment,
17+
} from "./types"
1418
import { ActionButton, Button, ButtonLink } from "@mitodl/smoot-design"
1519
import {
1620
RiArrowRightLine,
@@ -309,7 +313,7 @@ const CourseStartCountdown: React.FC<{
309313

310314
type DashboardCardProps = {
311315
Component?: React.ElementType
312-
dashboardResource: DashboardCourse
316+
dashboardResource: DashboardResource
313317
showNotComplete?: boolean
314318
className?: string
315319
courseNoun?: string
@@ -319,6 +323,7 @@ type DashboardCardProps = {
319323
titleHref?: string | null
320324
buttonHref?: string | null
321325
}
326+
322327
const DashboardCard: React.FC<DashboardCardProps> = ({
323328
dashboardResource,
324329
showNotComplete = true,
@@ -331,7 +336,8 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
331336
titleHref,
332337
buttonHref,
333338
}) => {
334-
const { title, marketingUrl, enrollment, run } = dashboardResource
339+
const course = dashboardResource as DashboardCourse
340+
const { title, marketingUrl, enrollment, run } = course
335341
const titleSection = isLoading ? (
336342
<>
337343
<Skeleton variant="text" width="95%" height={16} />
@@ -373,7 +379,7 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
373379
/>
374380
<CoursewareButton
375381
data-testid="courseware-button"
376-
coursewareId={dashboardResource.coursewareId}
382+
coursewareId={course.coursewareId}
377383
startDate={run.startDate}
378384
enrollmentStatus={enrollment?.status}
379385
href={buttonHref ? buttonHref : run.coursewareUrl}

0 commit comments

Comments
 (0)