diff --git a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx index 4ab71c50db..05924aaeba 100644 --- a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx @@ -728,4 +728,14 @@ describe("OrganizationContent", () => { expect(cardStatus).toHaveTextContent("Not Enrolled") } }) + + test("shows the not found screen if the organization is not found by orgSlug", async () => { + const { mitxOnlineUser } = setupOrgAndUser() + + setMockResponse.get(urls.userMe.get(), mitxOnlineUser) + + renderWithProviders() + + await screen.findByRole("heading", { name: "Organization not found" }) + }) }) diff --git a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx index b3d804df5c..60840229ff 100644 --- a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx +++ b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx @@ -1,6 +1,6 @@ "use client" -import React from "react" +import React, { useEffect } from "react" import DOMPurify from "isomorphic-dompurify" import Image from "next/image" import { useFeatureFlagEnabled } from "posthog-js/react" @@ -27,9 +27,10 @@ import { OrganizationPage, UserProgramEnrollmentDetail, } from "@mitodl/mitxonline-api-axios/v2" -import { useMitxOnlineUserMe } from "api/mitxonline-hooks/user" +import { mitxUserQueries } from "api/mitxonline-hooks/user" import { ButtonLink } from "@mitodl/smoot-design" import { RiAwardFill } from "@remixicon/react" +import { ErrorContent } from "../ErrorPage/ErrorPageTemplate" const HeaderRoot = styled.div({ display: "flex", @@ -426,24 +427,44 @@ const OrganizationContentInternal: React.FC< ) } +const matchOrganizationBySlug = + (orgSlug: string) => (organization: OrganizationPage) => { + return organization.slug.replace("org-", "") === orgSlug + } + type OrganizationContentProps = { orgSlug: string } const OrganizationContent: React.FC = ({ orgSlug, }) => { - const { isLoading: isLoadingMitxOnlineUser, data: mitxOnlineUser } = - useMitxOnlineUserMe() - const b2bOrganization = mitxOnlineUser?.b2b_organizations.find( - (org) => org.slug.replace("org-", "") === orgSlug, + const { isLoading: isLoadingMitxOnlineUser, data: mitxOnlineUser } = useQuery( + mitxUserQueries.me(), ) - const skeleton = ( - + + useEffect(() => { + if ( + mitxOnlineUser?.b2b_organizations.find(matchOrganizationBySlug(orgSlug)) + ) { + localStorage.setItem("last-dashboard-org", orgSlug) + } + }, [mitxOnlineUser, orgSlug]) + + if (isLoadingMitxOnlineUser) { + return ( + + ) + } + + const b2bOrganization = mitxOnlineUser?.b2b_organizations.find( + matchOrganizationBySlug(orgSlug), ) - if (isLoadingMitxOnlineUser || isLoadingMitxOnlineUser) return skeleton - return b2bOrganization ? ( - - ) : null + + if (!b2bOrganization) { + return + } + + return } export default OrganizationContent diff --git a/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.test.tsx b/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.test.tsx new file mode 100644 index 0000000000..2d8547be52 --- /dev/null +++ b/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.test.tsx @@ -0,0 +1,78 @@ +import React from "react" +import { renderWithProviders, waitFor } from "@/test-utils" +import OrganizationRedirect from "./OrganizationRedirect" +import { setMockResponse } from "api/test-utils" +import { urls, factories } from "api/mitxonline-test-utils" +import { setupOrgAndUser } from "./CoursewareDisplay/test-utils" + +jest.mock("next-nprogress-bar", () => ({ + useRouter: jest.fn(), +})) + +const mockReplace = jest.fn() + +const { useRouter } = jest.requireMock("next-nprogress-bar") +useRouter.mockReturnValue({ + replace: mockReplace, +}) + +describe("OrganizationRedirect", () => { + beforeEach(() => { + mockReplace.mockClear() + localStorage.clear() + setMockResponse.get(urls.enrollment.enrollmentsList(), []) + setMockResponse.get(urls.programEnrollments.enrollmentsList(), []) + setMockResponse.get(urls.contracts.contractsList(), []) + }) + + test("navigates to user's first organization", async () => { + const { mitxOnlineUser } = setupOrgAndUser() + + const userWithTwoOrgs = { + ...mitxOnlineUser, + b2b_organizations: [ + mitxOnlineUser.b2b_organizations[0], + factories.organizations.organization({}), + ], + } + + setMockResponse.get(urls.userMe.get(), userWithTwoOrgs) + + renderWithProviders() + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith( + `/dashboard/organization/${mitxOnlineUser.b2b_organizations[0].slug.replace("org-", "")}`, + ) + }) + }) + + test("navigates to user's last visited organization", async () => { + const { mitxOnlineUser } = setupOrgAndUser() + setMockResponse.get(urls.userMe.get(), mitxOnlineUser) + localStorage.setItem("last-dashboard-org", "last-visited-org") + renderWithProviders() + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith( + "/dashboard/organization/last-visited-org", + ) + }) + }) + + test("navigates to dashboard home if user has no organization", async () => { + const { mitxOnlineUser } = setupOrgAndUser() + + const userWithNoOrgs = { + ...mitxOnlineUser, + b2b_organizations: [], + } + + setMockResponse.get(urls.userMe.get(), userWithNoOrgs) + + 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 new file mode 100644 index 0000000000..8ac20f5b3a --- /dev/null +++ b/frontends/main/src/app-pages/DashboardPage/OrganizationRedirect.tsx @@ -0,0 +1,47 @@ +"use client" + +import React, { useEffect } from "react" +import { useRouter } from "next-nprogress-bar" +import { useQuery } from "@tanstack/react-query" +import { mitxUserQueries } from "api/mitxonline-hooks/user" +import { Skeleton } from "ol-components" + +const OrganizationRedirect: React.FC = () => { + const router = useRouter() + + const { isLoading: isLoadingMitxOnlineUser, data: mitxOnlineUser } = useQuery( + mitxUserQueries.me(), + ) + + useEffect(() => { + if (!isLoadingMitxOnlineUser) { + if (mitxOnlineUser) { + const b2bOrganization = mitxOnlineUser.b2b_organizations[0] + if (b2bOrganization) { + const lastVisited = localStorage.getItem("last-dashboard-org") + if (lastVisited) { + router.replace(`/dashboard/organization/${lastVisited}`) + } else { + router.replace( + `/dashboard/organization/${b2bOrganization.slug.replace("org-", "")}`, + ) + } + } else { + router.replace("/dashboard") + } + } else { + router.replace("/dashboard") + } + } + }, [isLoadingMitxOnlineUser, mitxOnlineUser, router]) + + if (isLoadingMitxOnlineUser) { + return ( + + ) + } + + return null +} + +export default OrganizationRedirect diff --git a/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx b/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx index 173e2aac8d..000e0212ff 100644 --- a/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx +++ b/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx @@ -81,6 +81,33 @@ const Button = styled(ButtonLink)({ minWidth: "200px", }) +export const ErrorContent: React.FC = ({ + title, + timSays, +}) => { + return ( + + + + {timSays || "Oops!"} + + + + {title} + +
+ +
+
+ ) +} + const ErrorPageTemplate: React.FC = ({ title, timSays, @@ -109,25 +136,7 @@ const ErrorPageTemplate: React.FC = ({ } return ( - - - - {timSays || "Oops!"} - - - - {title} - -
- -
-
+
) } diff --git a/frontends/main/src/app/dashboard/organization/[slug]/page.tsx b/frontends/main/src/app/dashboard/organization/[slug]/page.tsx index 1eb88be809..690c46961b 100644 --- a/frontends/main/src/app/dashboard/organization/[slug]/page.tsx +++ b/frontends/main/src/app/dashboard/organization/[slug]/page.tsx @@ -6,8 +6,8 @@ const Page: React.FC> = async ({ params, }) => { const resolved = await params - invariant(resolved?.slug, "slug is required") - return + invariant(resolved.slug, "slug is required") + return } export default Page diff --git a/frontends/main/src/app/dashboard/organization/page.tsx b/frontends/main/src/app/dashboard/organization/page.tsx new file mode 100644 index 0000000000..f825983677 --- /dev/null +++ b/frontends/main/src/app/dashboard/organization/page.tsx @@ -0,0 +1,8 @@ +import React from "react" +import OrganizationRedirect from "@/app-pages/DashboardPage/OrganizationRedirect" + +const Page: React.FC = async () => { + return +} + +export default Page