Skip to content

Commit

Permalink
fix(ui): fix New Submission button display (#794)
Browse files Browse the repository at this point in the history
* fix: use `custom:cms-roles` to determine whether user can submit

* chore: move county logic in `useGetCounties` and delete user context

* chore: mock `useGetCounties` in tests

* fix: prevent non-state users from accessing submission menus

* chore: delete useless test

* fix: add prop to control user access of forms

* chore: remove super user from default user permissions

* fix: change from `/dashboard` to `/`

* chore: fix tests
  • Loading branch information
asharonbaltazar committed Sep 27, 2024
1 parent 921066c commit 1e61932
Show file tree
Hide file tree
Showing 18 changed files with 205 additions and 170 deletions.
6 changes: 0 additions & 6 deletions react-app/src/api/index.test.ts

This file was deleted.

65 changes: 42 additions & 23 deletions react-app/src/api/useGetCounties.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
import { useQuery } from "@tanstack/react-query";
import { useGetUser } from "./useGetUser";
import { getUserStateCodes } from "@/utils";
import { FULL_CENSUS_STATES } from "shared-types";
import { useMemo } from "react";

const fetchPopulationData = async (stateString: string) => {
try {
if (stateString) {
const response = await fetch(
`https://api.census.gov/data/2019/pep/population?get=NAME&for=county:*&in=state:${stateString}`,
);
if (!response.ok) {
throw new Error("Failed to fetch county data");
}

const data = await response.json();
return data.slice(1).map((item) => item[0]);
}
return [];
} catch (error) {
console.error("Error fetching county data:", error);
throw error;
}
};

export const usePopulationData = (stateString: string) => {
const usePopulationData = (stateString: string) => {
return useQuery(
["populationData", stateString],
() => fetchPopulationData(stateString),
() =>
fetch(
`https://api.census.gov/data/2019/pep/population?get=NAME&for=county:*&in=state:${stateString}`,
)
.then((response) => response.json())
.then((population) => population.slice(1).map((item) => item[0])),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
Expand All @@ -32,4 +21,34 @@ export const usePopulationData = (stateString: string) => {
);
};

export default usePopulationData;
export const useGetCounties = (): { label: string; value: string }[] => {
const { data: userData } = useGetUser();

const stateCodes = useMemo(
() => getUserStateCodes(userData?.user),
[userData],
);

const stateNumericCodesString = useMemo(
() =>
stateCodes
.map(
(code) =>
FULL_CENSUS_STATES.find((state) => state.value === code)?.code,
)
.filter((code): code is string => code !== undefined && code !== "00")
.join(","),
[stateCodes],
);

const { data: populationData = [] } = usePopulationData(
stateNumericCodesString,
);

return (
populationData.map((county) => {
const [label] = county.split(",");
return { label, value: county };
}) ?? []
);
};
77 changes: 49 additions & 28 deletions react-app/src/components/ActionForm/ActionForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { ReactElement } from "react";
import { MemoryRouter } from "react-router-dom";
import { render, waitFor } from "@testing-library/react";
import { waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, test, expect, vi } from "vitest";
import { ActionForm } from "./index";
Expand All @@ -9,6 +7,8 @@ import { attachmentArraySchemaOptional } from "shared-types";
import * as userPrompt from "@/components/ConfirmationDialog/userPrompt";
import * as banner from "@/components/Banner/banner";
import * as documentPoller from "@/utils/Poller/documentPoller";
import { renderForm } from "@/utils/test-helpers/renderForm";
import { isCmsReadonlyUser } from "shared-utils";

vi.mock("aws-amplify", async (importOriginal) => {
const actual = await importOriginal();
Expand All @@ -28,17 +28,12 @@ vi.mock("../../utils/Poller/DataPoller", () => {
};
});

const renderWithMemoryRouter = (ActionFormArg: ReactElement) =>
render(ActionFormArg, {
wrapper: ({ children }) => <MemoryRouter>{children}</MemoryRouter>,
});

const PROGRESS_REMINDER =
/If you leave this page, you will lose your progress on this form./;

describe("ActionForm", () => {
test("renders `breadcrumbText`", () => {
const { queryByText } = renderWithMemoryRouter(
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({})}
Expand All @@ -56,7 +51,7 @@ describe("ActionForm", () => {
});

test("renders `title`", () => {
const { queryByText } = renderWithMemoryRouter(
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({})}
Expand All @@ -74,7 +69,7 @@ describe("ActionForm", () => {
});

test("renders `attachments.faqLink`", () => {
const { queryByText } = renderWithMemoryRouter(
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({
Expand All @@ -101,8 +96,34 @@ describe("ActionForm", () => {
);
});

test("doesn't render form if user access is denied", () => {
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({
attachments: z.object({
other: z.object({
label: z.string().default("Other"),
files: attachmentArraySchemaOptional(),
}),
}),
})}
fields={() => null}
documentPollerArgs={{
property: () => "id",
documentChecker: () => true,
}}
attachments={{ faqLink: "hello-world-link" }}
conditionsDeterminingUserAccess={[isCmsReadonlyUser]}
breadcrumbText="Example Breadcrumb"
/>,
);

expect(queryByText("Action Form Title")).not.toBeInTheDocument();
});

test("renders `defaultValues` in appropriate input", () => {
const { queryByDisplayValue } = renderWithMemoryRouter(
const { queryByDisplayValue } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({
Expand All @@ -123,7 +144,7 @@ describe("ActionForm", () => {
});

test("renders `attachments.specialInstructions`", () => {
const { queryByText } = renderWithMemoryRouter(
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({
Expand Down Expand Up @@ -153,7 +174,7 @@ describe("ActionForm", () => {
});

test("renders custom `promptOnLeavingForm` when clicking Cancel", async () => {
const { container } = renderWithMemoryRouter(
const { container } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({})}
Expand Down Expand Up @@ -189,7 +210,7 @@ describe("ActionForm", () => {
});

test("renders custom `promptPreSubmission` when clicking Submit", async () => {
const { container } = renderWithMemoryRouter(
const { container } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({
Expand Down Expand Up @@ -230,7 +251,7 @@ describe("ActionForm", () => {
test("calls `documentPoller` with `documentPollerArgs`", async () => {
const documentCheckerFunc = vi.fn();

const { container } = renderWithMemoryRouter(
const { container } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({
Expand Down Expand Up @@ -264,7 +285,7 @@ describe("ActionForm", () => {
test("calls `banner` with `bannerPostSubmission`", async () => {
const documentCheckerFunc = vi.fn();

const { container } = renderWithMemoryRouter(
const { container } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({
Expand Down Expand Up @@ -302,7 +323,7 @@ describe("ActionForm", () => {
});

test("renders all attachment properties within `attachments`", async () => {
const { queryAllByText } = renderWithMemoryRouter(
const { queryAllByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({
Expand Down Expand Up @@ -339,7 +360,7 @@ describe("ActionForm", () => {
});

test("renders Additional Information if `additionalInformation` is defined in schema", () => {
const { queryByText } = renderWithMemoryRouter(
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({
Expand All @@ -359,7 +380,7 @@ describe("ActionForm", () => {
});

test("doesn't render Additional Information if `additionalInformation` is undefined in schema", () => {
const { queryByText } = renderWithMemoryRouter(
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({})}
Expand All @@ -377,7 +398,7 @@ describe("ActionForm", () => {
});

test("renders Attachments if `attachments` is defined in schema", () => {
const { queryByText } = renderWithMemoryRouter(
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({
Expand All @@ -403,7 +424,7 @@ describe("ActionForm", () => {
});

test("doesn't render Attachments if `attachments` is undefined in schema", () => {
const { queryByText } = renderWithMemoryRouter(
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({})}
Expand All @@ -421,7 +442,7 @@ describe("ActionForm", () => {
});

test("renders ProgressReminder if schema has `attachments` property", () => {
const { queryAllByText } = renderWithMemoryRouter(
const { queryAllByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({
Expand All @@ -446,7 +467,7 @@ describe("ActionForm", () => {
});

test("renders ProgressReminder if `fields` property is defined", () => {
const { queryAllByText } = renderWithMemoryRouter(
const { queryAllByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({})}
Expand All @@ -469,7 +490,7 @@ describe("ActionForm", () => {
});

test("doesn't render ProgressReminder `fields` is undefined and `attachments` isn't defined in schema", () => {
const { queryByText } = renderWithMemoryRouter(
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({})}
Expand All @@ -487,7 +508,7 @@ describe("ActionForm", () => {
});

test("renders default wrapper if `fieldsLayout` is undefined", () => {
const { queryAllByText } = renderWithMemoryRouter(
const { queryAllByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({})}
Expand All @@ -514,7 +535,7 @@ describe("ActionForm", () => {
});

test("renders `fieldsLayout`", () => {
const { queryByText } = renderWithMemoryRouter(
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({})}
Expand All @@ -539,7 +560,7 @@ describe("ActionForm", () => {
});

test("renders `fieldsLayout` with correct `title`", () => {
const { queryByText } = renderWithMemoryRouter(
const { queryByText } = renderForm(
<ActionForm
title="Action Form Title"
schema={z.object({})}
Expand Down
24 changes: 22 additions & 2 deletions react-app/src/components/ActionForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,27 @@ import {
} from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import {
Navigate,
useLocation,
useNavigate,
useParams,
} from "react-router-dom";
import { SlotAdditionalInfo } from "@/features";
import { getFormOrigin } from "@/utils";
import {
CheckDocumentFunction,
documentPoller,
} from "@/utils/Poller/documentPoller";
import { API } from "aws-amplify";
import { Authority } from "shared-types";
import { Authority, CognitoUserAttributes } from "shared-types";
import { ActionFormAttachments } from "./ActionFormAttachments";
import {
getAttachments,
getAdditionalInformation,
} from "./actionForm.utilities";
import { isStateUser } from "shared-utils";
import { useGetUser } from "@/api";

type EnforceSchemaProps<Shape extends z.ZodRawShape> = z.ZodObject<
Shape & {
Expand Down Expand Up @@ -81,6 +88,9 @@ type ActionFormProps<Schema extends SchemaWithEnforcableProps> = {
| ((values: z.TypeOf<Schema>) => string);
documentChecker: CheckDocumentFunction;
};
conditionsDeterminingUserAccess?: ((
user: CognitoUserAttributes | null,
) => boolean)[];
breadcrumbText: string;
};

Expand All @@ -105,11 +115,13 @@ export const ActionForm = <Schema extends SchemaWithEnforcableProps>({
promptPreSubmission,
documentPollerArgs,
attachments,
conditionsDeterminingUserAccess = [isStateUser],
breadcrumbText,
}: ActionFormProps<Schema>) => {
const { id, authority } = useParams<{ id: string; authority: Authority }>();
const { pathname } = useLocation();
const navigate = useNavigate();
const { data: userObj } = useGetUser();

const breadcrumbs = optionCrumbsFromPath(pathname, authority);

Expand Down Expand Up @@ -166,6 +178,14 @@ export const ActionForm = <Schema extends SchemaWithEnforcableProps>({
[attachmentsFromSchema, Fields, form],
);

const doesUserHaveAccessToForm = conditionsDeterminingUserAccess.some(
(condition) => condition(userObj.user),
);

if (doesUserHaveAccessToForm === false) {
return <Navigate to="/" replace />;
}

return (
<SimplePageContainer>
<BreadCrumbs
Expand Down
1 change: 0 additions & 1 deletion react-app/src/components/Context/index.ts

This file was deleted.

Loading

0 comments on commit 1e61932

Please sign in to comment.