diff --git a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx
index d41a6aff88..4ab71c50db 100644
--- a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx
@@ -66,6 +66,31 @@ describe("OrganizationContent", () => {
})
})
+ it("displays courses in the correct order based on program.courseIds, regardless of API response order", async () => {
+ const { orgX, programA, coursesA } = setupProgramsAndCourses()
+
+ // Mock API to return courses in reverse order from program.courseIds
+ const reversedCoursesA = [...coursesA].reverse()
+ setMockResponse.get(
+ expect.stringContaining(
+ `/api/v2/courses/?id=${programA.courses.join("%2C")}`,
+ ),
+ { results: reversedCoursesA },
+ )
+
+ renderWithProviders()
+
+ const programElement = await screen.findByTestId("org-program-root")
+ const cards = await within(programElement).findAllByTestId(
+ "enrollment-card-desktop",
+ )
+
+ // Verify courses appear in program.courseIds order, not API response order
+ coursesA.forEach((course, i) => {
+ expect(cards[i]).toHaveTextContent(course.title)
+ })
+ })
+
test("Shows correct enrollment status", async () => {
const { orgX, programA, coursesA } = setupProgramsAndCourses()
const enrollments = [
@@ -132,19 +157,16 @@ describe("OrganizationContent", () => {
{ results: [programB] },
)
- // Mock the courses API calls for programs in the collection
- // Use dynamic matching since course IDs are randomly generated
- setMockResponse.get(
- expect.stringContaining(
- `/api/v2/courses/?id=${programA.courses.join("%2C")}`,
- ),
- { results: coursesA },
- )
+ // Mock the bulk course API call with first course from each program
+ const firstCourseA = coursesA.find((c) => c.id === programA.courses[0])
+ const firstCourseB = coursesB.find((c) => c.id === programB.courses[0])
+ const firstCourseIds = [programB.courses[0], programA.courses[0]] // B first, then A to match collection order
+
setMockResponse.get(
expect.stringContaining(
- `/api/v2/courses/?id=${programB.courses.join("%2C")}`,
+ `/api/v2/courses/?id=${firstCourseIds.join("%2C")}`,
),
- { results: coursesB },
+ { results: [firstCourseB, firstCourseA] }, // Response order should match request order
)
renderWithProviders()
@@ -168,8 +190,47 @@ describe("OrganizationContent", () => {
// Verify the order matches the programCollection.programs array [programB.id, programA.id]
const programCards = collection.getAllByTestId("enrollment-card-desktop")
- expect(programCards[0]).toHaveTextContent(coursesB[0].title)
- expect(programCards[1]).toHaveTextContent(coursesA[0].title)
+ expect(programCards[0]).toHaveTextContent(firstCourseB!.title)
+ expect(programCards[1]).toHaveTextContent(firstCourseA!.title)
+ })
+
+ test("Program collection displays the first course from each program", async () => {
+ const { orgX, programA, programCollection, coursesA } =
+ setupProgramsAndCourses()
+
+ programCollection.programs = [programA.id]
+ setMockResponse.get(urls.programCollections.programCollectionsList(), {
+ results: [programCollection],
+ })
+
+ setMockResponse.get(
+ expect.stringContaining(`/api/v2/programs/?id=${programA.id}`),
+ { results: [programA] },
+ )
+
+ // Mock bulk API call for the first course
+ const firstCourseId = programA.courses[0]
+ const firstCourse = coursesA.find((c) => c.id === firstCourseId)
+ setMockResponse.get(
+ expect.stringContaining(`/api/v2/courses/?id=${firstCourseId}`),
+ { results: [firstCourse] },
+ )
+
+ renderWithProviders()
+
+ const collection = await screen.findByTestId("org-program-collection-root")
+
+ // Wait for program cards to be rendered
+ const programCards = await waitFor(() => {
+ const programCards = within(collection).getAllByTestId(
+ "enrollment-card-desktop",
+ )
+ expect(programCards.length).toBeGreaterThan(0)
+ return programCards
+ })
+
+ // Should display the first course by program.courseIds order
+ expect(programCards[0]).toHaveTextContent(firstCourse!.title)
})
test("Does not render a program separately if it is part of a collection", async () => {
@@ -265,34 +326,32 @@ describe("OrganizationContent", () => {
const { orgX, programA, programB, programCollection, coursesB } =
setupProgramsAndCourses()
+ // Modify programA to have no courses to test "at least one program has courses"
+ const programANoCourses = { ...programA, courses: [] }
+
// Set up the collection to include both programs
- programCollection.programs = [programA.id, programB.id]
+ programCollection.programs = [programANoCourses.id, programB.id]
setMockResponse.get(urls.programCollections.programCollectionsList(), {
results: [programCollection],
})
// Mock individual program API calls for the collection
setMockResponse.get(
- expect.stringContaining(`/api/v2/programs/?id=${programA.id}`),
- { results: [programA] },
+ expect.stringContaining(`/api/v2/programs/?id=${programANoCourses.id}`),
+ { results: [programANoCourses] },
)
setMockResponse.get(
expect.stringContaining(`/api/v2/programs/?id=${programB.id}`),
{ results: [programB] },
)
- // Mock programA to have no courses, programB to have courses
- setMockResponse.get(
- expect.stringContaining(
- `/api/v2/courses/?id=${programA.courses.join("%2C")}`,
- ),
- { results: [] },
- )
+ // Mock bulk course API call - only programB has courses, so only its first course should be included
+ const firstCourseBId = programB.courses[0]
+ const firstCourseB = coursesB.find((c) => c.id === firstCourseBId)
+
setMockResponse.get(
- expect.stringContaining(
- `/api/v2/courses/?id=${programB.courses.join("%2C")}`,
- ),
- { results: coursesB },
+ expect.stringContaining(`/api/v2/courses/?id=${firstCourseBId}`),
+ { results: [firstCourseB] },
)
renderWithProviders()
@@ -307,11 +366,11 @@ describe("OrganizationContent", () => {
// Should see the collection header
expect(collection.getByText(programCollection.title)).toBeInTheDocument()
- // Should see programB's courses
+ // Should see programB's course
await waitFor(() => {
- expect(collection.getAllByText(coursesB[0].title).length).toBeGreaterThan(
- 0,
- )
+ expect(
+ collection.getAllByText(firstCourseB!.title).length,
+ ).toBeGreaterThan(0)
})
})
diff --git a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx
index 2612383a9f..ac9950533e 100644
--- a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx
@@ -131,13 +131,9 @@ const ProgramDescription = styled(Typography)({
// Custom hook to handle multiple program queries and check if any have courses
const useProgramCollectionCourses = (programIds: number[], orgId: number) => {
const programQueries = useQueries({
- queries: programIds.map((programId) => ({
- ...programsQueries.programsList({ id: programId, org_id: orgId }),
- queryKey: [
- ...programsQueries.programsList({ id: programId, org_id: orgId })
- .queryKey,
- ],
- })),
+ queries: programIds.map((programId) =>
+ programsQueries.programsList({ id: programId, org_id: orgId }),
+ ),
})
const isLoading = programQueries.some((query) => query.isLoading)
@@ -175,6 +171,25 @@ const OrgProgramCollectionDisplay: React.FC<{
const sanitizedDescription = DOMPurify.sanitize(collection.description ?? "")
const { isLoading, programsWithCourses, hasAnyCourses } =
useProgramCollectionCourses(collection.programIds, orgId)
+ const firstCourseIds = programsWithCourses
+ .map((p) => p?.program.courseIds[0])
+ .filter((id): id is number => id !== undefined)
+ const courses = useQuery({
+ ...coursesQueries.coursesList({
+ id: firstCourseIds,
+ org_id: orgId,
+ }),
+ enabled: firstCourseIds.length > 0,
+ })
+ const rawCourses =
+ courses.data?.results.sort((a, b) => {
+ return firstCourseIds.indexOf(a.id) - firstCourseIds.indexOf(b.id)
+ }) ?? []
+ const transformedCourses = transform.organizationCoursesWithContracts({
+ courses: rawCourses,
+ contracts: contracts ?? [],
+ enrollments: enrollments ?? [],
+ })
const header = (
@@ -214,17 +229,26 @@ const OrgProgramCollectionDisplay: React.FC<{
{header}
- {programsWithCourses.map((item) =>
- item ? (
- (
+
- ) : null,
- )}
+ ))}
+ {transformedCourses.map((course) => (
+
+ ))}
)
@@ -260,8 +284,12 @@ const OrgProgramDisplay: React.FC<{
)
if (programLoading || courses.isLoading) return skeleton
+ const rawCourses =
+ courses.data?.results.sort((a, b) => {
+ return program.courseIds.indexOf(a.id) - program.courseIds.indexOf(b.id)
+ }) ?? []
const transformedCourses = transform.organizationCoursesWithContracts({
- courses: courses.data?.results ?? [],
+ courses: rawCourses,
contracts: contracts ?? [],
enrollments: courseRunEnrollments ?? [],
})
@@ -307,59 +335,6 @@ const OrgProgramDisplay: React.FC<{
)
}
-const ProgramCollectionItem: React.FC<{
- program: DashboardProgram
- contracts?: ContractPage[]
- enrollments?: CourseRunEnrollment[]
- orgId: number
-}> = ({ program, contracts, enrollments, orgId }) => {
- return (
-
- )
-}
-
-const ProgramCard: React.FC<{
- program: DashboardProgram
- contracts?: ContractPage[]
- enrollments?: CourseRunEnrollment[]
- orgId: number
-}> = ({ program, contracts, enrollments, orgId }) => {
- const courses = useQuery(
- coursesQueries.coursesList({
- id: program.courseIds,
- org_id: orgId,
- }),
- )
- const skeleton = (
-
- )
- if (courses.isLoading) return skeleton
- const transformedCourses = transform.organizationCoursesWithContracts({
- courses: courses.data?.results ?? [],
- contracts: contracts ?? [],
- enrollments: enrollments ?? [],
- })
- if (courses.isLoading || !transformedCourses.length) return skeleton
- // For now we assume the first course is the main one for the program.
- const course = transformedCourses[0]
- return (
-
- )
-}
-
const OrganizationRoot = styled.div({
display: "flex",
flexDirection: "column",