Repository: CBIIT/crdc-datahub-ui
Pull request #682: CRDCDH-2473 Create Add/Edit Institutions page
Author: login: Alejandro-Vega
Created: 2025-04-14T17:26:25Z | Merged: 2025-04-15T19:55:41Z

### Overview

Create the Add/Edit Institution page.

> [!NOTE]
> The BE does not yet include support for passing the status to the createInstitution API so it will fail. Will be added later. Although, updating is working fine.

### Change Details (Specifics)

- Added `getInstitution`, `createInstitution`, and `updateInstitution` API definitions
- Modeled page after add/edit programs page 
- Added name and status fields for creating and editing an Institution
- Added navigation back to list page on save or on cancel
- Left error messages to be handled by BE 

### Related Ticket(s)

[CRDCDH-2473](https://tracker.nci.nih.gov/browse/CRDCDH-2473) (Task)
[CRDCDH-2442](https://tracker.nci.nih.gov/browse/CRDCDH-2442) (US 1)
[CRDCDH-2446](https://tracker.nci.nih.gov/browse/CRDCDH-2446) (US 2)



Diff:
diff --git a/src/assets/icons/institutions_icon.svg b/src/assets/icons/institutions_icon.svg
new file mode 100644
index 000000000..a8264c7e5
--- /dev/null
+++ b/src/assets/icons/institutions_icon.svg
@@ -0,0 +1,32 @@
+<svg width="188" height="188" viewBox="0 0 188 188" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <circle cx="94" cy="90" r="90" fill="black" fill-opacity="0.2" />
+    <g filter="url(#filter0_d_4283_6979)">
+        <circle cx="94" cy="90" r="86" stroke="#26B893" stroke-width="8" shape-rendering="crispEdges" />
+    </g>
+    <g clip-path="url(#clip0_4283_6979)">
+        <path
+            d="M94.22 8C48.81 8 12 44.81 12 90.22C12 135.63 48.81 172.44 94.22 172.44C139.63 172.44 176.44 135.63 176.44 90.22C176.44 44.81 139.63 8 94.22 8ZM152.03 139.81V67.06H126.58V77.32L134.84 81.95C134.84 81.95 134.89 81.98 134.91 82C134.98 82.04 135.04 82.09 135.1 82.14C135.16 82.18 135.21 82.23 135.26 82.28C135.31 82.33 135.36 82.38 135.4 82.43C135.44 82.48 135.5 82.54 135.54 82.6C135.58 82.66 135.61 82.71 135.65 82.77C135.69 82.83 135.72 82.9 135.75 82.96C135.78 83.03 135.8 83.09 135.82 83.15C135.84 83.21 135.87 83.29 135.89 83.36C135.91 83.43 135.92 83.49 135.93 83.56C135.94 83.64 135.95 83.72 135.95 83.8C135.95 83.83 135.95 83.86 135.95 83.88V112.12L144.09 116.56C144.09 116.56 144.14 116.6 144.17 116.61C144.24 116.65 144.3 116.7 144.37 116.75C144.43 116.79 144.48 116.84 144.53 116.89C144.58 116.94 144.63 116.99 144.68 117.04C144.73 117.09 144.78 117.15 144.82 117.21C144.86 117.27 144.9 117.32 144.93 117.39C144.97 117.45 145 117.51 145.03 117.58C145.06 117.65 145.09 117.71 145.11 117.78C145.13 117.85 145.16 117.92 145.17 117.99C145.19 118.06 145.2 118.12 145.21 118.19C145.22 118.27 145.23 118.35 145.23 118.43C145.23 118.46 145.23 118.49 145.23 118.52V146.76C143.8 148.05 142.32 149.27 140.8 150.45V119.83L96.32 95.54V166.37C95.61 166.39 94.91 166.42 94.2 166.42C93.42 166.42 92.65 166.38 91.88 166.36V95.31L49.05 115.72C48.63 116.24 48 116.55 47.33 116.55H47.31L40.26 119.91V144C38.7 142.44 37.22 140.81 35.81 139.12V118.51C35.81 118.51 35.82 118.42 35.82 118.38C35.82 118.29 35.83 118.2 35.85 118.12C35.86 118.06 35.88 117.99 35.9 117.93C35.92 117.86 35.94 117.78 35.97 117.71C36 117.65 36.02 117.58 36.06 117.52C36.09 117.45 36.13 117.39 36.17 117.32C36.21 117.26 36.25 117.21 36.29 117.16C36.33 117.1 36.38 117.04 36.43 116.99C36.48 116.94 36.53 116.9 36.58 116.85C36.63 116.8 36.69 116.75 36.75 116.71C36.82 116.66 36.89 116.62 36.96 116.58C37 116.55 37.04 116.53 37.09 116.5L45.12 112.67V94.42H29.07V129.71C22.05 118.18 18 104.64 18 90.18C18 48.19 52.19 14 94.22 14C136.25 14 170.44 48.19 170.44 90.22C170.44 109.15 163.49 126.47 152.03 139.81Z"
+            fill="white" />
+        <path d="M49.5703 110.58L91.9003 90.4104V65.1904L49.5703 85.2804V110.58Z" fill="white" />
+        <path d="M58.9697 75.8996L91.8997 60.2696V34.3496L58.9697 56.2896V75.8996Z" fill="white" />
+        <path d="M122.13 56.1502L96.3398 34.9102V60.3802L122.13 74.8202V56.1502Z" fill="white" />
+        <path d="M96.3398 90.4897L131.53 109.71V85.1797L96.3398 65.4697V90.4897Z" fill="white" />
+    </g>
+    <defs>
+        <filter id="filter0_d_4283_6979" x="0" y="0" width="188" height="188" filterUnits="userSpaceOnUse"
+            color-interpolation-filters="sRGB">
+            <feFlood flood-opacity="0" result="BackgroundImageFix" />
+            <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
+                result="hardAlpha" />
+            <feOffset dy="4" />
+            <feGaussianBlur stdDeviation="2" />
+            <feComposite in2="hardAlpha" operator="out" />
+            <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
+            <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4283_6979" />
+            <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4283_6979" result="shape" />
+        </filter>
+        <clipPath id="clip0_4283_6979">
+            <rect width="164.44" height="164.44" fill="white" transform="translate(12 8)" />
+        </clipPath>
+    </defs>
+</svg>
\ No newline at end of file
diff --git a/src/content/Institutions/Controller.tsx b/src/content/Institutions/Controller.tsx
index bf1dc4568..764a534c7 100644
--- a/src/content/Institutions/Controller.tsx
+++ b/src/content/Institutions/Controller.tsx
@@ -1,14 +1,11 @@
-import { FC, memo } from "react";
+import { FC } from "react";
 import { Navigate, useParams } from "react-router-dom";
 import { Status, useAuthContext } from "../../components/Contexts/AuthContext";
-import { InstitutionProvider } from "../../components/Contexts/InstitutionListContext";
 import ListView from "./ListView";
 import SuspenseLoader from "../../components/SuspenseLoader";
 import { hasPermission } from "../../config/AuthPermissions";
 import InstitutionView from "./InstitutionView";
 
-const MemoizedProvider = memo(InstitutionProvider);
-
 /**
  * Renders the correct view based on the URL and permissions-tier
  *
@@ -30,11 +27,7 @@ const InstitutionController: FC = () => {
     return <ListView />;
   }
 
-  return (
-    <MemoizedProvider>
-      <InstitutionView _id={institutionId} />
-    </MemoizedProvider>
-  );
+  return <InstitutionView _id={institutionId} />;
 };
 
 export default InstitutionController;
diff --git a/src/content/Institutions/InstitutionView.test.tsx b/src/content/Institutions/InstitutionView.test.tsx
new file mode 100644
index 000000000..e2b4a6c35
--- /dev/null
+++ b/src/content/Institutions/InstitutionView.test.tsx
@@ -0,0 +1,328 @@
+import React from "react";
+import { render, waitFor, within } from "@testing-library/react";
+import { MemoryRouter, MemoryRouterProps } from "react-router-dom";
+import userEvent from "@testing-library/user-event";
+import { axe } from "jest-axe";
+import { MockedProvider, MockedResponse } from "@apollo/client/testing";
+import InstitutionView from "./InstitutionView";
+import {
+  GET_INSTITUTION,
+  CREATE_INSTITUTION,
+  UPDATE_INSTITUTION,
+  GetInstitutionResp,
+  GetInstitutionInput,
+  CreateInstitutionResp,
+  CreateInstitutionInput,
+  UpdateInstitutionResp,
+  UpdateInstitutionInput,
+} from "../../graphql";
+import { SearchParamsProvider } from "../../components/Contexts/SearchParamsContext";
+
+const mockUsePageTitle = jest.fn();
+jest.mock("../../hooks/usePageTitle", () => ({
+  ...jest.requireActual("../../hooks/usePageTitle"),
+  __esModule: true,
+  default: (p) => mockUsePageTitle(p),
+}));
+
+const mockNavigate = jest.fn();
+jest.mock("react-router-dom", () => ({
+  ...jest.requireActual("react-router-dom"),
+  useNavigate: () => mockNavigate,
+}));
+
+const getInstitutionMock: MockedResponse<GetInstitutionResp, GetInstitutionInput> = {
+  request: {
+    query: GET_INSTITUTION,
+    variables: { _id: "institution-1" },
+  },
+  result: {
+    data: {
+      getInstitution: {
+        _id: "inst-1",
+        name: "Test Institution",
+        status: "Active",
+      },
+    },
+  },
+};
+
+const getInstitutionLoadingMock: MockedResponse<GetInstitutionResp, GetInstitutionInput> = {
+  request: {
+    query: GET_INSTITUTION,
+    variables: { _id: "institution-1" },
+  },
+  result: {
+    data: {
+      getInstitution: {
+        _id: "inst-1",
+        name: "Test Institution",
+        status: "Active",
+      },
+    },
+  },
+  delay: 1000,
+};
+
+const createInstitutionMock: MockedResponse<CreateInstitutionResp, CreateInstitutionInput> = {
+  request: {
+    query: CREATE_INSTITUTION,
+    variables: { name: "New Institution", status: "Active" },
+  },
+  result: {
+    data: {
+      createInstitution: {
+        _id: "new-id",
+      },
+    },
+  },
+};
+
+const updateInstitutionMock: MockedResponse<UpdateInstitutionResp, UpdateInstitutionInput> = {
+  request: {
+    query: UPDATE_INSTITUTION,
+    variables: { _id: "institution-1", name: "Updated Institution", status: "Inactive" },
+  },
+  result: {
+    data: {
+      updateInstitution: {
+        _id: "institution-1",
+        name: "Updated Institution",
+        status: "Inactive",
+      },
+    },
+  },
+};
+
+const createInstitutionErrorMock: MockedResponse<CreateInstitutionResp, CreateInstitutionInput> = {
+  request: {
+    query: CREATE_INSTITUTION,
+    variables: { name: "New Institution", status: "Active" },
+  },
+  error: new Error("There was a problem while creating a new Institution."),
+};
+
+const updateInstitutionErrorMock: MockedResponse<UpdateInstitutionResp, UpdateInstitutionInput> = {
+  request: {
+    query: UPDATE_INSTITUTION,
+    variables: { _id: "institution-1", name: "Updated Institution", status: "Inactive" },
+  },
+  error: new Error("There was a problem while updating the Institution."),
+};
+
+type ParentProps = {
+  mocks?: MockedResponse[];
+  initialEntries?: MemoryRouterProps["initialEntries"];
+  children: React.ReactNode;
+};
+
+const TestParent: React.FC<ParentProps> = ({ mocks = [], initialEntries = ["/"], children }) => (
+  <MockedProvider mocks={mocks} addTypename={false}>
+    <MemoryRouter initialEntries={initialEntries}>
+      <SearchParamsProvider>{children}</SearchParamsProvider>
+    </MemoryRouter>
+  </MockedProvider>
+);
+
+describe("Accessibility", () => {
+  it("has no accessibility violations", async () => {
+    const { container } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const results = await axe(container);
+    expect(results).toHaveNoViolations();
+  });
+});
+
+describe("InstitutionView Component", () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it("renders without crashing", () => {
+    const { getByTestId } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    expect(getByTestId("institution-form")).toBeInTheDocument();
+    expect(getByTestId("institutionName-input")).toBeInTheDocument();
+    expect(getByTestId("save-button")).toBeInTheDocument();
+    expect(getByTestId("cancel-button")).toBeInTheDocument();
+  });
+
+  it("should set the page title to 'Add Institution' when _id is 'new'", async () => {
+    render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect(mockUsePageTitle).toHaveBeenCalledWith("Add Institution");
+    });
+  });
+
+  it("should set the page title to 'Edit Institution institution-1' when editing", async () => {
+    render(
+      <TestParent mocks={[getInstitutionMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect(mockUsePageTitle).toHaveBeenCalledWith("Edit Institution institution-1");
+    });
+  });
+
+  it("creates a new institution successfully", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[createInstitutionMock]}>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    const saveButton = getByTestId("save-button");
+
+    userEvent.type(nameInput, "New Institution");
+
+    expect(mockNavigate).not.toHaveBeenCalled();
+    userEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith("Institution added successfully.", {
+        variant: "success",
+      });
+    });
+
+    expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+  });
+
+  it("updates an existing institution successfully", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[getInstitutionMock, updateInstitutionMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect((getByTestId("institutionName-input") as HTMLInputElement).value).toBe(
+        "Test Institution"
+      );
+    });
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    userEvent.clear(nameInput);
+    userEvent.type(nameInput, "Updated Institution");
+
+    const statusSelect = within(getByTestId("status-select")).getByRole("button");
+    userEvent.click(statusSelect);
+    const listbox = within(getByTestId("status-select")).getByRole("listbox", { hidden: true });
+    userEvent.click(within(listbox).getByText("Inactive"));
+
+    expect(mockNavigate).not.toHaveBeenCalled();
+    userEvent.click(getByTestId("save-button"));
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith("Institution updated successfully.", {
+        variant: "success",
+      });
+    });
+
+    expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+  });
+
+  it("navigates back to manage institutions page when cancel button is clicked", () => {
+    const { getByTestId } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    expect(mockNavigate).not.toHaveBeenCalled();
+
+    const cancelButton = getByTestId("cancel-button");
+    userEvent.click(cancelButton);
+
+    expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+  });
+
+  it("handles API errors gracefully when creating a new institution", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[createInstitutionErrorMock]}>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    const saveButton = getByTestId("save-button");
+
+    userEvent.type(nameInput, "New Institution");
+
+    expect(mockNavigate).not.toHaveBeenCalled();
+    userEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith(
+        "There was a problem while creating a new Institution.",
+        {
+          variant: "error",
+        }
+      );
+    });
+
+    expect(mockNavigate).not.toHaveBeenCalled();
+  });
+
+  it("handles API errors gracefully when updating an existing institution", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[getInstitutionMock, updateInstitutionErrorMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect((getByTestId("institutionName-input") as HTMLInputElement).value).toBe(
+        "Test Institution"
+      );
+    });
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    userEvent.clear(nameInput);
+    userEvent.type(nameInput, "Updated Institution");
+
+    const statusSelect = within(getByTestId("status-select")).getByRole("button");
+    userEvent.click(statusSelect);
+    const listbox = within(getByTestId("status-select")).getByRole("listbox", { hidden: true });
+    userEvent.click(within(listbox).getByText("Inactive"));
+
+    expect(mockNavigate).not.toHaveBeenCalled();
+    userEvent.click(getByTestId("save-button"));
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith(
+        "There was a problem while updating the Institution.",
+        { variant: "error" }
+      );
+    });
+
+    expect(mockNavigate).not.toHaveBeenCalled();
+  });
+
+  it("should show a loading spinner while retrieving the institution", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[getInstitutionLoadingMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect(getByTestId("institution-view-suspense-loader")).toBeInTheDocument();
+    });
+  });
+});
diff --git a/src/content/Institutions/InstitutionView.tsx b/src/content/Institutions/InstitutionView.tsx
index 252461c39..dadafa1fa 100644
--- a/src/content/Institutions/InstitutionView.tsx
+++ b/src/content/Institutions/InstitutionView.tsx
@@ -1,7 +1,332 @@
+import { useState } from "react";
+import { Box, Container, MenuItem, Stack, styled, Typography } from "@mui/material";
+import { LoadingButton } from "@mui/lab";
+import { useNavigate } from "react-router-dom";
+import { Controller, SubmitHandler, useForm } from "react-hook-form";
+import { useMutation, useQuery } from "@apollo/client";
+import { useSnackbar } from "notistack";
+import bannerSvg from "../../assets/banner/profile_banner.png";
+import institutionIcon from "../../assets/icons/institutions_icon.svg";
+import usePageTitle from "../../hooks/usePageTitle";
+import StyledAsterisk from "../../components/StyledFormComponents/StyledAsterisk";
+import BaseOutlinedInput from "../../components/StyledFormComponents/StyledOutlinedInput";
+import BaseSelect from "../../components/StyledFormComponents/StyledSelect";
+import { useSearchParamsContext } from "../../components/Contexts/SearchParamsContext";
+import {
+  CREATE_INSTITUTION,
+  CreateInstitutionInput,
+  CreateInstitutionResp,
+  GET_INSTITUTION,
+  GetInstitutionInput,
+  GetInstitutionResp,
+  UPDATE_INSTITUTION,
+  UpdateInstitutionInput,
+  UpdateInstitutionResp,
+} from "../../graphql";
+import { Logger } from "../../utils";
+import SuspenseLoader from "../../components/SuspenseLoader";
+
+const StyledBanner = styled("div")({
+  background: `url(${bannerSvg})`,
+  backgroundBlendMode: "luminosity, normal",
+  backgroundSize: "cover",
+  backgroundPosition: "center",
+  width: "100%",
+  height: "153px",
+});
+
+const StyledContainer = styled(Container)({
+  marginBottom: "90px",
+});
+
+const StyledIcon = styled("div")({
+  position: "relative",
+  transform: "translate(-218px, -75px)",
+  "& img": {
+    position: "absolute",
+  },
+  "& img:nth-of-type(1)": {
+    zIndex: 2,
+    filter: "drop-shadow(10px 13px 9px rgba(0, 0, 0, 0.35))",
+  },
+});
+
+const StyledContentStack = styled(Stack)({
+  marginLeft: "2px !important",
+});
+
+const StyledTitleBox = styled(Box)({
+  marginTop: "-86px",
+  marginBottom: "88px",
+  width: "100%",
+});
+
+const StyledPageTitle = styled(Typography)({
+  fontFamily: "Nunito Sans",
+  fontSize: "45px",
+  fontWeight: 800,
+  letterSpacing: "-1.5px",
+  color: "#fff",
+});
+
+const StyledField = styled("div")({
+  marginBottom: "10px",
+  minHeight: "41px",
+  display: "flex",
+  alignItems: "center",
+  justifyContent: "space-between",
+  gap: "40px",
+  fontSize: "18px",
+});
+
+const StyledLabel = styled("span")({
+  color: "#356AAD",
+  fontWeight: "700",
+  minWidth: "127px",
+});
+
+const BaseInputStyling = {
+  width: "363px",
+};
+
+const StyledTextField = styled(BaseOutlinedInput)(BaseInputStyling);
+const StyledSelect = styled(BaseSelect)(BaseInputStyling);
+
+const StyledButtonStack = styled(Stack)({
+  marginTop: "50px",
+});
+
+const StyledButton = styled(LoadingButton)({
+  minWidth: "120px",
+  fontSize: "16px",
+  padding: "10px",
+  lineHeight: "24px",
+});
+
+type FormInput = Pick<Institution, "name" | "status">;
+
 type Props = {
   _id: string;
 };
 
-const InstitutionView = ({ _id }: Props) => <div>InstitutionView</div>;
+const InstitutionView = ({ _id }: Props) => {
+  const isNew = _id && _id === "new";
+  usePageTitle(isNew ? "Add Institution" : `Edit Institution ${_id}`);
+  const navigate = useNavigate();
+  const { enqueueSnackbar } = useSnackbar();
+  const { lastSearchParams } = useSearchParamsContext();
+  const {
+    handleSubmit,
+    register,
+    control,
+    reset,
+    formState: { errors },
+  } = useForm<FormInput>({
+    mode: "onSubmit",
+    reValidateMode: "onSubmit",
+    defaultValues: {
+      name: "",
+      status: "Active",
+    },
+  });
+
+  const [saving, setSaving] = useState<boolean>(false);
+
+  const manageInstitutionsPageUrl = `/institutions${lastSearchParams?.["/institutions"] ?? ""}`;
+
+  const { loading: retrievingInstitution } = useQuery<GetInstitutionResp, GetInstitutionInput>(
+    GET_INSTITUTION,
+    {
+      variables: { _id },
+      skip: isNew,
+      onCompleted: (data) => {
+        const { name, status } = data?.getInstitution || {};
+        reset({ name, status });
+      },
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      onError: (error) => onError([error], "Unable to retrieve Institution."),
+    }
+  );
+
+  const [createInstitution] = useMutation<CreateInstitutionResp, CreateInstitutionInput>(
+    CREATE_INSTITUTION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to create a new Institution.");
+        }
+      },
+    }
+  );
+
+  const [updateInstitution] = useMutation<UpdateInstitutionResp, UpdateInstitutionInput>(
+    UPDATE_INSTITUTION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to save changes.");
+        }
+      },
+    }
+  );
+
+  const onError = <T extends { message: string }>(errors: readonly T[], message: string) => {
+    if (!errors?.length) {
+      return;
+    }
+
+    const flatErrors = errors?.flatMap((err) => err.message)?.join(" ");
+    Logger.error(`InstitutionView: ${message} ${flatErrors}`);
+    enqueueSnackbar(flatErrors, { variant: "error" });
+  };
+
+  const handleCreateInstitution = async ({ name, status }: FormInput): Promise<boolean> => {
+    const { data: d, errors } = await createInstitution({ variables: { name, status } });
+
+    if (errors || !d?.createInstitution?._id) {
+      onError(errors, "Unable to create a new Institution.");
+      return false;
+    }
+
+    enqueueSnackbar("Institution added successfully.", { variant: "success" });
+    return true;
+  };
+
+  const handleUpdateInstitution = async ({ name, status }: FormInput): Promise<boolean> => {
+    const { data: d, errors } = await updateInstitution({ variables: { _id, name, status } });
+
+    if (errors || !d?.updateInstitution?._id) {
+      onError(errors, "Unable to save changes.");
+      return false;
+    }
+
+    enqueueSnackbar("Institution updated successfully.", { variant: "success" });
+    reset({ ...d.updateInstitution });
+    return true;
+  };
+
+  const onSubmit: SubmitHandler<FormInput> = async (data: FormInput) => {
+    setSaving(true);
+
+    const action = isNew ? handleCreateInstitution : handleUpdateInstitution;
+    const success = await action(data);
+
+    setSaving(false);
+
+    if (success) {
+      navigate(manageInstitutionsPageUrl);
+    }
+  };
+
+  if (retrievingInstitution) {
+    return <SuspenseLoader data-testid="institution-view-suspense-loader" />;
+  }
+
+  return (
+    <Box>
+      <StyledBanner />
+      <StyledContainer maxWidth="lg">
+        <Stack direction="row" justifyContent="center" alignItems="flex-start" spacing={2}>
+          <StyledIcon>
+            <img src={institutionIcon} alt="Institution icon" />
+          </StyledIcon>
+
+          <StyledContentStack
+            direction="column"
+            justifyContent="center"
+            alignItems="center"
+            spacing={2}
+          >
+            <StyledTitleBox>
+              <StyledPageTitle variant="h1">
+                {isNew ? "Add Institution" : "Edit Institution"}
+              </StyledPageTitle>
+            </StyledTitleBox>
+
+            <form onSubmit={handleSubmit(onSubmit)} data-testid="institution-form">
+              <StyledField>
+                <StyledLabel id="institutionNameLabel">
+                  Name
+                  <StyledAsterisk />
+                </StyledLabel>
+                <StyledTextField
+                  {...register("name", { required: true, setValueAs: (val) => val?.trim() })}
+                  size="small"
+                  required
+                  disabled={retrievingInstitution}
+                  readOnly={saving}
+                  error={!!errors.name}
+                  inputProps={{
+                    "aria-labelledby": "institutionNameLabel",
+                    "data-testid": "institutionName-input",
+                  }}
+                />
+              </StyledField>
+
+              <StyledField>
+                <StyledLabel id="statusLabel">
+                  Status
+                  <StyledAsterisk />
+                </StyledLabel>
+                <Controller
+                  name="status"
+                  control={control}
+                  rules={{ required: true }}
+                  render={({ field }) => (
+                    <StyledSelect
+                      {...field}
+                      size="small"
+                      readOnly={retrievingInstitution}
+                      MenuProps={{ disablePortal: true }}
+                      error={!!errors.status}
+                      inputProps={{ "aria-labelledby": "statusLabel" }}
+                      data-testid="status-select"
+                    >
+                      <MenuItem value="Active">Active</MenuItem>
+                      <MenuItem value="Inactive">Inactive</MenuItem>
+                    </StyledSelect>
+                  )}
+                />
+              </StyledField>
+
+              <StyledButtonStack
+                direction="row"
+                justifyContent="center"
+                alignItems="center"
+                spacing={1}
+              >
+                <StyledButton
+                  data-testid="save-button"
+                  type="submit"
+                  loading={saving || retrievingInstitution}
+                  variant="contained"
+                  color="success"
+                >
+                  Save
+                </StyledButton>
+                <StyledButton
+                  data-testid="cancel-button"
+                  type="button"
+                  onClick={() => navigate(manageInstitutionsPageUrl)}
+                  variant="contained"
+                  color="info"
+                >
+                  Cancel
+                </StyledButton>
+              </StyledButtonStack>
+            </form>
+          </StyledContentStack>
+        </Stack>
+      </StyledContainer>
+    </Box>
+  );
+};
 
 export default InstitutionView;
diff --git a/src/graphql/createInstitution.ts b/src/graphql/createInstitution.ts
new file mode 100644
index 000000000..6aa1b85bd
--- /dev/null
+++ b/src/graphql/createInstitution.ts
@@ -0,0 +1,19 @@
+import { TypedDocumentNode } from "@apollo/client";
+import gql from "graphql-tag";
+
+export const mutation: TypedDocumentNode<Response, Input> = gql`
+  mutation createInstitution($name: String!, $status: String!) {
+    createInstitution(name: $name, status: $status) {
+      _id
+    }
+  }
+`;
+
+export type Input = {
+  name: string;
+  status: string;
+};
+
+export type Response = {
+  createInstitution: Pick<Institution, "_id">;
+};
diff --git a/src/graphql/getInstitution.ts b/src/graphql/getInstitution.ts
new file mode 100644
index 000000000..c1f1ebce2
--- /dev/null
+++ b/src/graphql/getInstitution.ts
@@ -0,0 +1,20 @@
+import { TypedDocumentNode } from "@apollo/client";
+import gql from "graphql-tag";
+
+export const query: TypedDocumentNode<Response, Input> = gql`
+  query getInstitution($_id: ID!) {
+    getInstitution(_id: $_id) {
+      _id
+      name
+      status
+    }
+  }
+`;
+
+export type Input = {
+  _id: string;
+};
+
+export type Response = {
+  getInstitution: Pick<Institution, "_id" | "name" | "status">;
+};
diff --git a/src/graphql/index.ts b/src/graphql/index.ts
index 47f9de388..36479b1e9 100644
--- a/src/graphql/index.ts
+++ b/src/graphql/index.ts
@@ -35,12 +35,6 @@ export type { Response as GetAppResp } from "./getApplication";
 export { mutation as UPDATE_MY_USER } from "./updateMyUser";
 export type { Input as UpdateMyUserInput, Response as UpdateMyUserResp } from "./updateMyUser";
 
-export { query as LIST_INSTITUTIONS } from "./listInstitutions";
-export type {
-  Input as ListInstitutionsInput,
-  Response as ListInstitutionsResp,
-} from "./listInstitutions";
-
 export { query as RETRIEVE_CDEs } from "./retrieveCDEs";
 export type { Response as RetrieveCDEsResp, Input as RetrieveCDEsInput } from "./retrieveCDEs";
 
@@ -231,6 +225,31 @@ export type {
 export { mutation as CREATE_ORG } from "./createOrganization";
 export type { Input as CreateOrgInput, Response as CreateOrgResp } from "./createOrganization";
 
+// Institutions
+export { query as LIST_INSTITUTIONS } from "./listInstitutions";
+export type {
+  Input as ListInstitutionsInput,
+  Response as ListInstitutionsResp,
+} from "./listInstitutions";
+
+export { query as GET_INSTITUTION } from "./getInstitution";
+export type {
+  Input as GetInstitutionInput,
+  Response as GetInstitutionResp,
+} from "./getInstitution";
+
+export { mutation as CREATE_INSTITUTION } from "./createInstitution";
+export type {
+  Input as CreateInstitutionInput,
+  Response as CreateInstitutionResp,
+} from "./createInstitution";
+
+export { mutation as UPDATE_INSTITUTION } from "./updateInstitution";
+export type {
+  Input as UpdateInstitutionInput,
+  Response as UpdateInstitutionResp,
+} from "./updateInstitution";
+
 // Misc.
 export { mutation as GRANT_TOKEN } from "./grantToken";
 export type { Response as GrantTokenResp } from "./grantToken";
diff --git a/src/graphql/updateInstitution.ts b/src/graphql/updateInstitution.ts
new file mode 100644
index 000000000..4f3a14296
--- /dev/null
+++ b/src/graphql/updateInstitution.ts
@@ -0,0 +1,22 @@
+import { TypedDocumentNode } from "@apollo/client";
+import gql from "graphql-tag";
+
+export const mutation: TypedDocumentNode<Response, Input> = gql`
+  mutation updateInstitution($_id: ID!, $name: String, $status: String) {
+    updateInstitution(_id: $_id, name: $name, status: $status) {
+      _id
+      name
+      status
+    }
+  }
+`;
+
+export type Input = {
+  _id: string;
+  name: string;
+  status: string;
+};
+
+export type Response = {
+  updateInstitution: Pick<Institution, "_id" | "name" | "status">;
+};


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**src/content/Institutions/InstitutionView.tsx:16**
* The imported constant 'CREATE_INSTITUION' appears to be misspelled. Align the name with the correct spelling 'CREATE_INSTITUTION' to prevent potential confusion.
```
import { CREATE_INSTITUION, CreateInstitutionInput, CreateInstitutionResp, GET_INSTITUTION, GetInstitutionInput, GetInstitutionResp, UPDATE_INSTITUTION, UpdateInstitutionInput, UpdateInstitutionResp, } from "../../graphql";
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T13:25:48Z
user: login: amattu2
body: All in all, it seems to work well! It looks like I left a lot of comments, but they're mostly duplicates of eachother.

Backend issues I noticed during testing:
- Casing is not ignored for name duplicate check (e.g. "NCI" and "nci" would work). I don't think this should be allowed, but reqs don't say
- Duplicate name error message is not correct
- Creating as inactive doesn't work (you already noted this)
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T13:59:17Z
user: login: Alejandro-Vega
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:31:38Z
user: login: Alejandro-Vega
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:33:39Z
user: login: Alejandro-Vega
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:36:27Z
user: login: Alejandro-Vega
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:36:33Z
user: login: Alejandro-Vega
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:36:39Z
user: login: Alejandro-Vega
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:44:42Z
user: login: Alejandro-Vega
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T15:05:53Z
user: login: Alejandro-Vega
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T15:06:20Z
user: login: Alejandro-Vega
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T15:11:50Z
user: login: amattu2
body: LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T19:55:32Z

Review comments:
diff_hunk: @@ -231,6 +225,31 @@ export type {
 export { mutation as CREATE_ORG } from "./createOrganization";
 export type { Input as CreateOrgInput, Response as CreateOrgResp } from "./createOrganization";
 
+// Institutions
+export { query as LIST_INSTITUTIONS } from "./listInstitutions";
+export type {
+  Input as ListInstitutionsInput,
+  Response as ListInstitutionsResp,
+} from "./listInstitutions";
+
+export { query as GET_INSTITUTION } from "./getInstitution";
+export type {
+  Input as GetInstitutionInput,
+  Response as GetInstitutionResp,
+} from "./getInstitution";
+
+export { mutation as CREATE_INSTITUION } from "./createInstitution";
path: src/graphql/index.ts
user: login: Copilot
body: There appears to be a typo in the constant name 'CREATE_INSTITUION'. Consider renaming it to 'CREATE_INSTITUTION' for clarity and consistency.
```suggestion
export { mutation as CREATE_INSTITUTION } from "./createInstitution";
```
created_at: 2025-04-15T13:25:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,322 @@
+import { useState } from "react";
+import { Box, Container, MenuItem, Stack, styled, Typography } from "@mui/material";
+import { LoadingButton } from "@mui/lab";
+import { useNavigate } from "react-router-dom";
+import { Controller, useForm } from "react-hook-form";
+import { useMutation, useQuery } from "@apollo/client";
+import { useSnackbar } from "notistack";
+import bannerSvg from "../../assets/banner/profile_banner.png";
+import institutionIcon from "../../assets/icons/institutions_icon.svg";
+import usePageTitle from "../../hooks/usePageTitle";
+import StyledAsterisk from "../../components/StyledFormComponents/StyledAsterisk";
+import BaseOutlinedInput from "../../components/StyledFormComponents/StyledOutlinedInput";
+import BaseSelect from "../../components/StyledFormComponents/StyledSelect";
+import { useSearchParamsContext } from "../../components/Contexts/SearchParamsContext";
+import {
+  CREATE_INSTITUION,
+  CreateInstitutionInput,
+  CreateInstitutionResp,
+  GET_INSTITUTION,
+  GetInstitutionInput,
+  GetInstitutionResp,
+  UPDATE_INSTITUTION,
+  UpdateInstitutionInput,
+  UpdateInstitutionResp,
+} from "../../graphql";
+import { Logger } from "../../utils";
+
+const StyledBanner = styled("div")({
+  background: `url(${bannerSvg})`,
+  backgroundBlendMode: "luminosity, normal",
+  backgroundSize: "cover",
+  backgroundPosition: "center",
+  width: "100%",
+  height: "153px",
+});
+
+const StyledContainer = styled(Container)({
+  marginBottom: "90px",
+});
+
+const StyledIcon = styled("div")({
+  position: "relative",
+  transform: "translate(-218px, -75px)",
+  "& img": {
+    position: "absolute",
+  },
+  "& img:nth-of-type(1)": {
+    zIndex: 2,
+    filter: "drop-shadow(10px 13px 9px rgba(0, 0, 0, 0.35))",
+  },
+});
+
+const StyledContentStack = styled(Stack)({
+  marginLeft: "2px !important",
+});
+
+const StyledTitleBox = styled(Box)({
+  marginTop: "-86px",
+  marginBottom: "88px",
+  width: "100%",
+});
+
+const StyledPageTitle = styled(Typography)({
+  fontFamily: "Nunito Sans",
+  fontSize: "45px",
+  fontWeight: 800,
+  letterSpacing: "-1.5px",
+  color: "#fff",
+});
+
+const StyledField = styled("div")({
+  marginBottom: "10px",
+  minHeight: "41px",
+  display: "flex",
+  alignItems: "center",
+  justifyContent: "space-between",
+  gap: "40px",
+  fontSize: "18px",
+});
+
+const StyledLabel = styled("span")({
+  color: "#356AAD",
+  fontWeight: "700",
+  minWidth: "127px",
+});
+
+const BaseInputStyling = {
+  width: "363px",
+};
+
+const StyledTextField = styled(BaseOutlinedInput)(BaseInputStyling);
+const StyledSelect = styled(BaseSelect)(BaseInputStyling);
+
+const StyledButtonStack = styled(Stack)({
+  marginTop: "50px",
+});
+
+const StyledButton = styled(LoadingButton)({
+  minWidth: "120px",
+  fontSize: "16px",
+  padding: "10px",
+  lineHeight: "24px",
+});
+
+type FormInput = Pick<Institution, "name" | "status">;
+
 type Props = {
   _id: string;
 };
 
-const InstitutionView = ({ _id }: Props) => <div>InstitutionView</div>;
+const InstitutionView = ({ _id }: Props) => {
+  const isNew = _id && _id === "new";
+  usePageTitle(isNew ? "Add Institution" : `Edit Institution ${_id}`);
+  const navigate = useNavigate();
+  const { enqueueSnackbar } = useSnackbar();
+  const { lastSearchParams } = useSearchParamsContext();
+  const {
+    handleSubmit,
+    register,
+    control,
+    reset,
+    formState: { errors },
+  } = useForm<FormInput>({
+    mode: "onSubmit",
+    reValidateMode: "onSubmit",
+    defaultValues: {
+      name: "",
+      status: "Active",
+    },
+  });
+
+  const [saving, setSaving] = useState<boolean>(false);
+
+  const manageInstitutionsPageUrl = `/institutions${lastSearchParams?.["/institutions"] ?? ""}`;
+
+  const { loading: retrievingInstitution } = useQuery<GetInstitutionResp, GetInstitutionInput>(
+    GET_INSTITUTION,
+    {
+      variables: { _id },
+      skip: isNew,
+      onCompleted: (data) => {
+        const { name, status } = data?.getInstitution || {};
+        reset({ name, status });
+      },
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      onError: (error) => onError([error], "Unable to retrieve Institution."),
+    }
+  );
+
+  const [createInstitution] = useMutation<CreateInstitutionResp, CreateInstitutionInput>(
+    CREATE_INSTITUION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to create a new Institution.");
+        }
+      },
+    }
+  );
+
+  const [updateInstitution] = useMutation<UpdateInstitutionResp, UpdateInstitutionInput>(
+    UPDATE_INSTITUTION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to save changes.");
+        }
+      },
+    }
+  );
+
+  const onError = <T extends { message: string }>(errors: readonly T[], message: string) => {
+    if (!errors?.length) {
+      return;
+    }
+
+    const flatErrors = errors?.flatMap((err) => err.message)?.join(" ");
+    Logger.error(`InstitutionView: ${message} ${flatErrors}`);
+    enqueueSnackbar(flatErrors, { variant: "error" });
+  };
+
+  const handleCreateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await createInstitution({ variables: { name, status } });
+
+    if (errors || !d?.createInstitution?._id) {
+      onError(errors, "Unable to create a new Institution.");
+      return;
+    }
+
+    enqueueSnackbar("Institution added successfully.", { variant: "success" });
+  };
+
+  const handleUpdateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await updateInstitution({ variables: { _id, name, status } });
+
+    if (errors || !d?.updateInstitution?._id) {
+      onError(errors, "Unable to save changes.");
+      return;
+    }
+
+    enqueueSnackbar("Institution updated successfully.", { variant: "success" });
+    reset({ ...d.updateInstitution });
+  };
+
+  const onSubmit = async (data: FormInput) => {
+    setSaving(true);
+
+    const action = isNew ? handleCreateInstitution : handleUpdateInstitution;
+    await action(data);
+
+    setSaving(false);
+    navigate(manageInstitutionsPageUrl);
+  };
+
path: src/content/Institutions/InstitutionView.tsx
user: login: amattu2
body: To be consistent with the other pages, I'd suggest we add the fullscreen loader here while we're waiting for the data to fetch (unless it's a new institution).
created_at: 2025-04-15T13:34:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,322 @@
+import { useState } from "react";
+import { Box, Container, MenuItem, Stack, styled, Typography } from "@mui/material";
+import { LoadingButton } from "@mui/lab";
+import { useNavigate } from "react-router-dom";
+import { Controller, useForm } from "react-hook-form";
+import { useMutation, useQuery } from "@apollo/client";
+import { useSnackbar } from "notistack";
+import bannerSvg from "../../assets/banner/profile_banner.png";
+import institutionIcon from "../../assets/icons/institutions_icon.svg";
+import usePageTitle from "../../hooks/usePageTitle";
+import StyledAsterisk from "../../components/StyledFormComponents/StyledAsterisk";
+import BaseOutlinedInput from "../../components/StyledFormComponents/StyledOutlinedInput";
+import BaseSelect from "../../components/StyledFormComponents/StyledSelect";
+import { useSearchParamsContext } from "../../components/Contexts/SearchParamsContext";
+import {
+  CREATE_INSTITUION,
+  CreateInstitutionInput,
+  CreateInstitutionResp,
+  GET_INSTITUTION,
+  GetInstitutionInput,
+  GetInstitutionResp,
+  UPDATE_INSTITUTION,
+  UpdateInstitutionInput,
+  UpdateInstitutionResp,
+} from "../../graphql";
+import { Logger } from "../../utils";
+
+const StyledBanner = styled("div")({
+  background: `url(${bannerSvg})`,
+  backgroundBlendMode: "luminosity, normal",
+  backgroundSize: "cover",
+  backgroundPosition: "center",
+  width: "100%",
+  height: "153px",
+});
+
+const StyledContainer = styled(Container)({
+  marginBottom: "90px",
+});
+
+const StyledIcon = styled("div")({
+  position: "relative",
+  transform: "translate(-218px, -75px)",
+  "& img": {
+    position: "absolute",
+  },
+  "& img:nth-of-type(1)": {
+    zIndex: 2,
+    filter: "drop-shadow(10px 13px 9px rgba(0, 0, 0, 0.35))",
+  },
+});
+
+const StyledContentStack = styled(Stack)({
+  marginLeft: "2px !important",
+});
+
+const StyledTitleBox = styled(Box)({
+  marginTop: "-86px",
+  marginBottom: "88px",
+  width: "100%",
+});
+
+const StyledPageTitle = styled(Typography)({
+  fontFamily: "Nunito Sans",
+  fontSize: "45px",
+  fontWeight: 800,
+  letterSpacing: "-1.5px",
+  color: "#fff",
+});
+
+const StyledField = styled("div")({
+  marginBottom: "10px",
+  minHeight: "41px",
+  display: "flex",
+  alignItems: "center",
+  justifyContent: "space-between",
+  gap: "40px",
+  fontSize: "18px",
+});
+
+const StyledLabel = styled("span")({
+  color: "#356AAD",
+  fontWeight: "700",
+  minWidth: "127px",
+});
+
+const BaseInputStyling = {
+  width: "363px",
+};
+
+const StyledTextField = styled(BaseOutlinedInput)(BaseInputStyling);
+const StyledSelect = styled(BaseSelect)(BaseInputStyling);
+
+const StyledButtonStack = styled(Stack)({
+  marginTop: "50px",
+});
+
+const StyledButton = styled(LoadingButton)({
+  minWidth: "120px",
+  fontSize: "16px",
+  padding: "10px",
+  lineHeight: "24px",
+});
+
+type FormInput = Pick<Institution, "name" | "status">;
+
 type Props = {
   _id: string;
 };
 
-const InstitutionView = ({ _id }: Props) => <div>InstitutionView</div>;
+const InstitutionView = ({ _id }: Props) => {
+  const isNew = _id && _id === "new";
+  usePageTitle(isNew ? "Add Institution" : `Edit Institution ${_id}`);
+  const navigate = useNavigate();
+  const { enqueueSnackbar } = useSnackbar();
+  const { lastSearchParams } = useSearchParamsContext();
+  const {
+    handleSubmit,
+    register,
+    control,
+    reset,
+    formState: { errors },
+  } = useForm<FormInput>({
+    mode: "onSubmit",
+    reValidateMode: "onSubmit",
+    defaultValues: {
+      name: "",
+      status: "Active",
+    },
+  });
+
+  const [saving, setSaving] = useState<boolean>(false);
+
+  const manageInstitutionsPageUrl = `/institutions${lastSearchParams?.["/institutions"] ?? ""}`;
+
+  const { loading: retrievingInstitution } = useQuery<GetInstitutionResp, GetInstitutionInput>(
+    GET_INSTITUTION,
+    {
+      variables: { _id },
+      skip: isNew,
+      onCompleted: (data) => {
+        const { name, status } = data?.getInstitution || {};
+        reset({ name, status });
+      },
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      onError: (error) => onError([error], "Unable to retrieve Institution."),
+    }
+  );
+
+  const [createInstitution] = useMutation<CreateInstitutionResp, CreateInstitutionInput>(
+    CREATE_INSTITUION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to create a new Institution.");
+        }
+      },
+    }
+  );
+
+  const [updateInstitution] = useMutation<UpdateInstitutionResp, UpdateInstitutionInput>(
+    UPDATE_INSTITUTION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to save changes.");
+        }
+      },
+    }
+  );
+
+  const onError = <T extends { message: string }>(errors: readonly T[], message: string) => {
+    if (!errors?.length) {
+      return;
+    }
+
+    const flatErrors = errors?.flatMap((err) => err.message)?.join(" ");
+    Logger.error(`InstitutionView: ${message} ${flatErrors}`);
+    enqueueSnackbar(flatErrors, { variant: "error" });
+  };
+
+  const handleCreateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await createInstitution({ variables: { name, status } });
+
+    if (errors || !d?.createInstitution?._id) {
+      onError(errors, "Unable to create a new Institution.");
+      return;
+    }
+
+    enqueueSnackbar("Institution added successfully.", { variant: "success" });
+  };
+
+  const handleUpdateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await updateInstitution({ variables: { _id, name, status } });
+
+    if (errors || !d?.updateInstitution?._id) {
+      onError(errors, "Unable to save changes.");
+      return;
+    }
+
+    enqueueSnackbar("Institution updated successfully.", { variant: "success" });
+    reset({ ...d.updateInstitution });
+  };
+
+  const onSubmit = async (data: FormInput) => {
path: src/content/Institutions/InstitutionView.tsx
user: login: amattu2
body: Trivial suggestion, but could we just add the RHF submit handler typing here?

(Imported from RHF)

```suggestion
  const onSubmit: SubmitHandler<FormInput> = async (data: FormInput) => {
```
created_at: 2025-04-15T13:44:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { TypedDocumentNode } from "@apollo/client";
+import gql from "graphql-tag";
+
+export const mutation: TypedDocumentNode<Input, Response> = gql`
path: src/graphql/createInstitution.ts
user: login: amattu2
body: I think the typing here is reversed. Response then Input. Unless it works both ways?
created_at: 2025-04-15T13:47:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import { TypedDocumentNode } from "@apollo/client";
+import gql from "graphql-tag";
+
+export const query: TypedDocumentNode<Input, Response> = gql`
path: src/graphql/getInstitution.ts
user: login: amattu2
body: Reversed?
created_at: 2025-04-15T13:47:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+import { TypedDocumentNode } from "@apollo/client";
+import gql from "graphql-tag";
+
+export const mutation: TypedDocumentNode<Input, Response> = gql`
path: src/graphql/updateInstitution.ts
user: login: amattu2
body: Reversed
created_at: 2025-04-15T13:47:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,286 @@
+import React from "react";
+import { render, waitFor, within } from "@testing-library/react";
+import { MemoryRouter, MemoryRouterProps } from "react-router-dom";
+import userEvent from "@testing-library/user-event";
+import { axe } from "jest-axe";
+import { MockedProvider, MockedResponse } from "@apollo/client/testing";
+import InstitutionView from "./InstitutionView";
+import {
+  GET_INSTITUTION,
+  CREATE_INSTITUION,
+  UPDATE_INSTITUTION,
+  GetInstitutionResp,
+  GetInstitutionInput,
+  CreateInstitutionResp,
+  CreateInstitutionInput,
+  UpdateInstitutionResp,
+  UpdateInstitutionInput,
+} from "../../graphql";
+import { SearchParamsProvider } from "../../components/Contexts/SearchParamsContext";
+
+const mockUsePageTitle = jest.fn();
+jest.mock("../../hooks/usePageTitle", () => ({
+  ...jest.requireActual("../../hooks/usePageTitle"),
+  __esModule: true,
+  default: (p) => mockUsePageTitle(p),
+}));
+
+const mockNavigate = jest.fn();
+jest.mock("react-router-dom", () => ({
+  ...jest.requireActual("react-router-dom"),
+  useNavigate: () => mockNavigate,
+}));
+
+const getInstitutionMock: MockedResponse<GetInstitutionResp, GetInstitutionInput> = {
+  request: {
+    query: GET_INSTITUTION,
+    variables: { _id: "institution-1" },
+  },
+  result: {
+    data: {
+      getInstitution: {
+        _id: "inst-1",
+        name: "Test Institution",
+        status: "Active",
+      },
+    },
+  },
+};
+
+const createInstitutionMock: MockedResponse<CreateInstitutionResp, CreateInstitutionInput> = {
+  request: {
+    query: CREATE_INSTITUION,
+    variables: { name: "New Institution", status: "Active" },
+  },
+  result: {
+    data: {
+      createInstitution: {
+        _id: "new-id",
+      },
+    },
+  },
+};
+
+const updateInstitutionMock: MockedResponse<UpdateInstitutionResp, UpdateInstitutionInput> = {
+  request: {
+    query: UPDATE_INSTITUTION,
+    variables: { _id: "institution-1", name: "Updated Institution", status: "Inactive" },
+  },
+  result: {
+    data: {
+      updateInstitution: {
+        _id: "institution-1",
+        name: "Updated Institution",
+        status: "Inactive",
+      },
+    },
+  },
+};
+
+const createInstitutionErrorMock: MockedResponse<CreateInstitutionResp, CreateInstitutionInput> = {
+  request: {
+    query: CREATE_INSTITUION,
+    variables: { name: "New Institution", status: "Active" },
+  },
+  error: new Error("There was a problem while creating a new Institution."),
+};
+
+const updateInstitutionErrorMock: MockedResponse<UpdateInstitutionResp, UpdateInstitutionInput> = {
+  request: {
+    query: UPDATE_INSTITUTION,
+    variables: { _id: "institution-1", name: "Updated Institution", status: "Inactive" },
+  },
+  error: new Error("There was a problem while updating the Institution."),
+};
+
+type ParentProps = {
+  mocks?: MockedResponse[];
+  initialEntries?: MemoryRouterProps["initialEntries"];
+  children: React.ReactNode;
+};
+
+const TestParent: React.FC<ParentProps> = ({ mocks = [], initialEntries = ["/"], children }) => (
+  <MockedProvider mocks={mocks} addTypename={false}>
+    <MemoryRouter initialEntries={initialEntries}>
+      <SearchParamsProvider>{children}</SearchParamsProvider>
+    </MemoryRouter>
+  </MockedProvider>
+);
+
+describe("Accessibility", () => {
+  it("has no accessibility violations", async () => {
+    const { container } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const results = await axe(container);
+    expect(results).toHaveNoViolations();
+  });
+});
+
+describe("InstitutionView Component", () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it("renders without crashing", () => {
+    const { getByTestId } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    expect(getByTestId("institution-form")).toBeInTheDocument();
+    expect(getByTestId("institutionName-input")).toBeInTheDocument();
+    expect(getByTestId("save-button")).toBeInTheDocument();
+    expect(getByTestId("cancel-button")).toBeInTheDocument();
+  });
+
+  it("should set the page title to 'Add Institution' when _id is 'new'", async () => {
+    render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect(mockUsePageTitle).toHaveBeenCalledWith("Add Institution");
+    });
+  });
+
+  it("should set the page title to 'Edit Institution institution-1' when editing", async () => {
+    render(
+      <TestParent mocks={[getInstitutionMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect(mockUsePageTitle).toHaveBeenCalledWith("Edit Institution institution-1");
+    });
+  });
+
+  it("creates a new institution successfully", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[createInstitutionMock]}>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    const saveButton = getByTestId("save-button");
+
+    userEvent.type(nameInput, "New Institution");
+    userEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith("Institution added successfully.", {
+        variant: "success",
+      });
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+    });
+  });
+
+  it("updates an existing institution successfully", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[getInstitutionMock, updateInstitutionMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect((getByTestId("institutionName-input") as HTMLInputElement).value).toBe(
+        "Test Institution"
+      );
+    });
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    userEvent.clear(nameInput);
+    userEvent.type(nameInput, "Updated Institution");
+
+    const statusSelect = within(getByTestId("status-select")).getByRole("button");
+    userEvent.click(statusSelect);
+    const listbox = within(getByTestId("status-select")).getByRole("listbox", { hidden: true });
+    userEvent.click(within(listbox).getByText("Inactive"));
+
+    userEvent.click(getByTestId("save-button"));
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith("Institution updated successfully.", {
+        variant: "success",
+      });
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+    });
+  });
+
+  it("navigates back to manage institutions page when cancel button is clicked", () => {
+    const { getByTestId } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const cancelButton = getByTestId("cancel-button");
+    userEvent.click(cancelButton);
+    expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+  });
+
+  it("handles API errors gracefully when creating a new institution", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[createInstitutionErrorMock]}>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    const saveButton = getByTestId("save-button");
+
+    userEvent.type(nameInput, "New Institution");
+    userEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith(
+        "There was a problem while creating a new Institution.",
+        {
+          variant: "error",
+        }
+      );
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+    });
+  });
+
+  it("handles API errors gracefully when updating an existing institution", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[getInstitutionMock, updateInstitutionErrorMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect((getByTestId("institutionName-input") as HTMLInputElement).value).toBe(
+        "Test Institution"
+      );
+    });
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    userEvent.clear(nameInput);
+    userEvent.type(nameInput, "Updated Institution");
+
+    const statusSelect = within(getByTestId("status-select")).getByRole("button");
+    userEvent.click(statusSelect);
+    const listbox = within(getByTestId("status-select")).getByRole("listbox", { hidden: true });
+    userEvent.click(within(listbox).getByText("Inactive"));
+
+    userEvent.click(getByTestId("save-button"));
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith(
+        "There was a problem while updating the Institution.",
+        { variant: "error" }
+      );
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
path: src/content/Institutions/InstitutionView.test.tsx
user: login: amattu2
body: See related annotation in InstitutionView.

Probably need to move this expect out of the waitFor else I think it would wait 5s before completing? (could be wrong).

```suggestion
      expect(mockNavigate).not.toHaveBeenCalled();
```
created_at: 2025-04-15T13:50:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,286 @@
+import React from "react";
+import { render, waitFor, within } from "@testing-library/react";
+import { MemoryRouter, MemoryRouterProps } from "react-router-dom";
+import userEvent from "@testing-library/user-event";
+import { axe } from "jest-axe";
+import { MockedProvider, MockedResponse } from "@apollo/client/testing";
+import InstitutionView from "./InstitutionView";
+import {
+  GET_INSTITUTION,
+  CREATE_INSTITUION,
+  UPDATE_INSTITUTION,
+  GetInstitutionResp,
+  GetInstitutionInput,
+  CreateInstitutionResp,
+  CreateInstitutionInput,
+  UpdateInstitutionResp,
+  UpdateInstitutionInput,
+} from "../../graphql";
+import { SearchParamsProvider } from "../../components/Contexts/SearchParamsContext";
+
+const mockUsePageTitle = jest.fn();
+jest.mock("../../hooks/usePageTitle", () => ({
+  ...jest.requireActual("../../hooks/usePageTitle"),
+  __esModule: true,
+  default: (p) => mockUsePageTitle(p),
+}));
+
+const mockNavigate = jest.fn();
+jest.mock("react-router-dom", () => ({
+  ...jest.requireActual("react-router-dom"),
+  useNavigate: () => mockNavigate,
+}));
+
+const getInstitutionMock: MockedResponse<GetInstitutionResp, GetInstitutionInput> = {
+  request: {
+    query: GET_INSTITUTION,
+    variables: { _id: "institution-1" },
+  },
+  result: {
+    data: {
+      getInstitution: {
+        _id: "inst-1",
+        name: "Test Institution",
+        status: "Active",
+      },
+    },
+  },
+};
+
+const createInstitutionMock: MockedResponse<CreateInstitutionResp, CreateInstitutionInput> = {
+  request: {
+    query: CREATE_INSTITUION,
+    variables: { name: "New Institution", status: "Active" },
+  },
+  result: {
+    data: {
+      createInstitution: {
+        _id: "new-id",
+      },
+    },
+  },
+};
+
+const updateInstitutionMock: MockedResponse<UpdateInstitutionResp, UpdateInstitutionInput> = {
+  request: {
+    query: UPDATE_INSTITUTION,
+    variables: { _id: "institution-1", name: "Updated Institution", status: "Inactive" },
+  },
+  result: {
+    data: {
+      updateInstitution: {
+        _id: "institution-1",
+        name: "Updated Institution",
+        status: "Inactive",
+      },
+    },
+  },
+};
+
+const createInstitutionErrorMock: MockedResponse<CreateInstitutionResp, CreateInstitutionInput> = {
+  request: {
+    query: CREATE_INSTITUION,
+    variables: { name: "New Institution", status: "Active" },
+  },
+  error: new Error("There was a problem while creating a new Institution."),
+};
+
+const updateInstitutionErrorMock: MockedResponse<UpdateInstitutionResp, UpdateInstitutionInput> = {
+  request: {
+    query: UPDATE_INSTITUTION,
+    variables: { _id: "institution-1", name: "Updated Institution", status: "Inactive" },
+  },
+  error: new Error("There was a problem while updating the Institution."),
+};
+
+type ParentProps = {
+  mocks?: MockedResponse[];
+  initialEntries?: MemoryRouterProps["initialEntries"];
+  children: React.ReactNode;
+};
+
+const TestParent: React.FC<ParentProps> = ({ mocks = [], initialEntries = ["/"], children }) => (
+  <MockedProvider mocks={mocks} addTypename={false}>
+    <MemoryRouter initialEntries={initialEntries}>
+      <SearchParamsProvider>{children}</SearchParamsProvider>
+    </MemoryRouter>
+  </MockedProvider>
+);
+
+describe("Accessibility", () => {
+  it("has no accessibility violations", async () => {
+    const { container } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const results = await axe(container);
+    expect(results).toHaveNoViolations();
+  });
+});
+
+describe("InstitutionView Component", () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it("renders without crashing", () => {
+    const { getByTestId } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    expect(getByTestId("institution-form")).toBeInTheDocument();
+    expect(getByTestId("institutionName-input")).toBeInTheDocument();
+    expect(getByTestId("save-button")).toBeInTheDocument();
+    expect(getByTestId("cancel-button")).toBeInTheDocument();
+  });
+
+  it("should set the page title to 'Add Institution' when _id is 'new'", async () => {
+    render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect(mockUsePageTitle).toHaveBeenCalledWith("Add Institution");
+    });
+  });
+
+  it("should set the page title to 'Edit Institution institution-1' when editing", async () => {
+    render(
+      <TestParent mocks={[getInstitutionMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect(mockUsePageTitle).toHaveBeenCalledWith("Edit Institution institution-1");
+    });
+  });
+
+  it("creates a new institution successfully", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[createInstitutionMock]}>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    const saveButton = getByTestId("save-button");
+
+    userEvent.type(nameInput, "New Institution");
+    userEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith("Institution added successfully.", {
+        variant: "success",
+      });
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+    });
+  });
+
+  it("updates an existing institution successfully", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[getInstitutionMock, updateInstitutionMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect((getByTestId("institutionName-input") as HTMLInputElement).value).toBe(
+        "Test Institution"
+      );
+    });
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    userEvent.clear(nameInput);
+    userEvent.type(nameInput, "Updated Institution");
+
+    const statusSelect = within(getByTestId("status-select")).getByRole("button");
+    userEvent.click(statusSelect);
+    const listbox = within(getByTestId("status-select")).getByRole("listbox", { hidden: true });
+    userEvent.click(within(listbox).getByText("Inactive"));
+
+    userEvent.click(getByTestId("save-button"));
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith("Institution updated successfully.", {
+        variant: "success",
+      });
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+    });
+  });
+
+  it("navigates back to manage institutions page when cancel button is clicked", () => {
+    const { getByTestId } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const cancelButton = getByTestId("cancel-button");
+    userEvent.click(cancelButton);
+    expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+  });
+
+  it("handles API errors gracefully when creating a new institution", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[createInstitutionErrorMock]}>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    const saveButton = getByTestId("save-button");
+
+    userEvent.type(nameInput, "New Institution");
+    userEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith(
+        "There was a problem while creating a new Institution.",
+        {
+          variant: "error",
+        }
+      );
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
path: src/content/Institutions/InstitutionView.test.tsx
user: login: amattu2
body: See related annotation in InstitutionView.

Probably need to move this expect out of the waitFor else I think it would wait 5s before completing? (could be wrong).

```suggestion
      expect(mockNavigate).not.toHaveBeenCalled();
```
created_at: 2025-04-15T13:50:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,322 @@
+import { useState } from "react";
+import { Box, Container, MenuItem, Stack, styled, Typography } from "@mui/material";
+import { LoadingButton } from "@mui/lab";
+import { useNavigate } from "react-router-dom";
+import { Controller, useForm } from "react-hook-form";
+import { useMutation, useQuery } from "@apollo/client";
+import { useSnackbar } from "notistack";
+import bannerSvg from "../../assets/banner/profile_banner.png";
+import institutionIcon from "../../assets/icons/institutions_icon.svg";
+import usePageTitle from "../../hooks/usePageTitle";
+import StyledAsterisk from "../../components/StyledFormComponents/StyledAsterisk";
+import BaseOutlinedInput from "../../components/StyledFormComponents/StyledOutlinedInput";
+import BaseSelect from "../../components/StyledFormComponents/StyledSelect";
+import { useSearchParamsContext } from "../../components/Contexts/SearchParamsContext";
+import {
+  CREATE_INSTITUION,
+  CreateInstitutionInput,
+  CreateInstitutionResp,
+  GET_INSTITUTION,
+  GetInstitutionInput,
+  GetInstitutionResp,
+  UPDATE_INSTITUTION,
+  UpdateInstitutionInput,
+  UpdateInstitutionResp,
+} from "../../graphql";
+import { Logger } from "../../utils";
+
+const StyledBanner = styled("div")({
+  background: `url(${bannerSvg})`,
+  backgroundBlendMode: "luminosity, normal",
+  backgroundSize: "cover",
+  backgroundPosition: "center",
+  width: "100%",
+  height: "153px",
+});
+
+const StyledContainer = styled(Container)({
+  marginBottom: "90px",
+});
+
+const StyledIcon = styled("div")({
+  position: "relative",
+  transform: "translate(-218px, -75px)",
+  "& img": {
+    position: "absolute",
+  },
+  "& img:nth-of-type(1)": {
+    zIndex: 2,
+    filter: "drop-shadow(10px 13px 9px rgba(0, 0, 0, 0.35))",
+  },
+});
+
+const StyledContentStack = styled(Stack)({
+  marginLeft: "2px !important",
+});
+
+const StyledTitleBox = styled(Box)({
+  marginTop: "-86px",
+  marginBottom: "88px",
+  width: "100%",
+});
+
+const StyledPageTitle = styled(Typography)({
+  fontFamily: "Nunito Sans",
+  fontSize: "45px",
+  fontWeight: 800,
+  letterSpacing: "-1.5px",
+  color: "#fff",
+});
+
+const StyledField = styled("div")({
+  marginBottom: "10px",
+  minHeight: "41px",
+  display: "flex",
+  alignItems: "center",
+  justifyContent: "space-between",
+  gap: "40px",
+  fontSize: "18px",
+});
+
+const StyledLabel = styled("span")({
+  color: "#356AAD",
+  fontWeight: "700",
+  minWidth: "127px",
+});
+
+const BaseInputStyling = {
+  width: "363px",
+};
+
+const StyledTextField = styled(BaseOutlinedInput)(BaseInputStyling);
+const StyledSelect = styled(BaseSelect)(BaseInputStyling);
+
+const StyledButtonStack = styled(Stack)({
+  marginTop: "50px",
+});
+
+const StyledButton = styled(LoadingButton)({
+  minWidth: "120px",
+  fontSize: "16px",
+  padding: "10px",
+  lineHeight: "24px",
+});
+
+type FormInput = Pick<Institution, "name" | "status">;
+
 type Props = {
   _id: string;
 };
 
-const InstitutionView = ({ _id }: Props) => <div>InstitutionView</div>;
+const InstitutionView = ({ _id }: Props) => {
+  const isNew = _id && _id === "new";
+  usePageTitle(isNew ? "Add Institution" : `Edit Institution ${_id}`);
+  const navigate = useNavigate();
+  const { enqueueSnackbar } = useSnackbar();
+  const { lastSearchParams } = useSearchParamsContext();
+  const {
+    handleSubmit,
+    register,
+    control,
+    reset,
+    formState: { errors },
+  } = useForm<FormInput>({
+    mode: "onSubmit",
+    reValidateMode: "onSubmit",
+    defaultValues: {
+      name: "",
+      status: "Active",
+    },
+  });
+
+  const [saving, setSaving] = useState<boolean>(false);
+
+  const manageInstitutionsPageUrl = `/institutions${lastSearchParams?.["/institutions"] ?? ""}`;
+
+  const { loading: retrievingInstitution } = useQuery<GetInstitutionResp, GetInstitutionInput>(
+    GET_INSTITUTION,
+    {
+      variables: { _id },
+      skip: isNew,
+      onCompleted: (data) => {
+        const { name, status } = data?.getInstitution || {};
+        reset({ name, status });
+      },
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      onError: (error) => onError([error], "Unable to retrieve Institution."),
+    }
+  );
+
+  const [createInstitution] = useMutation<CreateInstitutionResp, CreateInstitutionInput>(
+    CREATE_INSTITUION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to create a new Institution.");
+        }
+      },
+    }
+  );
+
+  const [updateInstitution] = useMutation<UpdateInstitutionResp, UpdateInstitutionInput>(
+    UPDATE_INSTITUTION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to save changes.");
+        }
+      },
+    }
+  );
+
+  const onError = <T extends { message: string }>(errors: readonly T[], message: string) => {
+    if (!errors?.length) {
+      return;
+    }
+
+    const flatErrors = errors?.flatMap((err) => err.message)?.join(" ");
+    Logger.error(`InstitutionView: ${message} ${flatErrors}`);
+    enqueueSnackbar(flatErrors, { variant: "error" });
+  };
+
+  const handleCreateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await createInstitution({ variables: { name, status } });
+
+    if (errors || !d?.createInstitution?._id) {
+      onError(errors, "Unable to create a new Institution.");
+      return;
+    }
+
+    enqueueSnackbar("Institution added successfully.", { variant: "success" });
+  };
+
+  const handleUpdateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await updateInstitution({ variables: { _id, name, status } });
+
+    if (errors || !d?.updateInstitution?._id) {
+      onError(errors, "Unable to save changes.");
+      return;
+    }
+
+    enqueueSnackbar("Institution updated successfully.", { variant: "success" });
+    reset({ ...d.updateInstitution });
+  };
+
+  const onSubmit = async (data: FormInput) => {
+    setSaving(true);
+
+    const action = isNew ? handleCreateInstitution : handleUpdateInstitution;
+    await action(data);
+
+    setSaving(false);
+    navigate(manageInstitutionsPageUrl);
path: src/content/Institutions/InstitutionView.tsx
user: login: amattu2
body: When you try to create/update an institution, and an error occurs, the page is redirecting to the list view instead of remaining on the form page. Unless I'm misreading the reqs, it should just remain on the page. 
created_at: 2025-04-15T13:54:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,322 @@
+import { useState } from "react";
+import { Box, Container, MenuItem, Stack, styled, Typography } from "@mui/material";
+import { LoadingButton } from "@mui/lab";
+import { useNavigate } from "react-router-dom";
+import { Controller, useForm } from "react-hook-form";
+import { useMutation, useQuery } from "@apollo/client";
+import { useSnackbar } from "notistack";
+import bannerSvg from "../../assets/banner/profile_banner.png";
+import institutionIcon from "../../assets/icons/institutions_icon.svg";
+import usePageTitle from "../../hooks/usePageTitle";
+import StyledAsterisk from "../../components/StyledFormComponents/StyledAsterisk";
+import BaseOutlinedInput from "../../components/StyledFormComponents/StyledOutlinedInput";
+import BaseSelect from "../../components/StyledFormComponents/StyledSelect";
+import { useSearchParamsContext } from "../../components/Contexts/SearchParamsContext";
+import {
+  CREATE_INSTITUION,
+  CreateInstitutionInput,
+  CreateInstitutionResp,
+  GET_INSTITUTION,
+  GetInstitutionInput,
+  GetInstitutionResp,
+  UPDATE_INSTITUTION,
+  UpdateInstitutionInput,
+  UpdateInstitutionResp,
+} from "../../graphql";
+import { Logger } from "../../utils";
+
+const StyledBanner = styled("div")({
+  background: `url(${bannerSvg})`,
+  backgroundBlendMode: "luminosity, normal",
+  backgroundSize: "cover",
+  backgroundPosition: "center",
+  width: "100%",
+  height: "153px",
+});
+
+const StyledContainer = styled(Container)({
+  marginBottom: "90px",
+});
+
+const StyledIcon = styled("div")({
+  position: "relative",
+  transform: "translate(-218px, -75px)",
+  "& img": {
+    position: "absolute",
+  },
+  "& img:nth-of-type(1)": {
+    zIndex: 2,
+    filter: "drop-shadow(10px 13px 9px rgba(0, 0, 0, 0.35))",
+  },
+});
+
+const StyledContentStack = styled(Stack)({
+  marginLeft: "2px !important",
+});
+
+const StyledTitleBox = styled(Box)({
+  marginTop: "-86px",
+  marginBottom: "88px",
+  width: "100%",
+});
+
+const StyledPageTitle = styled(Typography)({
+  fontFamily: "Nunito Sans",
+  fontSize: "45px",
+  fontWeight: 800,
+  letterSpacing: "-1.5px",
+  color: "#fff",
+});
+
+const StyledField = styled("div")({
+  marginBottom: "10px",
+  minHeight: "41px",
+  display: "flex",
+  alignItems: "center",
+  justifyContent: "space-between",
+  gap: "40px",
+  fontSize: "18px",
+});
+
+const StyledLabel = styled("span")({
+  color: "#356AAD",
+  fontWeight: "700",
+  minWidth: "127px",
+});
+
+const BaseInputStyling = {
+  width: "363px",
+};
+
+const StyledTextField = styled(BaseOutlinedInput)(BaseInputStyling);
+const StyledSelect = styled(BaseSelect)(BaseInputStyling);
+
+const StyledButtonStack = styled(Stack)({
+  marginTop: "50px",
+});
+
+const StyledButton = styled(LoadingButton)({
+  minWidth: "120px",
+  fontSize: "16px",
+  padding: "10px",
+  lineHeight: "24px",
+});
+
+type FormInput = Pick<Institution, "name" | "status">;
+
 type Props = {
   _id: string;
 };
 
-const InstitutionView = ({ _id }: Props) => <div>InstitutionView</div>;
+const InstitutionView = ({ _id }: Props) => {
+  const isNew = _id && _id === "new";
+  usePageTitle(isNew ? "Add Institution" : `Edit Institution ${_id}`);
+  const navigate = useNavigate();
+  const { enqueueSnackbar } = useSnackbar();
+  const { lastSearchParams } = useSearchParamsContext();
+  const {
+    handleSubmit,
+    register,
+    control,
+    reset,
+    formState: { errors },
+  } = useForm<FormInput>({
+    mode: "onSubmit",
+    reValidateMode: "onSubmit",
+    defaultValues: {
+      name: "",
+      status: "Active",
+    },
+  });
+
+  const [saving, setSaving] = useState<boolean>(false);
+
+  const manageInstitutionsPageUrl = `/institutions${lastSearchParams?.["/institutions"] ?? ""}`;
+
+  const { loading: retrievingInstitution } = useQuery<GetInstitutionResp, GetInstitutionInput>(
+    GET_INSTITUTION,
+    {
+      variables: { _id },
+      skip: isNew,
+      onCompleted: (data) => {
+        const { name, status } = data?.getInstitution || {};
+        reset({ name, status });
+      },
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      onError: (error) => onError([error], "Unable to retrieve Institution."),
+    }
+  );
+
+  const [createInstitution] = useMutation<CreateInstitutionResp, CreateInstitutionInput>(
+    CREATE_INSTITUION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to create a new Institution.");
+        }
+      },
+    }
+  );
+
+  const [updateInstitution] = useMutation<UpdateInstitutionResp, UpdateInstitutionInput>(
+    UPDATE_INSTITUTION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to save changes.");
+        }
+      },
+    }
+  );
+
+  const onError = <T extends { message: string }>(errors: readonly T[], message: string) => {
+    if (!errors?.length) {
+      return;
+    }
+
+    const flatErrors = errors?.flatMap((err) => err.message)?.join(" ");
+    Logger.error(`InstitutionView: ${message} ${flatErrors}`);
+    enqueueSnackbar(flatErrors, { variant: "error" });
+  };
+
+  const handleCreateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await createInstitution({ variables: { name, status } });
+
+    if (errors || !d?.createInstitution?._id) {
+      onError(errors, "Unable to create a new Institution.");
+      return;
+    }
+
+    enqueueSnackbar("Institution added successfully.", { variant: "success" });
+  };
+
+  const handleUpdateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await updateInstitution({ variables: { _id, name, status } });
+
+    if (errors || !d?.updateInstitution?._id) {
+      onError(errors, "Unable to save changes.");
+      return;
+    }
+
+    enqueueSnackbar("Institution updated successfully.", { variant: "success" });
+    reset({ ...d.updateInstitution });
+  };
+
+  const onSubmit = async (data: FormInput) => {
+    setSaving(true);
+
+    const action = isNew ? handleCreateInstitution : handleUpdateInstitution;
+    await action(data);
+
+    setSaving(false);
+    navigate(manageInstitutionsPageUrl);
+  };
+
path: src/content/Institutions/InstitutionView.tsx
user: login: Alejandro-Vega
body: Agreed, added in [8217bc6](https://github.com/CBIIT/crdc-datahub-ui/pull/682/commits/8217bc6881963087582db8dc2bcff979d3c018d4).
created_at: 2025-04-15T14:31:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,322 @@
+import { useState } from "react";
+import { Box, Container, MenuItem, Stack, styled, Typography } from "@mui/material";
+import { LoadingButton } from "@mui/lab";
+import { useNavigate } from "react-router-dom";
+import { Controller, useForm } from "react-hook-form";
+import { useMutation, useQuery } from "@apollo/client";
+import { useSnackbar } from "notistack";
+import bannerSvg from "../../assets/banner/profile_banner.png";
+import institutionIcon from "../../assets/icons/institutions_icon.svg";
+import usePageTitle from "../../hooks/usePageTitle";
+import StyledAsterisk from "../../components/StyledFormComponents/StyledAsterisk";
+import BaseOutlinedInput from "../../components/StyledFormComponents/StyledOutlinedInput";
+import BaseSelect from "../../components/StyledFormComponents/StyledSelect";
+import { useSearchParamsContext } from "../../components/Contexts/SearchParamsContext";
+import {
+  CREATE_INSTITUION,
+  CreateInstitutionInput,
+  CreateInstitutionResp,
+  GET_INSTITUTION,
+  GetInstitutionInput,
+  GetInstitutionResp,
+  UPDATE_INSTITUTION,
+  UpdateInstitutionInput,
+  UpdateInstitutionResp,
+} from "../../graphql";
+import { Logger } from "../../utils";
+
+const StyledBanner = styled("div")({
+  background: `url(${bannerSvg})`,
+  backgroundBlendMode: "luminosity, normal",
+  backgroundSize: "cover",
+  backgroundPosition: "center",
+  width: "100%",
+  height: "153px",
+});
+
+const StyledContainer = styled(Container)({
+  marginBottom: "90px",
+});
+
+const StyledIcon = styled("div")({
+  position: "relative",
+  transform: "translate(-218px, -75px)",
+  "& img": {
+    position: "absolute",
+  },
+  "& img:nth-of-type(1)": {
+    zIndex: 2,
+    filter: "drop-shadow(10px 13px 9px rgba(0, 0, 0, 0.35))",
+  },
+});
+
+const StyledContentStack = styled(Stack)({
+  marginLeft: "2px !important",
+});
+
+const StyledTitleBox = styled(Box)({
+  marginTop: "-86px",
+  marginBottom: "88px",
+  width: "100%",
+});
+
+const StyledPageTitle = styled(Typography)({
+  fontFamily: "Nunito Sans",
+  fontSize: "45px",
+  fontWeight: 800,
+  letterSpacing: "-1.5px",
+  color: "#fff",
+});
+
+const StyledField = styled("div")({
+  marginBottom: "10px",
+  minHeight: "41px",
+  display: "flex",
+  alignItems: "center",
+  justifyContent: "space-between",
+  gap: "40px",
+  fontSize: "18px",
+});
+
+const StyledLabel = styled("span")({
+  color: "#356AAD",
+  fontWeight: "700",
+  minWidth: "127px",
+});
+
+const BaseInputStyling = {
+  width: "363px",
+};
+
+const StyledTextField = styled(BaseOutlinedInput)(BaseInputStyling);
+const StyledSelect = styled(BaseSelect)(BaseInputStyling);
+
+const StyledButtonStack = styled(Stack)({
+  marginTop: "50px",
+});
+
+const StyledButton = styled(LoadingButton)({
+  minWidth: "120px",
+  fontSize: "16px",
+  padding: "10px",
+  lineHeight: "24px",
+});
+
+type FormInput = Pick<Institution, "name" | "status">;
+
 type Props = {
   _id: string;
 };
 
-const InstitutionView = ({ _id }: Props) => <div>InstitutionView</div>;
+const InstitutionView = ({ _id }: Props) => {
+  const isNew = _id && _id === "new";
+  usePageTitle(isNew ? "Add Institution" : `Edit Institution ${_id}`);
+  const navigate = useNavigate();
+  const { enqueueSnackbar } = useSnackbar();
+  const { lastSearchParams } = useSearchParamsContext();
+  const {
+    handleSubmit,
+    register,
+    control,
+    reset,
+    formState: { errors },
+  } = useForm<FormInput>({
+    mode: "onSubmit",
+    reValidateMode: "onSubmit",
+    defaultValues: {
+      name: "",
+      status: "Active",
+    },
+  });
+
+  const [saving, setSaving] = useState<boolean>(false);
+
+  const manageInstitutionsPageUrl = `/institutions${lastSearchParams?.["/institutions"] ?? ""}`;
+
+  const { loading: retrievingInstitution } = useQuery<GetInstitutionResp, GetInstitutionInput>(
+    GET_INSTITUTION,
+    {
+      variables: { _id },
+      skip: isNew,
+      onCompleted: (data) => {
+        const { name, status } = data?.getInstitution || {};
+        reset({ name, status });
+      },
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      onError: (error) => onError([error], "Unable to retrieve Institution."),
+    }
+  );
+
+  const [createInstitution] = useMutation<CreateInstitutionResp, CreateInstitutionInput>(
+    CREATE_INSTITUION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to create a new Institution.");
+        }
+      },
+    }
+  );
+
+  const [updateInstitution] = useMutation<UpdateInstitutionResp, UpdateInstitutionInput>(
+    UPDATE_INSTITUTION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to save changes.");
+        }
+      },
+    }
+  );
+
+  const onError = <T extends { message: string }>(errors: readonly T[], message: string) => {
+    if (!errors?.length) {
+      return;
+    }
+
+    const flatErrors = errors?.flatMap((err) => err.message)?.join(" ");
+    Logger.error(`InstitutionView: ${message} ${flatErrors}`);
+    enqueueSnackbar(flatErrors, { variant: "error" });
+  };
+
+  const handleCreateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await createInstitution({ variables: { name, status } });
+
+    if (errors || !d?.createInstitution?._id) {
+      onError(errors, "Unable to create a new Institution.");
+      return;
+    }
+
+    enqueueSnackbar("Institution added successfully.", { variant: "success" });
+  };
+
+  const handleUpdateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await updateInstitution({ variables: { _id, name, status } });
+
+    if (errors || !d?.updateInstitution?._id) {
+      onError(errors, "Unable to save changes.");
+      return;
+    }
+
+    enqueueSnackbar("Institution updated successfully.", { variant: "success" });
+    reset({ ...d.updateInstitution });
+  };
+
+  const onSubmit = async (data: FormInput) => {
path: src/content/Institutions/InstitutionView.tsx
user: login: Alejandro-Vega
body: Thanks for the suggestion! Added in [3825bf5](https://github.com/CBIIT/crdc-datahub-ui/pull/682/commits/3825bf5ce5160292bf3435f93853195135657237).
created_at: 2025-04-15T14:33:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { TypedDocumentNode } from "@apollo/client";
+import gql from "graphql-tag";
+
+export const mutation: TypedDocumentNode<Input, Response> = gql`
path: src/graphql/createInstitution.ts
user: login: Alejandro-Vega
body: Oops, it shouldn't work both ways. Thanks for catching this. Fixed in [8b9ec95](https://github.com/CBIIT/crdc-datahub-ui/pull/682/commits/8b9ec95f3e3d7d5e5d5dfcba789acb7865124793).
created_at: 2025-04-15T14:36:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import { TypedDocumentNode } from "@apollo/client";
+import gql from "graphql-tag";
+
+export const query: TypedDocumentNode<Input, Response> = gql`
path: src/graphql/getInstitution.ts
user: login: Alejandro-Vega
body: Fixed in [8b9ec95](https://github.com/CBIIT/crdc-datahub-ui/pull/682/commits/8b9ec95f3e3d7d5e5d5dfcba789acb7865124793).
created_at: 2025-04-15T14:36:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+import { TypedDocumentNode } from "@apollo/client";
+import gql from "graphql-tag";
+
+export const mutation: TypedDocumentNode<Input, Response> = gql`
path: src/graphql/updateInstitution.ts
user: login: Alejandro-Vega
body: Fixed in [8b9ec95](https://github.com/CBIIT/crdc-datahub-ui/pull/682/commits/8b9ec95f3e3d7d5e5d5dfcba789acb7865124793).
created_at: 2025-04-15T14:36:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,286 @@
+import React from "react";
+import { render, waitFor, within } from "@testing-library/react";
+import { MemoryRouter, MemoryRouterProps } from "react-router-dom";
+import userEvent from "@testing-library/user-event";
+import { axe } from "jest-axe";
+import { MockedProvider, MockedResponse } from "@apollo/client/testing";
+import InstitutionView from "./InstitutionView";
+import {
+  GET_INSTITUTION,
+  CREATE_INSTITUION,
+  UPDATE_INSTITUTION,
+  GetInstitutionResp,
+  GetInstitutionInput,
+  CreateInstitutionResp,
+  CreateInstitutionInput,
+  UpdateInstitutionResp,
+  UpdateInstitutionInput,
+} from "../../graphql";
+import { SearchParamsProvider } from "../../components/Contexts/SearchParamsContext";
+
+const mockUsePageTitle = jest.fn();
+jest.mock("../../hooks/usePageTitle", () => ({
+  ...jest.requireActual("../../hooks/usePageTitle"),
+  __esModule: true,
+  default: (p) => mockUsePageTitle(p),
+}));
+
+const mockNavigate = jest.fn();
+jest.mock("react-router-dom", () => ({
+  ...jest.requireActual("react-router-dom"),
+  useNavigate: () => mockNavigate,
+}));
+
+const getInstitutionMock: MockedResponse<GetInstitutionResp, GetInstitutionInput> = {
+  request: {
+    query: GET_INSTITUTION,
+    variables: { _id: "institution-1" },
+  },
+  result: {
+    data: {
+      getInstitution: {
+        _id: "inst-1",
+        name: "Test Institution",
+        status: "Active",
+      },
+    },
+  },
+};
+
+const createInstitutionMock: MockedResponse<CreateInstitutionResp, CreateInstitutionInput> = {
+  request: {
+    query: CREATE_INSTITUION,
+    variables: { name: "New Institution", status: "Active" },
+  },
+  result: {
+    data: {
+      createInstitution: {
+        _id: "new-id",
+      },
+    },
+  },
+};
+
+const updateInstitutionMock: MockedResponse<UpdateInstitutionResp, UpdateInstitutionInput> = {
+  request: {
+    query: UPDATE_INSTITUTION,
+    variables: { _id: "institution-1", name: "Updated Institution", status: "Inactive" },
+  },
+  result: {
+    data: {
+      updateInstitution: {
+        _id: "institution-1",
+        name: "Updated Institution",
+        status: "Inactive",
+      },
+    },
+  },
+};
+
+const createInstitutionErrorMock: MockedResponse<CreateInstitutionResp, CreateInstitutionInput> = {
+  request: {
+    query: CREATE_INSTITUION,
+    variables: { name: "New Institution", status: "Active" },
+  },
+  error: new Error("There was a problem while creating a new Institution."),
+};
+
+const updateInstitutionErrorMock: MockedResponse<UpdateInstitutionResp, UpdateInstitutionInput> = {
+  request: {
+    query: UPDATE_INSTITUTION,
+    variables: { _id: "institution-1", name: "Updated Institution", status: "Inactive" },
+  },
+  error: new Error("There was a problem while updating the Institution."),
+};
+
+type ParentProps = {
+  mocks?: MockedResponse[];
+  initialEntries?: MemoryRouterProps["initialEntries"];
+  children: React.ReactNode;
+};
+
+const TestParent: React.FC<ParentProps> = ({ mocks = [], initialEntries = ["/"], children }) => (
+  <MockedProvider mocks={mocks} addTypename={false}>
+    <MemoryRouter initialEntries={initialEntries}>
+      <SearchParamsProvider>{children}</SearchParamsProvider>
+    </MemoryRouter>
+  </MockedProvider>
+);
+
+describe("Accessibility", () => {
+  it("has no accessibility violations", async () => {
+    const { container } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const results = await axe(container);
+    expect(results).toHaveNoViolations();
+  });
+});
+
+describe("InstitutionView Component", () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it("renders without crashing", () => {
+    const { getByTestId } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    expect(getByTestId("institution-form")).toBeInTheDocument();
+    expect(getByTestId("institutionName-input")).toBeInTheDocument();
+    expect(getByTestId("save-button")).toBeInTheDocument();
+    expect(getByTestId("cancel-button")).toBeInTheDocument();
+  });
+
+  it("should set the page title to 'Add Institution' when _id is 'new'", async () => {
+    render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect(mockUsePageTitle).toHaveBeenCalledWith("Add Institution");
+    });
+  });
+
+  it("should set the page title to 'Edit Institution institution-1' when editing", async () => {
+    render(
+      <TestParent mocks={[getInstitutionMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect(mockUsePageTitle).toHaveBeenCalledWith("Edit Institution institution-1");
+    });
+  });
+
+  it("creates a new institution successfully", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[createInstitutionMock]}>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    const saveButton = getByTestId("save-button");
+
+    userEvent.type(nameInput, "New Institution");
+    userEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith("Institution added successfully.", {
+        variant: "success",
+      });
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+    });
+  });
+
+  it("updates an existing institution successfully", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[getInstitutionMock, updateInstitutionMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect((getByTestId("institutionName-input") as HTMLInputElement).value).toBe(
+        "Test Institution"
+      );
+    });
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    userEvent.clear(nameInput);
+    userEvent.type(nameInput, "Updated Institution");
+
+    const statusSelect = within(getByTestId("status-select")).getByRole("button");
+    userEvent.click(statusSelect);
+    const listbox = within(getByTestId("status-select")).getByRole("listbox", { hidden: true });
+    userEvent.click(within(listbox).getByText("Inactive"));
+
+    userEvent.click(getByTestId("save-button"));
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith("Institution updated successfully.", {
+        variant: "success",
+      });
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+    });
+  });
+
+  it("navigates back to manage institutions page when cancel button is clicked", () => {
+    const { getByTestId } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const cancelButton = getByTestId("cancel-button");
+    userEvent.click(cancelButton);
+    expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+  });
+
+  it("handles API errors gracefully when creating a new institution", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[createInstitutionErrorMock]}>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    const saveButton = getByTestId("save-button");
+
+    userEvent.type(nameInput, "New Institution");
+    userEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith(
+        "There was a problem while creating a new Institution.",
+        {
+          variant: "error",
+        }
+      );
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
path: src/content/Institutions/InstitutionView.test.tsx
user: login: Alejandro-Vega
body: Unsure, but added it anyways to be safe. There were other tests that also had it in the waitFor, so I fixed those as well in [223fe55](https://github.com/CBIIT/crdc-datahub-ui/pull/682/commits/223fe55f438c0b15c42e0b6a847ac7330873ed5a).
created_at: 2025-04-15T14:44:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,322 @@
+import { useState } from "react";
+import { Box, Container, MenuItem, Stack, styled, Typography } from "@mui/material";
+import { LoadingButton } from "@mui/lab";
+import { useNavigate } from "react-router-dom";
+import { Controller, useForm } from "react-hook-form";
+import { useMutation, useQuery } from "@apollo/client";
+import { useSnackbar } from "notistack";
+import bannerSvg from "../../assets/banner/profile_banner.png";
+import institutionIcon from "../../assets/icons/institutions_icon.svg";
+import usePageTitle from "../../hooks/usePageTitle";
+import StyledAsterisk from "../../components/StyledFormComponents/StyledAsterisk";
+import BaseOutlinedInput from "../../components/StyledFormComponents/StyledOutlinedInput";
+import BaseSelect from "../../components/StyledFormComponents/StyledSelect";
+import { useSearchParamsContext } from "../../components/Contexts/SearchParamsContext";
+import {
+  CREATE_INSTITUION,
+  CreateInstitutionInput,
+  CreateInstitutionResp,
+  GET_INSTITUTION,
+  GetInstitutionInput,
+  GetInstitutionResp,
+  UPDATE_INSTITUTION,
+  UpdateInstitutionInput,
+  UpdateInstitutionResp,
+} from "../../graphql";
+import { Logger } from "../../utils";
+
+const StyledBanner = styled("div")({
+  background: `url(${bannerSvg})`,
+  backgroundBlendMode: "luminosity, normal",
+  backgroundSize: "cover",
+  backgroundPosition: "center",
+  width: "100%",
+  height: "153px",
+});
+
+const StyledContainer = styled(Container)({
+  marginBottom: "90px",
+});
+
+const StyledIcon = styled("div")({
+  position: "relative",
+  transform: "translate(-218px, -75px)",
+  "& img": {
+    position: "absolute",
+  },
+  "& img:nth-of-type(1)": {
+    zIndex: 2,
+    filter: "drop-shadow(10px 13px 9px rgba(0, 0, 0, 0.35))",
+  },
+});
+
+const StyledContentStack = styled(Stack)({
+  marginLeft: "2px !important",
+});
+
+const StyledTitleBox = styled(Box)({
+  marginTop: "-86px",
+  marginBottom: "88px",
+  width: "100%",
+});
+
+const StyledPageTitle = styled(Typography)({
+  fontFamily: "Nunito Sans",
+  fontSize: "45px",
+  fontWeight: 800,
+  letterSpacing: "-1.5px",
+  color: "#fff",
+});
+
+const StyledField = styled("div")({
+  marginBottom: "10px",
+  minHeight: "41px",
+  display: "flex",
+  alignItems: "center",
+  justifyContent: "space-between",
+  gap: "40px",
+  fontSize: "18px",
+});
+
+const StyledLabel = styled("span")({
+  color: "#356AAD",
+  fontWeight: "700",
+  minWidth: "127px",
+});
+
+const BaseInputStyling = {
+  width: "363px",
+};
+
+const StyledTextField = styled(BaseOutlinedInput)(BaseInputStyling);
+const StyledSelect = styled(BaseSelect)(BaseInputStyling);
+
+const StyledButtonStack = styled(Stack)({
+  marginTop: "50px",
+});
+
+const StyledButton = styled(LoadingButton)({
+  minWidth: "120px",
+  fontSize: "16px",
+  padding: "10px",
+  lineHeight: "24px",
+});
+
+type FormInput = Pick<Institution, "name" | "status">;
+
 type Props = {
   _id: string;
 };
 
-const InstitutionView = ({ _id }: Props) => <div>InstitutionView</div>;
+const InstitutionView = ({ _id }: Props) => {
+  const isNew = _id && _id === "new";
+  usePageTitle(isNew ? "Add Institution" : `Edit Institution ${_id}`);
+  const navigate = useNavigate();
+  const { enqueueSnackbar } = useSnackbar();
+  const { lastSearchParams } = useSearchParamsContext();
+  const {
+    handleSubmit,
+    register,
+    control,
+    reset,
+    formState: { errors },
+  } = useForm<FormInput>({
+    mode: "onSubmit",
+    reValidateMode: "onSubmit",
+    defaultValues: {
+      name: "",
+      status: "Active",
+    },
+  });
+
+  const [saving, setSaving] = useState<boolean>(false);
+
+  const manageInstitutionsPageUrl = `/institutions${lastSearchParams?.["/institutions"] ?? ""}`;
+
+  const { loading: retrievingInstitution } = useQuery<GetInstitutionResp, GetInstitutionInput>(
+    GET_INSTITUTION,
+    {
+      variables: { _id },
+      skip: isNew,
+      onCompleted: (data) => {
+        const { name, status } = data?.getInstitution || {};
+        reset({ name, status });
+      },
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      onError: (error) => onError([error], "Unable to retrieve Institution."),
+    }
+  );
+
+  const [createInstitution] = useMutation<CreateInstitutionResp, CreateInstitutionInput>(
+    CREATE_INSTITUION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to create a new Institution.");
+        }
+      },
+    }
+  );
+
+  const [updateInstitution] = useMutation<UpdateInstitutionResp, UpdateInstitutionInput>(
+    UPDATE_INSTITUTION,
+    {
+      context: { clientName: "backend" },
+      fetchPolicy: "no-cache",
+      errorPolicy: "all",
+      onError: (error) => {
+        if (error.networkError) {
+          onError([error], "Unable to save changes.");
+        }
+      },
+    }
+  );
+
+  const onError = <T extends { message: string }>(errors: readonly T[], message: string) => {
+    if (!errors?.length) {
+      return;
+    }
+
+    const flatErrors = errors?.flatMap((err) => err.message)?.join(" ");
+    Logger.error(`InstitutionView: ${message} ${flatErrors}`);
+    enqueueSnackbar(flatErrors, { variant: "error" });
+  };
+
+  const handleCreateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await createInstitution({ variables: { name, status } });
+
+    if (errors || !d?.createInstitution?._id) {
+      onError(errors, "Unable to create a new Institution.");
+      return;
+    }
+
+    enqueueSnackbar("Institution added successfully.", { variant: "success" });
+  };
+
+  const handleUpdateInstitution = async ({ name, status }: FormInput) => {
+    const { data: d, errors } = await updateInstitution({ variables: { _id, name, status } });
+
+    if (errors || !d?.updateInstitution?._id) {
+      onError(errors, "Unable to save changes.");
+      return;
+    }
+
+    enqueueSnackbar("Institution updated successfully.", { variant: "success" });
+    reset({ ...d.updateInstitution });
+  };
+
+  const onSubmit = async (data: FormInput) => {
+    setSaving(true);
+
+    const action = isNew ? handleCreateInstitution : handleUpdateInstitution;
+    await action(data);
+
+    setSaving(false);
+    navigate(manageInstitutionsPageUrl);
path: src/content/Institutions/InstitutionView.tsx
user: login: Alejandro-Vega
body: Fixed to remain on page when error occurs in [f108e54](https://github.com/CBIIT/crdc-datahub-ui/pull/682/commits/f108e543e5270c514e21095ff51688a6f6509d57).
created_at: 2025-04-15T15:05:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,286 @@
+import React from "react";
+import { render, waitFor, within } from "@testing-library/react";
+import { MemoryRouter, MemoryRouterProps } from "react-router-dom";
+import userEvent from "@testing-library/user-event";
+import { axe } from "jest-axe";
+import { MockedProvider, MockedResponse } from "@apollo/client/testing";
+import InstitutionView from "./InstitutionView";
+import {
+  GET_INSTITUTION,
+  CREATE_INSTITUION,
+  UPDATE_INSTITUTION,
+  GetInstitutionResp,
+  GetInstitutionInput,
+  CreateInstitutionResp,
+  CreateInstitutionInput,
+  UpdateInstitutionResp,
+  UpdateInstitutionInput,
+} from "../../graphql";
+import { SearchParamsProvider } from "../../components/Contexts/SearchParamsContext";
+
+const mockUsePageTitle = jest.fn();
+jest.mock("../../hooks/usePageTitle", () => ({
+  ...jest.requireActual("../../hooks/usePageTitle"),
+  __esModule: true,
+  default: (p) => mockUsePageTitle(p),
+}));
+
+const mockNavigate = jest.fn();
+jest.mock("react-router-dom", () => ({
+  ...jest.requireActual("react-router-dom"),
+  useNavigate: () => mockNavigate,
+}));
+
+const getInstitutionMock: MockedResponse<GetInstitutionResp, GetInstitutionInput> = {
+  request: {
+    query: GET_INSTITUTION,
+    variables: { _id: "institution-1" },
+  },
+  result: {
+    data: {
+      getInstitution: {
+        _id: "inst-1",
+        name: "Test Institution",
+        status: "Active",
+      },
+    },
+  },
+};
+
+const createInstitutionMock: MockedResponse<CreateInstitutionResp, CreateInstitutionInput> = {
+  request: {
+    query: CREATE_INSTITUION,
+    variables: { name: "New Institution", status: "Active" },
+  },
+  result: {
+    data: {
+      createInstitution: {
+        _id: "new-id",
+      },
+    },
+  },
+};
+
+const updateInstitutionMock: MockedResponse<UpdateInstitutionResp, UpdateInstitutionInput> = {
+  request: {
+    query: UPDATE_INSTITUTION,
+    variables: { _id: "institution-1", name: "Updated Institution", status: "Inactive" },
+  },
+  result: {
+    data: {
+      updateInstitution: {
+        _id: "institution-1",
+        name: "Updated Institution",
+        status: "Inactive",
+      },
+    },
+  },
+};
+
+const createInstitutionErrorMock: MockedResponse<CreateInstitutionResp, CreateInstitutionInput> = {
+  request: {
+    query: CREATE_INSTITUION,
+    variables: { name: "New Institution", status: "Active" },
+  },
+  error: new Error("There was a problem while creating a new Institution."),
+};
+
+const updateInstitutionErrorMock: MockedResponse<UpdateInstitutionResp, UpdateInstitutionInput> = {
+  request: {
+    query: UPDATE_INSTITUTION,
+    variables: { _id: "institution-1", name: "Updated Institution", status: "Inactive" },
+  },
+  error: new Error("There was a problem while updating the Institution."),
+};
+
+type ParentProps = {
+  mocks?: MockedResponse[];
+  initialEntries?: MemoryRouterProps["initialEntries"];
+  children: React.ReactNode;
+};
+
+const TestParent: React.FC<ParentProps> = ({ mocks = [], initialEntries = ["/"], children }) => (
+  <MockedProvider mocks={mocks} addTypename={false}>
+    <MemoryRouter initialEntries={initialEntries}>
+      <SearchParamsProvider>{children}</SearchParamsProvider>
+    </MemoryRouter>
+  </MockedProvider>
+);
+
+describe("Accessibility", () => {
+  it("has no accessibility violations", async () => {
+    const { container } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const results = await axe(container);
+    expect(results).toHaveNoViolations();
+  });
+});
+
+describe("InstitutionView Component", () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it("renders without crashing", () => {
+    const { getByTestId } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    expect(getByTestId("institution-form")).toBeInTheDocument();
+    expect(getByTestId("institutionName-input")).toBeInTheDocument();
+    expect(getByTestId("save-button")).toBeInTheDocument();
+    expect(getByTestId("cancel-button")).toBeInTheDocument();
+  });
+
+  it("should set the page title to 'Add Institution' when _id is 'new'", async () => {
+    render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect(mockUsePageTitle).toHaveBeenCalledWith("Add Institution");
+    });
+  });
+
+  it("should set the page title to 'Edit Institution institution-1' when editing", async () => {
+    render(
+      <TestParent mocks={[getInstitutionMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect(mockUsePageTitle).toHaveBeenCalledWith("Edit Institution institution-1");
+    });
+  });
+
+  it("creates a new institution successfully", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[createInstitutionMock]}>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    const saveButton = getByTestId("save-button");
+
+    userEvent.type(nameInput, "New Institution");
+    userEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith("Institution added successfully.", {
+        variant: "success",
+      });
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+    });
+  });
+
+  it("updates an existing institution successfully", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[getInstitutionMock, updateInstitutionMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect((getByTestId("institutionName-input") as HTMLInputElement).value).toBe(
+        "Test Institution"
+      );
+    });
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    userEvent.clear(nameInput);
+    userEvent.type(nameInput, "Updated Institution");
+
+    const statusSelect = within(getByTestId("status-select")).getByRole("button");
+    userEvent.click(statusSelect);
+    const listbox = within(getByTestId("status-select")).getByRole("listbox", { hidden: true });
+    userEvent.click(within(listbox).getByText("Inactive"));
+
+    userEvent.click(getByTestId("save-button"));
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith("Institution updated successfully.", {
+        variant: "success",
+      });
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+    });
+  });
+
+  it("navigates back to manage institutions page when cancel button is clicked", () => {
+    const { getByTestId } = render(
+      <TestParent>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const cancelButton = getByTestId("cancel-button");
+    userEvent.click(cancelButton);
+    expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+  });
+
+  it("handles API errors gracefully when creating a new institution", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[createInstitutionErrorMock]}>
+        <InstitutionView _id="new" />
+      </TestParent>
+    );
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    const saveButton = getByTestId("save-button");
+
+    userEvent.type(nameInput, "New Institution");
+    userEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith(
+        "There was a problem while creating a new Institution.",
+        {
+          variant: "error",
+        }
+      );
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
+    });
+  });
+
+  it("handles API errors gracefully when updating an existing institution", async () => {
+    const { getByTestId } = render(
+      <TestParent mocks={[getInstitutionMock, updateInstitutionErrorMock]}>
+        <InstitutionView _id="institution-1" />
+      </TestParent>
+    );
+
+    await waitFor(() => {
+      expect((getByTestId("institutionName-input") as HTMLInputElement).value).toBe(
+        "Test Institution"
+      );
+    });
+
+    const nameInput = getByTestId("institutionName-input") as HTMLInputElement;
+    userEvent.clear(nameInput);
+    userEvent.type(nameInput, "Updated Institution");
+
+    const statusSelect = within(getByTestId("status-select")).getByRole("button");
+    userEvent.click(statusSelect);
+    const listbox = within(getByTestId("status-select")).getByRole("listbox", { hidden: true });
+    userEvent.click(within(listbox).getByText("Inactive"));
+
+    userEvent.click(getByTestId("save-button"));
+
+    await waitFor(() => {
+      expect(global.mockEnqueue).toHaveBeenCalledWith(
+        "There was a problem while updating the Institution.",
+        { variant: "error" }
+      );
+      expect(mockNavigate).toHaveBeenCalledWith("/institutions");
path: src/content/Institutions/InstitutionView.test.tsx
user: login: Alejandro-Vega
body: Fixed in [223fe55](https://github.com/CBIIT/crdc-datahub-ui/pull/682/commits/223fe55f438c0b15c42e0b6a847ac7330873ed5a) and [f108e54](https://github.com/CBIIT/crdc-datahub-ui/pull/682/commits/f108e543e5270c514e21095ff51688a6f6509d57).
created_at: 2025-04-15T15:06:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -231,6 +225,31 @@ export type {
 export { mutation as CREATE_ORG } from "./createOrganization";
 export type { Input as CreateOrgInput, Response as CreateOrgResp } from "./createOrganization";
 
+// Institutions
+export { query as LIST_INSTITUTIONS } from "./listInstitutions";
+export type {
+  Input as ListInstitutionsInput,
+  Response as ListInstitutionsResp,
+} from "./listInstitutions";
+
+export { query as GET_INSTITUTION } from "./getInstitution";
+export type {
+  Input as GetInstitutionInput,
+  Response as GetInstitutionResp,
+} from "./getInstitution";
+
+export { mutation as CREATE_INSTITUION } from "./createInstitution";
path: src/graphql/index.ts
user: login: Alejandro-Vega
body: Fixed typo in [a810cad](https://github.com/CBIIT/crdc-datahub-ui/pull/682/commits/a810cad3bdfc6a3320dc48024d7b32ab3b5a31fb).
created_at: 2025-04-15T15:11:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: coveralls
created_at: 2025-04-14T17:39:24Z
body: ## Pull Request Test Coverage Report for [Build 14472973986](https://coveralls.io/builds/73237642)


### Details

* **68** of **70**   **(97.14%)**  changed or added relevant lines in **5** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.3%**) to **62.571%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [src/content/Institutions/Controller.tsx](https://coveralls.io/builds/73237642/source?filename=src%2Fcontent%2FInstitutions%2FController.tsx#L30) | 0 | 1 | 0.0%
| [src/content/Institutions/InstitutionView.tsx](https://coveralls.io/builds/73237642/source?filename=src%2Fcontent%2FInstitutions%2FInstitutionView.tsx#L148) | 65 | 66 | 98.48%
<!-- | **Total:** | **68** | **70** | **97.14%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/73237642/badge)](https://coveralls.io/builds/73237642) |
| :-- | --: |
| Change from base [Build 14447518492](https://coveralls.io/builds/73214826): |  0.3% |
| Covered Lines: | 4740 |
| Relevant Lines: | 7181 |

---
##### 💛  - [Coveralls](https://coveralls.io)

author_association: NONE
user: login: Alejandro-Vega
created_at: 2025-04-15T15:12:20Z
body: > All in all, it seems to work well! It looks like I left a lot of comments, but they're mostly duplicates of eachother.
> 
> Backend issues I noticed during testing:
> 
> * Casing is not ignored for name duplicate check (e.g. "NCI" and "nci" would work). I don't think this should be allowed, but reqs don't say
> * Duplicate name error message is not correct
> * Creating as inactive doesn't work (you already noted this)

I will reopen the BE tickets to fix these issues. Thanks for finding them for me.
author_association: CONTRIBUTOR