Skip to content

Commit 1cafec1

Browse files
authored
fix program collections in org dashboards (#2625)
* update mitxonline api package and update program collection signature * fix issue with non-unique key on program collection loading skeletons * program in req_tree is now a number (the ID) * update UserOrganization to OrganizationPage * use index instead of random number for skeleton key * add some spacing between program collections if there happens to be multiple
1 parent b02658f commit 1cafec1

File tree

11 files changed

+95
-30
lines changed

11 files changed

+95
-30
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.10.14",
33+
"@mitodl/mitxonline-api-axios": "^2025.10.21",
3434
"@tanstack/react-query": "^5.66.0",
3535
"axios": "^1.12.2"
3636
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ const programPageItem: PartialFactory<ProgramPageItem> = (override) => {
285285
node_type: "operator",
286286
operator: "all_of",
287287
operator_value: null,
288-
program: faker.lorem.slug(),
288+
program: faker.number.int(),
289289
course: null,
290290
title: "Required Courses",
291291
elective_flag: false,
@@ -297,7 +297,7 @@ const programPageItem: PartialFactory<ProgramPageItem> = (override) => {
297297
node_type: "operator",
298298
operator: "min_number_of",
299299
operator_value: "2",
300-
program: faker.lorem.slug(),
300+
program: faker.number.int(),
301301
course: null,
302302
title: "Elective Courses",
303303
elective_flag: true,

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ const programCollection: PartialFactory<V2ProgramCollection> = (
8585
const defaults: V2ProgramCollection = {
8686
id: uniqueProgramId.enforce(() => faker.number.int()),
8787
description: faker.lorem.paragraph(),
88-
programs: programs({ count: 2 }).results.map((p) => p.id),
88+
programs: programs({ count: 2 }).results.map((p, i) => {
89+
return {
90+
id: p.id,
91+
title: p.title,
92+
order: i + 1,
93+
}
94+
}),
8995
title: faker.lorem.words(3),
9096
created_on: faker.date.past().toISOString(),
9197
updated_on: faker.date.recent().toISOString(),

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
User,
55
LegalAddress,
66
UserProfile,
7-
UserOrganization,
7+
OrganizationPage,
88
} from "@mitodl/mitxonline-api-axios/v2"
99
import { UniqueEnforcer } from "enforce-unique"
1010

@@ -46,7 +46,7 @@ const userProfile = (): UserProfile => ({
4646
type_is_other: faker.datatype.boolean(),
4747
})
4848

49-
const userOrganization = (): UserOrganization => ({
49+
const userOrganization = (): OrganizationPage => ({
5050
id: faker.number.int(),
5151
name: faker.company.name(),
5252
description: faker.company.catchPhrase(),

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.4.1",
17-
"@mitodl/mitxonline-api-axios": "^2025.10.14",
17+
"@mitodl/mitxonline-api-axios": "^2025.10.21",
1818
"@mitodl/smoot-design": "^6.17.1",
1919
"@next/bundle-analyzer": "^14.2.15",
2020
"@react-pdf/renderer": "^4.3.0",

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ const createTestContracts = (
303303
name: faker.company.name(),
304304
organization: orgId,
305305
slug: faker.lorem.slug(),
306+
membership_type: faker.helpers.arrayElement(["managed", "unmanaged"]),
306307
}))
307308
}
308309

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ const mitxonlineProgramCollection = (
270270
type: DashboardResourceType.ProgramCollection,
271271
title: raw.title,
272272
description: raw.description ?? null,
273-
programIds: raw.programs,
273+
programs: raw.programs,
274274
}
275275
}
276276

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,13 @@ type DashboardProgramCollection = {
8181
type: typeof DashboardResourceType.ProgramCollection
8282
title: string
8383
description?: string | null
84-
programIds: number[]
84+
programs: DashboardProgramCollectionProgram[]
85+
}
86+
87+
type DashboardProgramCollectionProgram = {
88+
id?: number
89+
title?: string
90+
order?: number
8591
}
8692

8793
type DashboardResource =
@@ -97,4 +103,5 @@ export type {
97103
DashboardCourseEnrollment,
98104
DashboardProgram,
99105
DashboardProgramCollection,
106+
DashboardProgramCollectionProgram,
100107
}

frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,18 @@ describe("OrganizationContent", () => {
144144
setupProgramsAndCourses()
145145

146146
// Set up the collection to include both programs in a specific order
147-
programCollection.programs = [programB.id, programA.id] // Note: B first, then A
147+
programCollection.programs = [
148+
{
149+
id: programB.id,
150+
title: programB.title,
151+
order: 1,
152+
},
153+
{
154+
id: programA.id,
155+
title: programA.title,
156+
order: 2,
157+
},
158+
] // Note: B first, then A
148159
setMockResponse.get(urls.programCollections.programCollectionsList(), {
149160
results: [programCollection],
150161
})
@@ -200,7 +211,13 @@ describe("OrganizationContent", () => {
200211
const { orgX, programA, programCollection, coursesA } =
201212
setupProgramsAndCourses()
202213

203-
programCollection.programs = [programA.id]
214+
programCollection.programs = [
215+
{
216+
id: programA.id,
217+
title: programA.title,
218+
order: 1,
219+
},
220+
]
204221
setMockResponse.get(urls.programCollections.programCollectionsList(), {
205222
results: [programCollection],
206223
})
@@ -240,7 +257,18 @@ describe("OrganizationContent", () => {
240257
setupProgramsAndCourses()
241258

242259
// Set up the collection to include both programs
243-
programCollection.programs = [programA.id, programB.id]
260+
programCollection.programs = [
261+
{
262+
id: programB.id,
263+
title: programB.title,
264+
order: 1,
265+
},
266+
{
267+
id: programA.id,
268+
title: programA.title,
269+
order: 2,
270+
},
271+
]
244272
setMockResponse.get(urls.programCollections.programCollectionsList(), {
245273
results: [programCollection],
246274
})
@@ -332,7 +360,18 @@ describe("OrganizationContent", () => {
332360
const programANoCourses = { ...programA, courses: [] }
333361

334362
// Set up the collection to include both programs
335-
programCollection.programs = [programANoCourses.id, programB.id]
363+
programCollection.programs = [
364+
{
365+
id: programANoCourses.id,
366+
title: programANoCourses.title,
367+
order: 1,
368+
},
369+
{
370+
id: programB.id,
371+
title: programB.title,
372+
order: 2,
373+
},
374+
]
336375
setMockResponse.get(urls.programCollections.programCollectionsList(), {
337376
results: [programCollection],
338377
})

frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { PlainList, Skeleton, Stack, styled, Typography } from "ol-components"
1919
import {
2020
DashboardProgram,
2121
DashboardProgramCollection,
22+
DashboardProgramCollectionProgram,
2223
} from "./CoursewareDisplay/types"
2324
import graduateLogo from "@/public/images/dashboard/graduate.png"
2425
import {
@@ -129,25 +130,36 @@ const ProgramDescription = styled(Typography)({
129130
},
130131
})
131132

133+
const ProgramCollectionsList = styled(PlainList)({
134+
display: "flex",
135+
flexDirection: "column",
136+
gap: "40px",
137+
})
138+
132139
// Custom hook to handle multiple program queries and check if any have courses
133-
const useProgramCollectionCourses = (programIds: number[], orgId: number) => {
140+
const useProgramCollectionCourses = (
141+
programs: DashboardProgramCollectionProgram[],
142+
orgId: number,
143+
) => {
134144
const programQueries = useQueries({
135-
queries: programIds.map((programId) =>
136-
programsQueries.programsList({ id: programId, org_id: orgId }),
137-
),
145+
queries: programs
146+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
147+
.map((program) =>
148+
programsQueries.programsList({ id: program.id, org_id: orgId }),
149+
),
138150
})
139151

140152
const isLoading = programQueries.some((query) => query.isLoading)
141153

142154
const programsWithCourses = programQueries
143-
.map((query, index) => {
155+
.map((query) => {
144156
if (!query.data?.results?.length) {
145157
return null
146158
}
147159
const program = query.data.results[0]
148160
const transformedProgram = transform.mitxonlineProgram(program)
149161
return {
150-
programId: programIds[index],
162+
programId: query.data.results[0].id,
151163
program: transformedProgram,
152164
hasCourses: program.courses && program.courses.length > 0,
153165
}
@@ -171,7 +183,7 @@ const OrgProgramCollectionDisplay: React.FC<{
171183
}> = ({ collection, contracts, enrollments, orgId }) => {
172184
const sanitizedDescription = DOMPurify.sanitize(collection.description ?? "")
173185
const { isLoading, programsWithCourses, hasAnyCourses } =
174-
useProgramCollectionCourses(collection.programIds, orgId)
186+
useProgramCollectionCourses(collection.programs, orgId)
175187
const firstCourseIds = programsWithCourses
176188
.map((p) => p?.program.courseIds[0])
177189
.filter((id): id is number => id !== undefined)
@@ -231,9 +243,9 @@ const OrgProgramCollectionDisplay: React.FC<{
231243
{header}
232244
<PlainList>
233245
{courses.isLoading &&
234-
programsWithCourses.map((item) => (
246+
programsWithCourses.map((item, index) => (
235247
<Skeleton
236-
key={item?.programId}
248+
key={`${collection.id}-${item?.programId}-${index}`}
237249
width="100%"
238250
height="65px"
239251
style={{ marginBottom: "16px" }}
@@ -400,7 +412,7 @@ const OrganizationContentInternal: React.FC<
400412
{programCollections.isLoading ? (
401413
skeleton
402414
) : (
403-
<PlainList>
415+
<ProgramCollectionsList>
404416
{programCollections.data?.results.map((collection) => {
405417
const transformedCollection =
406418
transform.mitxonlineProgramCollection(collection)
@@ -414,7 +426,7 @@ const OrganizationContentInternal: React.FC<
414426
/>
415427
)
416428
})}
417-
</PlainList>
429+
</ProgramCollectionsList>
418430
)}
419431
{programs.data?.results.length === 0 && (
420432
<HeaderRoot>

0 commit comments

Comments
 (0)