diff --git a/frontends/api/src/mitxonline/test-utils/urls.ts b/frontends/api/src/mitxonline/test-utils/urls.ts
index 671eef5f0f..c160c08c3a 100644
--- a/frontends/api/src/mitxonline/test-utils/urls.ts
+++ b/frontends/api/src/mitxonline/test-utils/urls.ts
@@ -36,7 +36,7 @@ const b2b = {
const programs = {
programsList: (opts?: ProgramsApiProgramsListV2Request) =>
- `${API_BASE_URL}/api/v2/programs/${queryify(opts)}`,
+ `${API_BASE_URL}/api/v2/programs/${queryify(opts, { explode: false })}`,
programDetail: (id: number) => `${API_BASE_URL}/api/v2/programs/${id}/`,
}
diff --git a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx b/frontends/main/src/app-pages/DashboardPage/ContractContent.test.tsx
similarity index 78%
rename from frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx
rename to frontends/main/src/app-pages/DashboardPage/ContractContent.test.tsx
index 13c2055bca..46bdbd8690 100644
--- a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/ContractContent.test.tsx
@@ -6,7 +6,7 @@ import {
waitFor,
user,
} from "@/test-utils"
-import OrganizationContent from "./OrganizationContent"
+import ContractContent from "./ContractContent"
import { setMockResponse } from "api/test-utils"
import { urls, factories } from "api/mitxonline-test-utils"
import {
@@ -27,7 +27,7 @@ import { faker } from "@faker-js/faker/locale/en"
const makeCourseEnrollment = factories.enrollment.courseEnrollment
const makeGrade = factories.enrollment.grade
-describe("OrganizationContent", () => {
+describe("ContractContent", () => {
beforeEach(() => {
setMockResponse.get(urls.enrollment.enrollmentsListV2(), [])
setMockResponse.get(urls.programEnrollments.enrollmentsList(), [])
@@ -39,7 +39,12 @@ describe("OrganizationContent", () => {
const { orgX, programA, programB, coursesA, coursesB } =
setupProgramsAndCourses()
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
await screen.findByRole("heading", {
name: orgX.name,
@@ -78,7 +83,12 @@ describe("OrganizationContent", () => {
{ results: reversedCoursesA },
)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const programElements = await screen.findAllByTestId("org-program-root")
// Find the program with programA's title
@@ -96,7 +106,8 @@ describe("OrganizationContent", () => {
})
it("displays programs in the correct order based on contract.programs, regardless of API response order", async () => {
- const { orgX, programA, programB } = setupProgramsAndCourses()
+ const { orgX, programA, programB, coursesA, coursesB } =
+ setupProgramsAndCourses()
// Update the contract to specify program order (B first, then A)
const contract = factories.contracts.contract({
@@ -113,8 +124,34 @@ describe("OrganizationContent", () => {
setMockResponse.get(urls.programs.programsList({ org_id: orgX.id }), {
results: [programA, programB],
})
+ // Add the contract-filtered programs query
+ setMockResponse.get(
+ urls.programs.programsList({
+ org_id: orgX.id,
+ contract_id: contract.id,
+ }),
+ {
+ results: [programA, programB],
+ },
+ )
+ // Add the contract-filtered courses query
+ setMockResponse.get(
+ urls.courses.coursesList({
+ org_id: orgX.id,
+ contract_id: contract.id,
+ page_size: 200,
+ }),
+ {
+ results: [...coursesA, ...coursesB],
+ },
+ )
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
// Debug: see what's actually rendered
await screen.findByRole("heading", { name: orgX.name })
@@ -146,7 +183,12 @@ describe("OrganizationContent", () => {
// Override the default empty enrollments for this test
setMockResponse.get(urls.enrollment.enrollmentsListV2(), enrollments)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const [programElA] = await screen.findAllByTestId("org-program-root")
const cards = await within(programElA).findAllByTestId(
@@ -198,10 +240,13 @@ describe("OrganizationContent", () => {
results: [programCollection],
})
- // Mock the new bulk programs API call with array of IDs
+ // Mock the new bulk programs API call with array of IDs and contract_id
const programIds = [programB.id, programA.id] // B first, then A to match collection order
setMockResponse.get(
- expect.stringContaining(`/api/v2/programs/?id=${programIds.join("%2C")}`),
+ urls.programs.programsList({
+ id: programIds,
+ contract_id: orgX.contracts[0].id,
+ }),
{ results: [programB, programA] }, // Return in same order as requested
)
@@ -210,16 +255,21 @@ describe("OrganizationContent", () => {
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
- // Mock the program collection courses query
+ // Mock the program collection courses query with contract_id
setMockResponse.get(
urls.courses.coursesList({
id: firstCourseIds,
- org_id: orgX.id,
+ contract_id: orgX.contracts[0].id,
}),
{ results: [firstCourseB, firstCourseA] },
)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const collectionHeader = await screen.findByRole("heading", {
name: programCollection.title,
@@ -260,24 +310,32 @@ describe("OrganizationContent", () => {
results: [programCollection],
})
- // Mock the new bulk programs API call with array of IDs
+ // Mock the new bulk programs API call with array of IDs and contract_id
setMockResponse.get(
- expect.stringContaining(`/api/v2/programs/?id=${programA.id}`),
+ urls.programs.programsList({
+ id: [programA.id],
+ contract_id: orgX.contracts[0].id,
+ }),
{ results: [programA] },
)
- // Mock bulk API call for the first course
+ // Mock bulk API call for the first course with contract_id
const firstCourseId = programA.courses[0]
const firstCourse = coursesA.find((c) => c.id === firstCourseId)
setMockResponse.get(
urls.courses.coursesList({
id: [firstCourseId],
- org_id: orgX.id,
+ contract_id: orgX.contracts[0].id,
}),
{ results: [firstCourse] },
)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const collection = await screen.findByTestId("org-program-collection-root")
const collectionWrapper = within(collection)
@@ -314,26 +372,34 @@ describe("OrganizationContent", () => {
results: [programCollection],
})
- // Mock the bulk programs API call for the collection
+ // Mock the bulk programs API call for the collection with contract_id
const programIds = [programB.id, programA.id]
setMockResponse.get(
- expect.stringContaining(`/api/v2/programs/?id=${programIds.join("%2C")}`),
+ urls.programs.programsList({
+ id: programIds,
+ contract_id: orgX.contracts[0].id,
+ }),
{ results: [programB, programA] },
)
- // Mock course API calls for program collection (first courses)
+ // Mock course API calls for program collection (first courses) with contract_id
const firstCourseIds = [programB.courses[0], programA.courses[0]]
const firstCourseA = coursesA.find((c) => c.id === programA.courses[0])
const firstCourseB = coursesB.find((c) => c.id === programB.courses[0])
setMockResponse.get(
urls.courses.coursesList({
id: firstCourseIds,
- org_id: orgX.id,
+ contract_id: orgX.contracts[0].id,
}),
{ results: [firstCourseB, firstCourseA] },
)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const collectionItems = await screen.findAllByTestId(
"org-program-collection-root",
@@ -349,11 +415,35 @@ describe("OrganizationContent", () => {
setMockResponse.get(urls.programs.programsList({ org_id: orgX.id }), {
results: [],
})
+ setMockResponse.get(
+ urls.programs.programsList({
+ org_id: orgX.id,
+ contract_id: orgX.contracts[0].id,
+ }),
+ {
+ results: [],
+ },
+ )
+ setMockResponse.get(
+ urls.courses.coursesList({
+ org_id: orgX.id,
+ contract_id: orgX.contracts[0].id,
+ page_size: 200,
+ }),
+ {
+ results: [],
+ },
+ )
setMockResponse.get(urls.programCollections.programCollectionsList(), {
results: [],
})
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
// Wait for the header to appear
await screen.findByRole("heading", {
@@ -385,7 +475,12 @@ describe("OrganizationContent", () => {
results: [],
})
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
// Wait for the header to appear
await screen.findByRole("heading", {
@@ -428,10 +523,13 @@ describe("OrganizationContent", () => {
results: [programCollection],
})
- // Mock the bulk programs API call for the collection
+ // Mock the bulk programs API call for the collection with contract_id
const programIds = [programANoCourses.id, programB.id]
setMockResponse.get(
- expect.stringContaining(`/api/v2/programs/?id=${programIds.join("%2C")}`),
+ urls.programs.programsList({
+ id: programIds,
+ contract_id: orgX.contracts[0].id,
+ }),
{ results: [programANoCourses, programB] },
)
@@ -441,13 +539,18 @@ describe("OrganizationContent", () => {
setMockResponse.get(
urls.courses.coursesList({
- id: [firstCourseBId], // Only programB's first course since programA has no courses
- org_id: orgX.id,
+ id: [firstCourseBId],
+ contract_id: orgX.contracts[0].id,
}),
{ results: [firstCourseB] },
)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
// The collection should be rendered since programB has courses
const collectionItems = await screen.findAllByTestId(
@@ -465,7 +568,7 @@ describe("OrganizationContent", () => {
})
test("Shows the program certificate link button if the program has a certificate", async () => {
- const { orgX, programA } = setupProgramsAndCourses()
+ const { orgX, programA, coursesA } = setupProgramsAndCourses()
// Mock the program to have a certificate
const programWithCertificate = {
@@ -486,6 +589,27 @@ describe("OrganizationContent", () => {
setMockResponse.get(urls.programs.programsList({ org_id: orgX.id }), {
results: [programWithCertificate],
})
+ // Add the contract-filtered programs query
+ setMockResponse.get(
+ urls.programs.programsList({
+ org_id: orgX.id,
+ contract_id: orgX.contracts[0].id,
+ }),
+ {
+ results: [programWithCertificate],
+ },
+ )
+ // Add the contract-filtered courses query
+ setMockResponse.get(
+ urls.courses.coursesList({
+ org_id: orgX.id,
+ contract_id: orgX.contracts[0].id,
+ page_size: 200,
+ }),
+ {
+ results: coursesA,
+ },
+ )
setMockResponse.get(urls.programEnrollments.enrollmentsList(), [
programEnrollment,
])
@@ -493,7 +617,12 @@ describe("OrganizationContent", () => {
programEnrollment,
])
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const programRoot = await screen.findByTestId("org-program-root")
const certificateButton = within(programRoot).getByRole("link", {
@@ -507,11 +636,17 @@ describe("OrganizationContent", () => {
test("displays only courses with contract-scoped runs", async () => {
const { orgX, user, mitxOnlineUser } = setupOrgAndUser()
- const contracts = createTestContracts(orgX.id, 1)
- const courses = createCoursesWithContractRuns(contracts)
+ const baseCourses = factories.courses.courses({ count: 3 }).results
const program = factories.programs.program({
- courses: courses.map((c) => c.id),
+ courses: baseCourses.map((c) => c.id),
})
+ const contracts = createTestContracts(orgX.id, 1, [program.id])
+ orgX.contracts = contracts
+ // Update the user's b2b_organizations to include contracts
+ mitxOnlineUser.b2b_organizations[0].contracts = contracts
+ const courses = createCoursesWithContractRuns(contracts)
+ // Update program to use the actual course IDs from createCoursesWithContractRuns
+ program.courses = courses.map((c) => c.id)
setupOrgDashboardMocks(
orgX,
@@ -522,7 +657,12 @@ describe("OrganizationContent", () => {
contracts,
)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
// Wait for programs to load
const programElements = await screen.findAllByTestId("org-program-root")
@@ -553,13 +693,20 @@ describe("OrganizationContent", () => {
jest.useFakeTimers()
jest.setSystemTime(new Date("2024-01-01T00:00:00Z"))
const { orgX, user, mitxOnlineUser } = setupOrgAndUser()
- const contracts = createTestContracts(orgX.id, 1)
// Create courses with specific, predictable dates for the contract runs
// Use a date that's guaranteed to be in the future relative to mocked time
const specificStartDate = "2024-12-01T00:00:00Z"
const specificEndDate = "2025-01-15T00:00:00Z"
+ const baseCourses = factories.courses.courses({ count: 3 }).results
+ const program = factories.programs.program({
+ courses: baseCourses.map((c) => c.id),
+ })
+ const contracts = createTestContracts(orgX.id, 1, [program.id])
+ orgX.contracts = contracts
+ mitxOnlineUser.b2b_organizations[0].contracts = contracts
+
const courses = createCoursesWithContractRuns(contracts).map((course) => ({
...course,
courseruns: course.courseruns.map((run) => {
@@ -573,10 +720,8 @@ describe("OrganizationContent", () => {
return run
}),
}))
-
- const program = factories.programs.program({
- courses: courses.map((c) => c.id),
- })
+ // Update program to use the actual course IDs
+ program.courses = courses.map((c) => c.id)
setupOrgDashboardMocks(
orgX,
@@ -587,7 +732,12 @@ describe("OrganizationContent", () => {
contracts,
)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const cards = await within(
await screen.findByTestId("org-program-root"),
@@ -609,9 +759,16 @@ describe("OrganizationContent", () => {
jest.setSystemTime(new Date("2024-01-01T00:00:00Z"))
const { orgX, user, mitxOnlineUser } = setupOrgAndUser()
- const contracts = createTestContracts(orgX.id, 1)
// Create courses where non-contract runs have different, easily identifiable data
+ const baseCourses = factories.courses.courses({ count: 3 }).results
+ const program = factories.programs.program({
+ courses: baseCourses.map((c) => c.id),
+ })
+ const contracts = createTestContracts(orgX.id, 1, [program.id])
+ orgX.contracts = contracts
+ mitxOnlineUser.b2b_organizations[0].contracts = contracts
+
const courses = createCoursesWithContractRuns(contracts).map((course) => ({
...course,
courseruns: course.courseruns.map((run) => {
@@ -631,10 +788,8 @@ describe("OrganizationContent", () => {
}
}),
}))
-
- const program = factories.programs.program({
- courses: courses.map((c) => c.id),
- })
+ // Update program to use the actual course IDs
+ program.courses = courses.map((c) => c.id)
setupOrgDashboardMocks(
orgX,
@@ -645,7 +800,12 @@ describe("OrganizationContent", () => {
contracts,
)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const cards = await within(
await screen.findByTestId("org-program-root"),
@@ -671,9 +831,16 @@ describe("OrganizationContent", () => {
test("displays correct pricing from contract-scoped runs", async () => {
const { orgX, user, mitxOnlineUser } = setupOrgAndUser()
- const contracts = createTestContracts(orgX.id, 1)
// Create courses with different pricing for contract vs non-contract runs
+ const baseCourses = factories.courses.courses({ count: 3 }).results
+ const program = factories.programs.program({
+ courses: baseCourses.map((c) => c.id),
+ })
+ const contracts = createTestContracts(orgX.id, 1, [program.id])
+ orgX.contracts = contracts
+ mitxOnlineUser.b2b_organizations[0].contracts = contracts
+
const courses = createCoursesWithContractRuns(contracts).map((course) => ({
...course,
courseruns: course.courseruns.map((run) => {
@@ -707,10 +874,8 @@ describe("OrganizationContent", () => {
}
}),
}))
-
- const program = factories.programs.program({
- courses: courses.map((c) => c.id),
- })
+ // Update program to use the actual course IDs
+ program.courses = courses.map((c) => c.id)
setupOrgDashboardMocks(
orgX,
@@ -721,7 +886,12 @@ describe("OrganizationContent", () => {
contracts,
)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const cards = await within(
await screen.findByTestId("org-program-root"),
@@ -739,9 +909,17 @@ describe("OrganizationContent", () => {
test("handles mixed scenarios with enrolled and non-enrolled contract runs", async () => {
const { orgX, user, mitxOnlineUser } = setupOrgAndUser()
- const contracts = createTestContracts(orgX.id, 1)
+ const baseCourses = factories.courses.courses({ count: 3 }).results
+ const program = factories.programs.program({
+ courses: baseCourses.map((c) => c.id),
+ })
+ const contracts = createTestContracts(orgX.id, 1, [program.id])
+ orgX.contracts = contracts
+ mitxOnlineUser.b2b_organizations[0].contracts = contracts
const contractIds = contracts.map((c) => c.id)
const courses = createCoursesWithContractRuns(contracts)
+ // Update program to use the actual course IDs
+ program.courses = courses.map((c) => c.id)
// Create enrollment for first course only
const enrollments = [
@@ -772,10 +950,6 @@ describe("OrganizationContent", () => {
// Override enrollments for this test
setMockResponse.get(urls.enrollment.enrollmentsListV2(), enrollments)
- const program = factories.programs.program({
- courses: courses.map((c) => c.id),
- })
-
setupOrgDashboardMocks(
orgX,
user,
@@ -785,7 +959,12 @@ describe("OrganizationContent", () => {
contracts,
)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const cards = await within(
await screen.findByTestId("org-program-root"),
@@ -808,7 +987,9 @@ describe("OrganizationContent", () => {
setMockResponse.get(urls.userMe.get(), mitxOnlineUser)
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
await screen.findByRole("heading", { name: "Organization not found" })
})
@@ -820,7 +1001,12 @@ describe("OrganizationContent", () => {
orgX.contracts[0].welcome_message_extra =
"
This is additional information with HTML formatting.
"
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
await screen.findByText("Welcome to our program!")
@@ -836,7 +1022,12 @@ describe("OrganizationContent", () => {
orgX.contracts[0].welcome_message_extra =
"Extra content with emphasis
"
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const showMoreLink = await screen.findByText("Show more")
@@ -865,7 +1056,12 @@ describe("OrganizationContent", () => {
contractWithoutWelcome.welcome_message_extra = "Extra content
"
orgX.contracts[0] = contractWithoutWelcome
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
expect(screen.queryByText("Show more")).toBeNull()
expect(screen.queryByText("Extra content")).toBeNull()
@@ -880,20 +1076,37 @@ describe("OrganizationContent", () => {
.welcome_message_extra
orgX.contracts[0] = contractWithoutExtra
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
expect(screen.queryByText("Welcome message")).toBeNull()
expect(screen.queryByText("Show more")).toBeNull()
})
- test("does not display welcome message when organization has no contracts", async () => {
+ test("shows the not found screen if the contract is not found by contractSlug", async () => {
const { orgX } = setupProgramsAndCourses()
orgX.contracts = []
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
- expect(screen.queryByText("Show more")).toBeNull()
+ await screen.findByRole("heading", { name: "Contract not found" })
+ })
+
+ test("shows the not found screen when contract slug doesn't match any contracts", async () => {
+ const { orgX } = setupProgramsAndCourses()
+
+ renderWithProviders(
+ ,
+ )
+
+ await screen.findByRole("heading", { name: "Contract not found" })
})
test("sanitizes HTML content in welcome_message_extra", async () => {
@@ -903,7 +1116,12 @@ describe("OrganizationContent", () => {
orgX.contracts[0].welcome_message_extra =
'Safe content
'
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const showMoreLink = await screen.findByText("Show more")
await user.click(showMoreLink)
@@ -932,7 +1150,12 @@ describe("OrganizationContent", () => {
secondContract,
]
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
await screen.findByText("First welcome message")
@@ -947,10 +1170,16 @@ describe("OrganizationContent", () => {
test("displays correct run URL when user is enrolled in one of multiple runs", async () => {
const { orgX, user, mitxOnlineUser } = setupOrgAndUser()
- const contracts = createTestContracts(orgX.id, 1)
// Create a course with 3 different runs with distinct URLs
const course = factories.courses.course()
+ const program = factories.programs.program({
+ courses: [course.id],
+ })
+ const contracts = createTestContracts(orgX.id, 1, [program.id])
+ orgX.contracts = contracts
+ mitxOnlineUser.b2b_organizations[0].contracts = contracts
+
const runs = [
factories.courses.courseRun({
b2b_contract: contracts[0].id,
@@ -986,10 +1215,6 @@ describe("OrganizationContent", () => {
grades: [],
})
- const program = factories.programs.program({
- courses: [course.id],
- })
-
setupOrgDashboardMocks(
orgX,
user,
@@ -1001,7 +1226,12 @@ describe("OrganizationContent", () => {
setMockResponse.get(urls.enrollment.enrollmentsListV2(), [enrollment])
- renderWithProviders()
+ renderWithProviders(
+ ,
+ )
const programElement = await screen.findByTestId("org-program-root")
const card = await within(programElement).findByTestId(
diff --git a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx b/frontends/main/src/app-pages/DashboardPage/ContractContent.tsx
similarity index 78%
rename from frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx
rename to frontends/main/src/app-pages/DashboardPage/ContractContent.tsx
index 1534f1e623..27ea41584f 100644
--- a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/ContractContent.tsx
@@ -9,7 +9,6 @@ import {
programCollectionQueries,
} from "api/mitxonline-hooks/programs"
import { coursesQueries } from "api/mitxonline-hooks/courses"
-import { contractQueries } from "api/mitxonline-hooks/contracts"
import * as transform from "./CoursewareDisplay/transform"
import { enrollmentQueries } from "api/mitxonline-hooks/enrollment"
import { DashboardCard } from "./CoursewareDisplay/DashboardCard"
@@ -28,15 +27,16 @@ import {
} from "./CoursewareDisplay/types"
import graduateLogo from "@/public/images/dashboard/graduate.png"
import {
- ContractPage,
CourseRunEnrollmentRequestV2,
- OrganizationPage,
V2UserProgramEnrollmentDetail,
+ ContractPage,
+ OrganizationPage,
} from "@mitodl/mitxonline-api-axios/v2"
import { mitxUserQueries } from "api/mitxonline-hooks/user"
import { ButtonLink } from "@mitodl/smoot-design"
import { RiAwardFill } from "@remixicon/react"
import { ErrorContent } from "../ErrorPage/ErrorPageTemplate"
+import { matchOrganizationBySlug } from "@/common/utils"
const HeaderRoot = styled.div({
display: "flex",
@@ -59,7 +59,10 @@ const ImageContainer = styled.div(({ theme }) => ({
},
}))
-const OrganizationHeader: React.FC<{ org?: OrganizationPage }> = ({ org }) => {
+const ContractHeader: React.FC<{
+ org?: OrganizationPage
+ contract?: ContractPage
+}> = ({ org, contract }) => {
return (
@@ -79,8 +82,7 @@ const OrganizationHeader: React.FC<{ org?: OrganizationPage }> = ({ org }) => {
{org?.name}
- {/* For now we will use the first contract name until we refactor this to be based on contracts / offerings */}
- {org?.contracts[0]?.name}
+ {contract?.name}
)
@@ -93,13 +95,14 @@ const WelcomeMessageExtra = styled(Typography)({
},
})
-const WelcomeMessage: React.FC<{ org?: OrganizationPage }> = ({ org }) => {
+const WelcomeMessage: React.FC<{ contract?: ContractPage }> = ({
+ contract,
+}) => {
const empty =
const [showingMore, setShowingMore] = React.useState(false)
- if (!org?.contracts?.length) {
+ if (!contract) {
return empty
}
- const contract = org.contracts[0]
const welcomeMessage = contract.welcome_message
const welcomeMessageExtra = DOMPurify.sanitize(contract.welcome_message_extra)
if (!welcomeMessage || !welcomeMessageExtra) {
@@ -189,13 +192,16 @@ const ProgramCollectionsList = styled(PlainList)({
// Custom hook to handle multiple program queries and check if any have courses
const useProgramCollectionCourses = (
programs: DashboardProgramCollectionProgram[],
- orgId: number,
+ contractId: number,
) => {
const programIds = programs
.map((program) => program.id)
.filter((id) => id !== undefined)
const programsQuery = useQuery({
- ...programsQueries.programsList({ id: programIds, org_id: orgId }),
+ ...programsQueries.programsList({
+ id: programIds,
+ contract_id: contractId,
+ }),
enabled: programIds.length > 0,
})
const isLoading = programsQuery.isLoading
@@ -225,20 +231,19 @@ const useProgramCollectionCourses = (
const OrgProgramCollectionDisplay: React.FC<{
collection: DashboardProgramCollection
- contracts?: ContractPage[]
+ contract: ContractPage
enrollments?: CourseRunEnrollmentRequestV2[]
- orgId: number
-}> = ({ collection, contracts, enrollments, orgId }) => {
+}> = ({ collection, contract, enrollments }) => {
const sanitizedDescription = DOMPurify.sanitize(collection.description ?? "")
const { isLoading, programsWithCourses, hasAnyCourses } =
- useProgramCollectionCourses(collection.programs, orgId)
+ useProgramCollectionCourses(collection.programs, contract.id)
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,
+ contract_id: contract.id,
}),
enabled: firstCourseIds !== undefined && firstCourseIds.length > 0,
})
@@ -251,7 +256,7 @@ const OrgProgramCollectionDisplay: React.FC<{
}) ?? []
const transformedCourses = transform.organizationCoursesWithContracts({
courses: rawCourses,
- contracts: contracts ?? [],
+ contract: contract,
enrollments: enrollments ?? [],
})
@@ -320,14 +325,14 @@ const OrgProgramCollectionDisplay: React.FC<{
const OrgProgramDisplay: React.FC<{
program: DashboardProgram
- contracts?: ContractPage[]
+ contract?: ContractPage
courseRunEnrollments?: CourseRunEnrollmentRequestV2[]
programEnrollments?: V2UserProgramEnrollmentDetail[]
programLoading: boolean
orgId: number
}> = ({
program,
- contracts,
+ contract,
courseRunEnrollments,
programEnrollments,
programLoading,
@@ -355,7 +360,7 @@ const OrgProgramDisplay: React.FC<{
}) ?? []
const transformedCourses = transform.organizationCoursesWithContracts({
courses: rawCourses,
- contracts: contracts ?? [],
+ contract: contract,
enrollments: courseRunEnrollments ?? [],
})
@@ -401,35 +406,52 @@ const OrgProgramDisplay: React.FC<{
)
}
-const OrganizationRoot = styled.div({
+const ContractRoot = styled.div({
display: "flex",
flexDirection: "column",
gap: "40px",
})
-type OrganizationContentInternalProps = {
+type ContractContentInternalProps = {
org: OrganizationPage
+ contract: ContractPage
}
-const OrganizationContentInternal: React.FC<
- OrganizationContentInternalProps
-> = ({ org }) => {
+const ContractContentInternal: React.FC = ({
+ org,
+ contract,
+}) => {
const orgId = org.id
- const contracts = useQuery(contractQueries.contractsList())
- const orgContracts = contracts.data?.filter(
- (contract) => contract.organization === orgId,
- )
- // For now, the relevant contract is always the first one
- const orgContract = orgContracts ? orgContracts[0] : null
const courseRunEnrollments = useQuery(
enrollmentQueries.courseRunEnrollmentsList(),
)
const programEnrollments = useQuery(
enrollmentQueries.programEnrollmentsList(),
)
- const programs = useQuery(programsQueries.programsList({ org_id: orgId }))
+ const programs = useQuery(
+ programsQueries.programsList({ org_id: orgId, contract_id: contract.id }),
+ )
const programCollections = useQuery(
programCollectionQueries.programCollectionsList({}),
)
+ const courses = useQuery(
+ coursesQueries.coursesList({
+ org_id: orgId,
+ contract_id: contract.id,
+ page_size: 200,
+ }),
+ )
+
+ // Helper to check if a program has any courses with contract-scoped runs
+ const programHasContractRuns = (programId: number): boolean => {
+ const programData = programs.data?.results.find((p) => p.id === programId)
+ if (!programData?.courses || !courses.data?.results) return false
+
+ // Since courses query is already filtered by contract_id,
+ // we just need to check if any of the program's courses exist in the results
+ return programData.courses.some((courseId) =>
+ courses.data.results.some((c) => c.id === courseId),
+ )
+ }
// Get IDs of all programs that are in collections
const programsInCollections = new Set(
@@ -440,17 +462,20 @@ const OrganizationContentInternal: React.FC<
const transformedPrograms = programs.data?.results
.filter((program) => !programsInCollections.has(program.id))
+ .filter(() => {
+ // If contract has no programs defined, show nothing
+ return contract?.programs && contract.programs.length > 0
+ })
.filter((program) => {
- if (!orgContract?.programs || orgContract.programs.length === 0) {
- return true
- }
- return orgContract.programs.includes(program.id)
+ // Only include programs that are in the contract
+ return contract?.programs.includes(program.id)
})
+ .filter((program) => programHasContractRuns(program.id))
.map((program) => transform.mitxonlineProgram(program))
.sort((a, b) => {
- if (!orgContract?.programs) return 0
- const indexA = orgContract.programs.indexOf(a.id)
- const indexB = orgContract.programs.indexOf(b.id)
+ if (!contract?.programs) return 0
+ const indexA = contract.programs.indexOf(a.id)
+ const indexB = contract.programs.indexOf(b.id)
return indexA - indexB
})
@@ -467,8 +492,8 @@ const OrganizationContentInternal: React.FC<
return (
<>
-
-
+
+
{skeleton}
>
@@ -478,16 +503,16 @@ const OrganizationContentInternal: React.FC<
return (
<>
-
-
+
+
-
+
{!transformedPrograms
? skeleton
: transformedPrograms.map((program) => (
{(programCollections.data?.results ?? [])
+ .filter(() => {
+ // If contract has no programs defined, show nothing
+ return contract?.programs && contract.programs.length > 0
+ })
.filter((collection) => {
// Only show collections where at least one program is in the contract
const collectionProgramIds = collection.programs.map((p) => p.id)
- if (!orgContract?.programs || orgContract.programs.length === 0) {
- return collectionProgramIds.length > 0
- }
return collectionProgramIds.some(
- (id) => id !== undefined && orgContract.programs.includes(id),
+ (id) => id !== undefined && contract?.programs.includes(id),
+ )
+ })
+ .filter((collection) => {
+ // Filter out collections where none of the programs have valid course runs
+ const collectionProgramIds = collection.programs
+ .map((p) => p.id)
+ .filter((id): id is number => id !== undefined)
+ return collectionProgramIds.some((id) =>
+ programHasContractRuns(id),
)
})
.map((collection) => {
@@ -514,9 +549,8 @@ const OrganizationContentInternal: React.FC<
)
})}
@@ -528,33 +562,41 @@ const OrganizationContentInternal: React.FC<
)}
-
+
>
)
}
-const matchOrganizationBySlug =
- (orgSlug: string) => (organization: OrganizationPage) => {
- return organization.slug.replace("org-", "") === orgSlug
- }
-
-type OrganizationContentProps = {
+type ContractContentProps = {
orgSlug: string
+ contractSlug: string
}
-const OrganizationContent: React.FC = ({
+const ContractContent: React.FC = ({
orgSlug,
+ contractSlug,
}) => {
const { isLoading: isLoadingMitxOnlineUser, data: mitxOnlineUser } = useQuery(
mitxUserQueries.me(),
)
+ const b2bOrganization = mitxOnlineUser?.b2b_organizations.find(
+ matchOrganizationBySlug(orgSlug),
+ )
+ const b2bContract = b2bOrganization?.contracts.find(
+ (contract) => contract.slug === contractSlug,
+ )
+
useEffect(() => {
- if (
- mitxOnlineUser?.b2b_organizations.find(matchOrganizationBySlug(orgSlug))
- ) {
+ if (b2bOrganization) {
localStorage.setItem("last-dashboard-org", orgSlug)
}
- }, [mitxOnlineUser, orgSlug])
+ }, [b2bOrganization, orgSlug])
+
+ useEffect(() => {
+ if (b2bContract) {
+ localStorage.setItem("last-dashboard-contract", contractSlug)
+ }
+ }, [b2bContract, contractSlug])
if (isLoadingMitxOnlineUser) {
return (
@@ -562,17 +604,19 @@ const OrganizationContent: React.FC = ({
)
}
- const b2bOrganization = mitxOnlineUser?.b2b_organizations.find(
- matchOrganizationBySlug(orgSlug),
- )
-
if (!b2bOrganization) {
return
}
- return
+ if (!b2bContract) {
+ return
+ }
+
+ return (
+
+ )
}
-export default OrganizationContent
+export default ContractContent
-export type { OrganizationContentProps }
+export type { ContractContentProps }
diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.test.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.test.tsx
index 5dcac8df1f..080f018351 100644
--- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.test.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.test.tsx
@@ -138,24 +138,25 @@ describe("OrganizationCards", () => {
})
it("renders Continue buttons with correct organization URLs", async () => {
+ const contract1 = mitxOnlineFactories.contracts.contract({
+ id: 1,
+ name: "Contract 1",
+ organization: 1,
+ slug: "contract-1",
+ })
+ const contract2 = mitxOnlineFactories.contracts.contract({
+ id: 2,
+ name: "Contract 2",
+ organization: 1,
+ slug: "contract-2",
+ })
const organization: OrganizationPage = {
id: 1,
name: "Test Organization",
description: "Test description",
logo: "https://example.com/logo.png",
slug: "org-test-org",
- contracts: [
- mitxOnlineFactories.contracts.contract({
- id: 1,
- name: "Contract 1",
- organization: 1,
- }),
- mitxOnlineFactories.contracts.contract({
- id: 2,
- name: "Contract 2",
- organization: 1,
- }),
- ],
+ contracts: [contract1, contract2],
}
setup({ organizations: [organization] })
@@ -165,12 +166,19 @@ describe("OrganizationCards", () => {
})
expect(continueButtons).toHaveLength(4) // 2 contracts × 2 screen sizes (mobile + desktop)
- continueButtons.forEach((button) => {
- expect(button).toHaveAttribute(
- "href",
- "/dashboard/organization/test-org",
- )
- })
+ // Check that we have the correct number of buttons for each contract
+ const contract1Buttons = continueButtons.filter(
+ (button) =>
+ button.getAttribute("href") ===
+ "/dashboard/organization/test-org/contract/contract-1",
+ )
+ const contract2Buttons = continueButtons.filter(
+ (button) =>
+ button.getAttribute("href") ===
+ "/dashboard/organization/test-org/contract/contract-2",
+ )
+ expect(contract1Buttons).toHaveLength(2) // mobile + desktop
+ expect(contract2Buttons).toHaveLength(2) // mobile + desktop
})
it("renders cards for both mobile and desktop screen sizes", async () => {
@@ -238,11 +246,13 @@ describe("OrganizationCards", () => {
id: 1,
name: "Org1 Contract 1",
organization: 1,
+ slug: "org1-contract-1",
}),
mitxOnlineFactories.contracts.contract({
id: 2,
name: "Org1 Contract 2",
organization: 1,
+ slug: "org1-contract-2",
}),
],
}
@@ -257,6 +267,7 @@ describe("OrganizationCards", () => {
id: 3,
name: "Org2 Contract 1",
organization: 2,
+ slug: "org2-contract-1",
}),
],
}
@@ -286,22 +297,28 @@ describe("OrganizationCards", () => {
expect(screen.getAllByText("Org1 Contract 2")).toHaveLength(2) // mobile + desktop
expect(screen.getAllByText("Org2 Contract 1")).toHaveLength(2) // mobile + desktop
- // Check Continue button URLs point to correct organizations
- const org1Buttons = screen
- .getAllByRole("link", { name: "Continue" })
- .filter(
- (button) =>
- button.getAttribute("href") === "/dashboard/organization/one",
- )
- const org2Buttons = screen
- .getAllByRole("link", { name: "Continue" })
- .filter(
- (button) =>
- button.getAttribute("href") === "/dashboard/organization/two",
- )
+ // Check Continue button URLs point to correct organizations and contracts
+ const allButtons = screen.getAllByRole("link", { name: "Continue" })
+
+ const org1Contract1Buttons = allButtons.filter(
+ (button) =>
+ button.getAttribute("href") ===
+ "/dashboard/organization/one/contract/org1-contract-1",
+ )
+ const org1Contract2Buttons = allButtons.filter(
+ (button) =>
+ button.getAttribute("href") ===
+ "/dashboard/organization/one/contract/org1-contract-2",
+ )
+ const org2Contract1Buttons = allButtons.filter(
+ (button) =>
+ button.getAttribute("href") ===
+ "/dashboard/organization/two/contract/org2-contract-1",
+ )
- expect(org1Buttons).toHaveLength(4) // 2 contracts × 2 screen sizes
- expect(org2Buttons).toHaveLength(2) // 1 contract × 2 screen sizes
+ expect(org1Contract1Buttons).toHaveLength(2) // mobile + desktop
+ expect(org1Contract2Buttons).toHaveLength(2) // mobile + desktop
+ expect(org2Contract1Buttons).toHaveLength(2) // mobile + desktop
})
})
@@ -335,19 +352,19 @@ describe("OrganizationCards", () => {
})
it("handles slug transformation correctly (removes 'org-' prefix)", async () => {
+ const contract = mitxOnlineFactories.contracts.contract({
+ id: 1,
+ name: "Test Contract",
+ organization: 1,
+ slug: "test-contract",
+ })
const organization: OrganizationPage = {
id: 1,
name: "Test Organization",
description: "Test description",
logo: "https://example.com/logo.png",
slug: "org-my-company",
- contracts: [
- mitxOnlineFactories.contracts.contract({
- id: 1,
- name: "Test Contract",
- organization: 1,
- }),
- ],
+ contracts: [contract],
}
setup({ organizations: [organization] })
@@ -357,24 +374,24 @@ describe("OrganizationCards", () => {
})
expect(continueButtons[0]).toHaveAttribute(
"href",
- "/dashboard/organization/my-company",
+ "/dashboard/organization/my-company/contract/test-contract",
)
})
it("handles slug without 'org-' prefix", async () => {
+ const contract = mitxOnlineFactories.contracts.contract({
+ id: 1,
+ name: "Test Contract",
+ organization: 1,
+ slug: "test-contract",
+ })
const organization: OrganizationPage = {
id: 1,
name: "Test Organization",
description: "Test description",
logo: "https://example.com/logo.png",
slug: "my-company", // No 'org-' prefix
- contracts: [
- mitxOnlineFactories.contracts.contract({
- id: 1,
- name: "Test Contract",
- organization: 1,
- }),
- ],
+ contracts: [contract],
}
setup({ organizations: [organization] })
@@ -384,7 +401,7 @@ describe("OrganizationCards", () => {
})
expect(continueButtons[0]).toHaveAttribute(
"href",
- "/dashboard/organization/my-company",
+ "/dashboard/organization/my-company/contract/test-contract",
)
})
diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.tsx
index 98044121b6..d44d3a071a 100644
--- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.tsx
@@ -6,7 +6,7 @@ import { useQuery } from "@tanstack/react-query"
import { DashboardCardRoot } from "./DashboardCard"
import { mitxUserQueries } from "api/mitxonline-hooks/user"
import { ButtonLink } from "@mitodl/smoot-design"
-import { organizationView } from "@/common/urls"
+import { contractView } from "@/common/urls"
import { OrganizationPage } from "@mitodl/mitxonline-api-axios/v2"
import { RiArrowRightLine } from "@remixicon/react"
@@ -104,7 +104,7 @@ const OrganizationContracts: React.FC = ({
}) => {
const contractContent =
org.contracts?.map((contract) => {
- const href = organizationView(org.slug.replace("org-", ""))
+ const href = contractView(org.slug.replace("org-", ""), contract.slug)
return (
diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts
index 0b7ac92779..d3543728e4 100644
--- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts
+++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts
@@ -152,25 +152,49 @@ const setupEnrollments = (includeExpired: boolean) => {
const setupProgramsAndCourses = () => {
const user = u.factories.user.user()
const orgX = factories.organizations.organization({ name: "Org X" })
+
const contract = makeContract({
organization: orgX.id,
name: "Org X Contract",
+ programs: [], // Will be set after creating programs
})
- orgX.contracts = [contract]
- const mitxOnlineUser = factories.user.user({ b2b_organizations: [orgX] })
- setMockResponse.get(u.urls.userMe.get(), user)
- setMockResponse.get(urls.userMe.get(), mitxOnlineUser)
- setMockResponse.get(urls.organization.organizationList(""), orgX)
- setMockResponse.get(urls.organization.organizationList(orgX.slug), orgX)
const coursesA = makeCourses({ count: 4 })
const coursesB = makeCourses({ count: 3 })
+
+ // Add contract IDs to course runs
+ coursesA.results = coursesA.results.map((course) => ({
+ ...course,
+ courseruns: course.courseruns.map((run) => ({
+ ...run,
+ b2b_contract: contract.id,
+ })),
+ }))
+ coursesB.results = coursesB.results.map((course) => ({
+ ...course,
+ courseruns: course.courseruns.map((run) => ({
+ ...run,
+ b2b_contract: contract.id,
+ })),
+ }))
+
const programA = makeProgram({
courses: coursesA.results.map((c) => c.id),
})
const programB = makeProgram({
courses: coursesB.results.map((c) => c.id),
})
+
+ // Now set the programs on the contract
+ contract.programs = [programA.id, programB.id]
+
+ orgX.contracts = [contract]
+ const mitxOnlineUser = factories.user.user({ b2b_organizations: [orgX] })
+ setMockResponse.get(u.urls.userMe.get(), user)
+ setMockResponse.get(urls.userMe.get(), mitxOnlineUser)
+ setMockResponse.get(urls.organization.organizationList(""), orgX)
+ setMockResponse.get(urls.organization.organizationList(orgX.slug), orgX)
+
const programCollection = makeProgramCollection({
title: "Program Collection",
programs: [],
@@ -179,6 +203,15 @@ const setupProgramsAndCourses = () => {
setMockResponse.get(urls.programs.programsList({ org_id: orgX.id }), {
results: [programA, programB],
})
+ setMockResponse.get(
+ urls.programs.programsList({
+ org_id: orgX.id,
+ contract_id: contract.id,
+ }),
+ {
+ results: [programA, programB],
+ },
+ )
setMockResponse.get(urls.programCollections.programCollectionsList(), {
results: [programCollection],
})
@@ -190,6 +223,16 @@ const setupProgramsAndCourses = () => {
urls.programs.programsList({ id: [programB.id], org_id: orgX.id }),
{ results: [programB] },
)
+ setMockResponse.get(
+ urls.courses.coursesList({
+ org_id: orgX.id,
+ contract_id: contract.id,
+ page_size: 200,
+ }),
+ {
+ results: [...coursesA.results, ...coursesB.results],
+ },
+ )
setMockResponse.get(
urls.courses.coursesList({
id: programA.courses,
@@ -307,6 +350,27 @@ function setupOrgDashboardMocks(
{ results: programs },
)
+ // Mock programs query with contract filter
+ if (contracts.length > 0) {
+ setMockResponse.get(
+ mitxonline.urls.programs.programsList({
+ org_id: org.id,
+ contract_id: contracts[0].id,
+ }),
+ { results: programs },
+ )
+
+ // Mock courses query with contract filter for program validation
+ setMockResponse.get(
+ mitxonline.urls.courses.coursesList({
+ org_id: org.id,
+ contract_id: contracts[0].id,
+ page_size: 200,
+ }),
+ { results: courses },
+ )
+ }
+
programs.forEach((program) => {
setMockResponse.get(
mitxonline.urls.courses.coursesList({
@@ -325,8 +389,11 @@ function setupOrgDashboardMocks(
const createTestContracts = (
orgId: number,
count: number = 1,
+ programs: number[] = [],
): ContractPage[] =>
- Array.from({ length: count }, () => makeContract({ organization: orgId }))
+ Array.from({ length: count }, () =>
+ makeContract({ organization: orgId, programs }),
+ )
/**
* Test utility to create courses with contract-scoped course runs
diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.test.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.test.tsx
index 4a5296f2dd..49de7020d6 100644
--- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.test.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.test.tsx
@@ -155,7 +155,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
const transformedCourses = organizationCoursesWithContracts({
courses,
- contracts,
+ contract: contracts[0],
enrollments: [],
})
@@ -195,7 +195,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
const transformedCourses = organizationCoursesWithContracts({
courses,
- contracts,
+ contract: contracts[0],
enrollments: [],
})
@@ -218,7 +218,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
const transformedCourses = organizationCoursesWithContracts({
courses,
- contracts,
+ contract: contracts[0],
enrollments: enrollments,
})
@@ -265,7 +265,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
const transformedCourses = organizationCoursesWithContracts({
courses,
- contracts,
+ contract: contracts[0],
enrollments: allEnrollments,
})
@@ -293,7 +293,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
const courses = createCoursesWithContractRuns(contracts)
courses.forEach((course) => {
- const transformedCourse = createOrgUnenrolledCourse(course, contracts)
+ const transformedCourse = createOrgUnenrolledCourse(course, contracts[0])
// Should select the run with matching contract
const expectedRun = course.courseruns.find(
@@ -320,7 +320,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
],
})
- const transformedCourse = createOrgUnenrolledCourse(course, contracts)
+ const transformedCourse = createOrgUnenrolledCourse(course, contracts[0])
// Should still return a valid course object
expect(transformedCourse.title).toBe(course.title)
@@ -481,7 +481,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
const transformedCourses = organizationCoursesWithContracts({
courses,
- contracts,
+ contract: contracts[0],
enrollments: enrollments,
})
@@ -529,7 +529,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
const transformedCourses = organizationCoursesWithContracts({
courses,
- contracts,
+ contract: contracts[0],
enrollments: enrollments,
})
@@ -592,7 +592,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
const transformedCourses = organizationCoursesWithContracts({
courses: [course],
- contracts,
+ contract: contracts[0],
enrollments: [enrollmentLowGrade, enrollmentHighGrade],
})
@@ -654,7 +654,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
const transformedCourses = organizationCoursesWithContracts({
courses: [course],
- contracts,
+ contract: contracts[0],
enrollments: [enrollmentHighGradeNoCert, enrollmentWithCert],
})
diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.ts b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.ts
index 4d6e1da568..ac276d5319 100644
--- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.ts
+++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.ts
@@ -183,11 +183,11 @@ const enrollmentsToOrgDashboardEnrollments = (
const createOrgUnenrolledCourse = (
course: CourseWithCourseRunsSerializerV2,
- contracts: ContractPage[] | undefined,
+ contract: ContractPage | undefined,
): DashboardCourse => {
- const contractIds = contracts?.map((contract) => contract.id)
+ const contractId = contract?.id
const run = course.courseruns.find((run) => {
- return run.b2b_contract && contractIds?.includes(run.b2b_contract)
+ return run.b2b_contract && run.b2b_contract === contractId
})
return {
key: getKey({
@@ -240,7 +240,7 @@ const selectBestEnrollment = (
const organizationCoursesWithContracts = (raw: {
courses: CourseWithCourseRunsSerializerV2[]
- contracts?: ContractPage[] // Make optional
+ contract?: ContractPage
enrollments: CourseRunEnrollmentRequestV2[]
}): DashboardCourse[] => {
const enrollmentsByCourseId = groupBy(
@@ -248,20 +248,14 @@ const organizationCoursesWithContracts = (raw: {
(enrollment) => enrollment.run.course.id,
)
- // Get contract IDs for easy lookup
- const contractIds = raw.contracts?.map((contract) => contract.id) || []
-
const transformedCourses = raw.courses.map((course) => {
const enrollments = enrollmentsByCourseId[course.id]
if (enrollments?.length > 0) {
- if (raw.contracts && raw.contracts.length > 0) {
- // Filter enrollments to only include those with valid contracts
+ if (raw.contract) {
+ // Filter enrollments to only include those matching this contract
const contractEnrollments = enrollments.filter((enrollment) => {
- const courseRunContractId = enrollment.b2b_contract_id
- return (
- courseRunContractId && contractIds.includes(courseRunContractId)
- )
+ return enrollment.b2b_contract_id === raw.contract?.id
})
if (contractEnrollments.length > 0) {
@@ -297,8 +291,8 @@ const organizationCoursesWithContracts = (raw: {
}
}
- // If contracts are provided but a matching one isn't found, treat it as unenrolled
- return createOrgUnenrolledCourse(course, raw.contracts)
+ // If contract is provided but a matching enrollment isn't found, treat it as unenrolled
+ return createOrgUnenrolledCourse(course, raw.contract)
} else if (enrollments?.length > 0) {
// If no contracts provided, just find the matching enrollment to the course
const matchingEnrollment = enrollments.find(
@@ -320,7 +314,7 @@ const organizationCoursesWithContracts = (raw: {
}
// If no enrollments or no matching enrollment found, treat it as unenrolled
- return createOrgUnenrolledCourse(course, raw.contracts)
+ return createOrgUnenrolledCourse(course, raw.contract)
})
return transformedCourses
}
diff --git a/frontends/main/src/app-pages/DashboardPage/DashboardLayout.test.tsx b/frontends/main/src/app-pages/DashboardPage/DashboardLayout.test.tsx
index 9b04084e3e..a87e061809 100644
--- a/frontends/main/src/app-pages/DashboardPage/DashboardLayout.test.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/DashboardLayout.test.tsx
@@ -15,7 +15,7 @@ import React from "react"
import {
DASHBOARD_HOME,
MY_LISTS,
- organizationView,
+ contractView,
PROFILE,
SETTINGS,
} from "@/common/urls"
@@ -62,23 +62,25 @@ describe("DashboardLayout", () => {
})
test("Renders the expected tab links and labels", async () => {
+ const contract = mitxOnlineFactories.contracts.contract({
+ name: "Test Contract",
+ })
const organizations = [
mitxOnlineFactories.organizations.organization({
slug: "org-test-org",
name: "Test Organization",
+ contracts: [contract],
}),
]
- const contracts = [
- mitxOnlineFactories.contracts.contract({
- organization: organizations[0].id,
- name: "Test Contract",
- }),
- ]
+ const contracts = [contract]
setup({ organizations, contracts })
const expectedUrls = [
DASHBOARD_HOME,
- ...organizations.map((org) =>
- organizationView(org.slug.replace("org-", "")),
+ ...organizations.map((org, index) =>
+ contractView(
+ org.slug.replace("org-", ""),
+ contracts[index]?.slug ?? "",
+ ),
),
MY_LISTS,
PROFILE,
diff --git a/frontends/main/src/app-pages/DashboardPage/DashboardLayout.tsx b/frontends/main/src/app-pages/DashboardPage/DashboardLayout.tsx
index c6a5ece287..647d04b0a2 100644
--- a/frontends/main/src/app-pages/DashboardPage/DashboardLayout.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/DashboardLayout.tsx
@@ -27,18 +27,16 @@ import { usePathname } from "next/navigation"
import backgroundImage from "@/public/images/backgrounds/user_menu_background.svg"
import {
+ contractView,
DASHBOARD_HOME,
MY_LISTS,
- organizationView,
PROFILE,
SETTINGS,
} from "@/common/urls"
import dynamic from "next/dynamic"
import { MitxOnlineUser, mitxUserQueries } from "api/mitxonline-hooks/user"
import { useUserMe } from "api/hooks/user"
-import { contractQueries } from "api/mitxonline-hooks/contracts"
import { useQuery } from "@tanstack/react-query"
-import { ContractPage } from "@mitodl/mitxonline-api-axios/v2"
const LearningResourceDrawer = dynamic(
() =>
@@ -238,38 +236,37 @@ type TabData = {
desktop: React.ReactNode
}
}
-const getTabData = (
- user?: MitxOnlineUser,
- contracts?: ContractPage[],
-): TabData[] => {
- const orgTabs =
- user && contracts
- ? user?.b2b_organizations.map((org) => {
- const orgContracts = contracts?.filter(
- (contract) => contract.organization === org.id,
- )
- const contract =
- orgContracts && orgContracts.length > 0 ? orgContracts[0] : null
- const label = (
- <>
-
- {org.name}
-
- {` - ${contract?.name}`}
- >
- )
- return {
- value: organizationView(org.slug.replace("org-", "")),
- href: organizationView(org.slug.replace("org-", "")),
- label: {
- mobile: label,
- desktop: (
- } text={label} />
- ),
- },
- }
+const getTabData = (user?: MitxOnlineUser): TabData[] => {
+ const orgTabs = user
+ ? user?.b2b_organizations
+ .map((org) => {
+ return org.contracts.map((contract) => {
+ const label = (
+ <>
+
+ {org.name}
+
+ {` - ${contract.name}`}
+ >
+ )
+ const href = contractView(
+ org.slug.replace("org-", ""),
+ contract.slug,
+ )
+ return {
+ value: href,
+ href: href,
+ label: {
+ mobile: label,
+ desktop: (
+ } text={label} />
+ ),
+ },
+ }
+ })
})
- : []
+ .flat()
+ : []
return [
{
value: DASHBOARD_HOME,
@@ -319,16 +316,10 @@ const DashboardPage: React.FC<{
...mitxUserQueries.me(),
},
)
- const { data: contracts, isLoading: isLoadingContracts } = useQuery(
- contractQueries.contractsList(),
- )
const tabData = useMemo(
- () =>
- isLoadingMitxOnlineUser || isLoadingContracts
- ? getTabData()
- : getTabData(mitxOnlineUser, contracts),
- [isLoadingMitxOnlineUser, isLoadingContracts, mitxOnlineUser, contracts],
+ () => (isLoadingMitxOnlineUser ? getTabData() : getTabData(mitxOnlineUser)),
+ [isLoadingMitxOnlineUser, mitxOnlineUser],
)
const tabValue = useMemo(() => {
diff --git a/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.test.tsx b/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.test.tsx
index b335835edc..cbea1946c1 100644
--- a/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.test.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.test.tsx
@@ -24,13 +24,17 @@ describe("OrganizationRedirect", () => {
setMockResponse.get(urls.contracts.contractsList(), [])
})
- test("navigates to user's first organization", async () => {
+ test("navigates to user's first organization's first contract", async () => {
const { mitxOnlineUser } = setupOrgAndUser()
+ const contract = factories.contracts.contract({})
const userWithTwoOrgs = {
...mitxOnlineUser,
b2b_organizations: [
- mitxOnlineUser.b2b_organizations[0],
+ {
+ ...mitxOnlineUser.b2b_organizations[0],
+ contracts: [contract],
+ },
factories.organizations.organization({}),
],
}
@@ -40,20 +44,55 @@ describe("OrganizationRedirect", () => {
renderWithProviders()
await waitFor(() => {
+ const orgSlug = userWithTwoOrgs.b2b_organizations[0].slug.replace(
+ "org-",
+ "",
+ )
+ const contractSlug = contract.slug
expect(mockReplace).toHaveBeenCalledWith(
- `/dashboard/organization/${mitxOnlineUser.b2b_organizations[0].slug.replace("org-", "")}`,
+ `/dashboard/organization/${orgSlug}/contract/${contractSlug}`,
)
})
})
- test("navigates to user's last visited organization", async () => {
+ test("navigates to user's last visited organization and contract", async () => {
const { mitxOnlineUser } = setupOrgAndUser()
setMockResponse.get(urls.userMe.get(), mitxOnlineUser)
localStorage.setItem("last-dashboard-org", "last-visited-org")
+ localStorage.setItem("last-dashboard-contract", "last-visited-contract")
+ renderWithProviders()
+ await waitFor(() => {
+ expect(mockReplace).toHaveBeenCalledWith(
+ "/dashboard/organization/last-visited-org/contract/last-visited-contract",
+ )
+ })
+ })
+
+ test("navigates to first contract if only org in localStorage", async () => {
+ const { mitxOnlineUser } = setupOrgAndUser()
+
+ const contract = factories.contracts.contract({})
+ const userWithContract = {
+ ...mitxOnlineUser,
+ b2b_organizations: [
+ {
+ ...mitxOnlineUser.b2b_organizations[0],
+ contracts: [contract],
+ },
+ ],
+ }
+
+ setMockResponse.get(urls.userMe.get(), userWithContract)
+ localStorage.setItem("last-dashboard-org", "last-visited-org")
renderWithProviders()
await waitFor(() => {
+ const orgSlug = userWithContract.b2b_organizations[0].slug.replace(
+ "org-",
+ "",
+ )
+ const contractSlug = contract.slug
expect(mockReplace).toHaveBeenCalledWith(
- "/dashboard/organization/last-visited-org",
+ `/dashboard/organization/${orgSlug}/contract/${contractSlug}`,
)
})
})
@@ -74,4 +113,36 @@ describe("OrganizationRedirect", () => {
expect(mockReplace).toHaveBeenCalledWith("/dashboard")
})
})
+
+ test("navigates to dashboard home if organization has no contracts", async () => {
+ const { mitxOnlineUser } = setupOrgAndUser()
+
+ const userWithOrgButNoContract = {
+ ...mitxOnlineUser,
+ b2b_organizations: [
+ {
+ ...mitxOnlineUser.b2b_organizations[0],
+ contracts: [],
+ },
+ ],
+ }
+
+ setMockResponse.get(urls.userMe.get(), userWithOrgButNoContract)
+
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(mockReplace).toHaveBeenCalledWith("/dashboard")
+ })
+ })
+
+ test("navigates to dashboard home if user is not authenticated", async () => {
+ setMockResponse.get(urls.userMe.get(), null)
+
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(mockReplace).toHaveBeenCalledWith("/dashboard")
+ })
+ })
})
diff --git a/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.tsx b/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.tsx
index 8ac20f5b3a..d443a42b9e 100644
--- a/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.tsx
@@ -3,6 +3,7 @@
import React, { useEffect } from "react"
import { useRouter } from "next-nprogress-bar"
import { useQuery } from "@tanstack/react-query"
+import { contractView } from "@/common/urls"
import { mitxUserQueries } from "api/mitxonline-hooks/user"
import { Skeleton } from "ol-components"
@@ -18,13 +19,22 @@ const OrganizationRedirect: React.FC = () => {
if (mitxOnlineUser) {
const b2bOrganization = mitxOnlineUser.b2b_organizations[0]
if (b2bOrganization) {
- const lastVisited = localStorage.getItem("last-dashboard-org")
- if (lastVisited) {
- router.replace(`/dashboard/organization/${lastVisited}`)
+ const lastOrg = localStorage.getItem("last-dashboard-org")
+ const lastContract = localStorage.getItem("last-dashboard-contract")
+ if (lastOrg && lastContract) {
+ const contractUrl = contractView(lastOrg, lastContract)
+ router.replace(contractUrl)
} else {
- router.replace(
- `/dashboard/organization/${b2bOrganization.slug.replace("org-", "")}`,
- )
+ const contract = b2bOrganization.contracts[0]
+ if (contract) {
+ const contractUrl = contractView(
+ b2bOrganization.slug.replace("org-", ""),
+ contract.slug,
+ )
+ router.replace(contractUrl)
+ } else {
+ router.replace("/dashboard")
+ }
}
} else {
router.replace("/dashboard")
diff --git a/frontends/main/src/app/dashboard/organization/[orgSlug]/contract/[contractSlug]/page.tsx b/frontends/main/src/app/dashboard/organization/[orgSlug]/contract/[contractSlug]/page.tsx
new file mode 100644
index 0000000000..d15d94abe3
--- /dev/null
+++ b/frontends/main/src/app/dashboard/organization/[orgSlug]/contract/[contractSlug]/page.tsx
@@ -0,0 +1,16 @@
+import React from "react"
+import ContractContent from "@/app-pages/DashboardPage/ContractContent"
+
+const Page: React.FC<
+ PageProps<"/dashboard/organization/[orgSlug]/contract/[contractSlug]">
+> = async ({ params }) => {
+ const resolved = await params
+ return (
+
+ )
+}
+
+export default Page
diff --git a/frontends/main/src/app/dashboard/organization/[orgSlug]/page.test.tsx b/frontends/main/src/app/dashboard/organization/[orgSlug]/page.test.tsx
new file mode 100644
index 0000000000..ebb8428454
--- /dev/null
+++ b/frontends/main/src/app/dashboard/organization/[orgSlug]/page.test.tsx
@@ -0,0 +1,202 @@
+import React from "react"
+import { renderWithProviders, waitFor } from "@/test-utils"
+import Page from "./page"
+import { setMockResponse } from "api/test-utils"
+import { urls, factories } from "api/mitxonline-test-utils"
+import { setupOrgAndUser } from "@/app-pages/DashboardPage/CoursewareDisplay/test-utils"
+import { act } from "@testing-library/react"
+import { contractView, DASHBOARD_HOME } from "@/common/urls"
+
+jest.mock("next-nprogress-bar", () => ({
+ useRouter: jest.fn(),
+}))
+
+const mockReplace = jest.fn()
+
+const { useRouter } = jest.requireMock("next-nprogress-bar")
+useRouter.mockReturnValue({
+ replace: mockReplace,
+})
+
+describe("Organization Page", () => {
+ beforeEach(() => {
+ mockReplace.mockClear()
+ setMockResponse.get(urls.programEnrollments.enrollmentsList(), [])
+ setMockResponse.get(urls.contracts.contractsList(), [])
+ })
+
+ test("redirects to contract view with first contract when organization and contract exist", async () => {
+ const { mitxOnlineUser } = setupOrgAndUser()
+
+ const contract = factories.contracts.contract({})
+ const userWithContract = {
+ ...mitxOnlineUser,
+ b2b_organizations: [
+ {
+ ...mitxOnlineUser.b2b_organizations[0],
+ contracts: [contract],
+ },
+ ],
+ }
+
+ setMockResponse.get(urls.userMe.get(), userWithContract)
+
+ const orgSlug = userWithContract.b2b_organizations[0].slug.replace(
+ "org-",
+ "",
+ )
+
+ // eslint-disable-next-line testing-library/no-unnecessary-act
+ await act(async () => {
+ renderWithProviders()
+ })
+
+ await waitFor(() => {
+ expect(mockReplace).toHaveBeenCalledWith(
+ contractView(orgSlug, contract.slug),
+ )
+ })
+ })
+
+ test("redirects to dashboard when organization has no contracts", async () => {
+ const { mitxOnlineUser } = setupOrgAndUser()
+
+ const userWithNoContracts = {
+ ...mitxOnlineUser,
+ b2b_organizations: [
+ {
+ ...mitxOnlineUser.b2b_organizations[0],
+ contracts: [],
+ },
+ ],
+ }
+
+ setMockResponse.get(urls.userMe.get(), userWithNoContracts)
+
+ const orgSlug = userWithNoContracts.b2b_organizations[0].slug.replace(
+ "org-",
+ "",
+ )
+
+ // eslint-disable-next-line testing-library/no-unnecessary-act
+ await act(async () => {
+ renderWithProviders()
+ })
+
+ await waitFor(() => {
+ expect(mockReplace).toHaveBeenCalledWith(DASHBOARD_HOME)
+ })
+ })
+
+ test("redirects to dashboard when organization is not found", async () => {
+ const { mitxOnlineUser } = setupOrgAndUser()
+ setMockResponse.get(urls.userMe.get(), mitxOnlineUser)
+
+ // eslint-disable-next-line testing-library/no-unnecessary-act
+ await act(async () => {
+ renderWithProviders(
+ ,
+ )
+ })
+
+ await waitFor(() => {
+ expect(mockReplace).toHaveBeenCalledWith(DASHBOARD_HOME)
+ })
+ })
+
+ test("redirects to first contract when organization has multiple contracts", async () => {
+ const { mitxOnlineUser } = setupOrgAndUser()
+
+ const contract1 = factories.contracts.contract({ slug: "first-contract" })
+ const contract2 = factories.contracts.contract({ slug: "second-contract" })
+
+ const userWithMultipleContracts = {
+ ...mitxOnlineUser,
+ b2b_organizations: [
+ {
+ ...mitxOnlineUser.b2b_organizations[0],
+ contracts: [contract1, contract2],
+ },
+ ],
+ }
+
+ setMockResponse.get(urls.userMe.get(), userWithMultipleContracts)
+
+ const orgSlug = userWithMultipleContracts.b2b_organizations[0].slug.replace(
+ "org-",
+ "",
+ )
+
+ // eslint-disable-next-line testing-library/no-unnecessary-act
+ await act(async () => {
+ renderWithProviders()
+ })
+
+ await waitFor(() => {
+ expect(mockReplace).toHaveBeenCalledWith(
+ contractView(orgSlug, contract1.slug),
+ )
+ })
+ })
+
+ test("redirects correctly when matching organization slug with org- prefix", async () => {
+ const { mitxOnlineUser } = setupOrgAndUser()
+
+ const contract = factories.contracts.contract({})
+ const userWithContract = {
+ ...mitxOnlineUser,
+ b2b_organizations: [
+ {
+ ...mitxOnlineUser.b2b_organizations[0],
+ slug: "org-test-organization",
+ contracts: [contract],
+ },
+ ],
+ }
+
+ setMockResponse.get(urls.userMe.get(), userWithContract)
+
+ // eslint-disable-next-line testing-library/no-unnecessary-act
+ await act(async () => {
+ renderWithProviders(
+ ,
+ )
+ })
+
+ await waitFor(() => {
+ expect(mockReplace).toHaveBeenCalledWith(
+ contractView("test-organization", contract.slug),
+ )
+ })
+ })
+
+ test("redirects to dashboard when no contract available", async () => {
+ const { mitxOnlineUser } = setupOrgAndUser()
+
+ const userWithNoContracts = {
+ ...mitxOnlineUser,
+ b2b_organizations: [
+ {
+ ...mitxOnlineUser.b2b_organizations[0],
+ contracts: [],
+ },
+ ],
+ }
+
+ setMockResponse.get(urls.userMe.get(), userWithNoContracts)
+
+ const orgSlug = userWithNoContracts.b2b_organizations[0].slug.replace(
+ "org-",
+ "",
+ )
+
+ // eslint-disable-next-line testing-library/no-unnecessary-act
+ await act(async () => {
+ renderWithProviders()
+ })
+
+ await waitFor(() => {
+ expect(mockReplace).toHaveBeenCalledWith(DASHBOARD_HOME)
+ })
+ })
+})
diff --git a/frontends/main/src/app/dashboard/organization/[orgSlug]/page.tsx b/frontends/main/src/app/dashboard/organization/[orgSlug]/page.tsx
new file mode 100644
index 0000000000..e64e8739a9
--- /dev/null
+++ b/frontends/main/src/app/dashboard/organization/[orgSlug]/page.tsx
@@ -0,0 +1,39 @@
+"use client"
+
+import React, { use, useEffect } from "react"
+import { useQuery } from "@tanstack/react-query"
+import { mitxUserQueries } from "api/mitxonline-hooks/user"
+import { matchOrganizationBySlug } from "@/common/utils"
+import { useRouter } from "next-nprogress-bar"
+import { contractView, DASHBOARD_HOME } from "@/common/urls"
+
+const Page: React.FC<{
+ params: Promise<{ orgSlug: string }>
+}> = ({ params }) => {
+ const router = useRouter()
+ const { isLoading: isLoadingMitxOnlineUser, data: mitxOnlineUser } = useQuery(
+ mitxUserQueries.me(),
+ )
+
+ const resolved = use(params)
+ const orgSlug = resolved.orgSlug
+
+ const b2bOrganization = mitxOnlineUser?.b2b_organizations.find(
+ matchOrganizationBySlug(orgSlug),
+ )
+ const firstContractSlug = b2bOrganization?.contracts[0]?.slug
+
+ useEffect(() => {
+ if (!isLoadingMitxOnlineUser) {
+ if (firstContractSlug) {
+ router.replace(contractView(orgSlug, firstContractSlug))
+ } else {
+ router.replace(DASHBOARD_HOME)
+ }
+ }
+ }, [isLoadingMitxOnlineUser, firstContractSlug, orgSlug, router])
+
+ return null
+}
+
+export default Page
diff --git a/frontends/main/src/app/dashboard/organization/[slug]/page.tsx b/frontends/main/src/app/dashboard/organization/[slug]/page.tsx
deleted file mode 100644
index 690c46961b..0000000000
--- a/frontends/main/src/app/dashboard/organization/[slug]/page.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from "react"
-import OrganizationContent from "@/app-pages/DashboardPage/OrganizationContent"
-import invariant from "tiny-invariant"
-
-const Page: React.FC> = async ({
- params,
-}) => {
- const resolved = await params
- invariant(resolved.slug, "slug is required")
- return
-}
-
-export default Page
diff --git a/frontends/main/src/common/urls.ts b/frontends/main/src/common/urls.ts
index f43d6f2dcb..4dabfcfc2b 100644
--- a/frontends/main/src/common/urls.ts
+++ b/frontends/main/src/common/urls.ts
@@ -69,9 +69,10 @@ export const SETTINGS = dashboardView("settings")
export const USERLIST_VIEW = "/dashboard/my-lists/[id]"
export const userListView = (id: number) =>
generatePath(USERLIST_VIEW, { id: String(id) })
-export const ORGANIZATION_VIEW = "/dashboard/organization/[slug]"
-export const organizationView = (slug: string) =>
- generatePath(ORGANIZATION_VIEW, { slug: slug })
+export const CONTRACT_VIEW =
+ "/dashboard/organization/[orgSlug]/contract/[contractSlug]"
+export const contractView = (orgSlug: string, contractSlug: string) =>
+ generatePath(CONTRACT_VIEW, { orgSlug: orgSlug, contractSlug: contractSlug })
export const PROGRAM_VIEW = "/dashboard/program/[id]"
export const programView = (id: number) =>
generatePath(PROGRAM_VIEW, { id: String(id) })
diff --git a/frontends/main/src/common/utils.ts b/frontends/main/src/common/utils.ts
index dd19c3e1bb..c41091a078 100644
--- a/frontends/main/src/common/utils.ts
+++ b/frontends/main/src/common/utils.ts
@@ -1,3 +1,5 @@
+import { OrganizationPage } from "@mitodl/mitxonline-api-axios/v2"
+
const isInEnum = (
value: string,
enumObject: Record,
@@ -5,4 +7,9 @@ const isInEnum = (
return Object.values(enumObject).includes(value as T)
}
-export { isInEnum }
+const matchOrganizationBySlug =
+ (orgSlug: string) => (organization: OrganizationPage) => {
+ return organization.slug.replace("org-", "") === orgSlug
+ }
+
+export { isInEnum, matchOrganizationBySlug }