Skip to content

Commit

Permalink
Merge pull request #381 from USEPA/feature/integrate-2023-change-requ…
Browse files Browse the repository at this point in the history
…est-form

Feature/integrate 2023 change request form
  • Loading branch information
courtneymyers authored Feb 10, 2024
2 parents a7389f8 + 7ff6ad5 commit 65e8ffe
Show file tree
Hide file tree
Showing 21 changed files with 1,027 additions and 72 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
FORMIO_2023_FRF_PATH: ${{ secrets.FORMIO_2023_FRF_PATH }}
FORMIO_2023_PRF_PATH: ${{ secrets.FORMIO_2023_PRF_PATH }}
FORMIO_2023_CRF_PATH: ${{ secrets.FORMIO_2023_CRF_PATH }}
FORMIO_2023_CHANGE_PATH: ${{ secrets.FORMIO_2023_CHANGE_PATH }}
FORMIO_BASE_URL: ${{ secrets.FORMIO_BASE_URL }}
FORMIO_PROJECT_NAME: ${{ secrets.FORMIO_PROJECT_NAME }}
FORMIO_API_KEY: ${{ secrets.FORMIO_API_KEY }}
Expand Down Expand Up @@ -142,6 +143,7 @@ jobs:
cf set-env $APP_NAME "FORMIO_2023_FRF_PATH" "$FORMIO_2023_FRF_PATH" > /dev/null
cf set-env $APP_NAME "FORMIO_2023_PRF_PATH" "$FORMIO_2023_PRF_PATH" > /dev/null
cf set-env $APP_NAME "FORMIO_2023_CRF_PATH" "$FORMIO_2023_CRF_PATH" > /dev/null
cf set-env $APP_NAME "FORMIO_2023_CHANGE_PATH" "$FORMIO_2023_CHANGE_PATH" > /dev/null
cf set-env $APP_NAME "FORMIO_BASE_URL" "$FORMIO_BASE_URL" > /dev/null
cf set-env $APP_NAME "FORMIO_PROJECT_NAME" "$FORMIO_PROJECT_NAME" > /dev/null
cf set-env $APP_NAME "FORMIO_API_KEY" "$FORMIO_API_KEY" > /dev/null
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
FORMIO_2023_FRF_PATH: ${{ secrets.FORMIO_2023_FRF_PATH }}
FORMIO_2023_PRF_PATH: ${{ secrets.FORMIO_2023_PRF_PATH }}
FORMIO_2023_CRF_PATH: ${{ secrets.FORMIO_2023_CRF_PATH }}
FORMIO_2023_CHANGE_PATH: ${{ secrets.FORMIO_2023_CHANGE_PATH }}
FORMIO_BASE_URL: ${{ secrets.FORMIO_BASE_URL }}
FORMIO_PROJECT_NAME: ${{ secrets.FORMIO_PROJECT_NAME }}
FORMIO_API_KEY: ${{ secrets.FORMIO_API_KEY }}
Expand Down Expand Up @@ -142,6 +143,7 @@ jobs:
cf set-env $APP_NAME "FORMIO_2023_FRF_PATH" "$FORMIO_2023_FRF_PATH" > /dev/null
cf set-env $APP_NAME "FORMIO_2023_PRF_PATH" "$FORMIO_2023_PRF_PATH" > /dev/null
cf set-env $APP_NAME "FORMIO_2023_CRF_PATH" "$FORMIO_2023_CRF_PATH" > /dev/null
cf set-env $APP_NAME "FORMIO_2023_CHANGE_PATH" "$FORMIO_2023_CHANGE_PATH" > /dev/null
cf set-env $APP_NAME "FORMIO_BASE_URL" "$FORMIO_BASE_URL" > /dev/null
cf set-env $APP_NAME "FORMIO_PROJECT_NAME" "$FORMIO_PROJECT_NAME" > /dev/null
cf set-env $APP_NAME "FORMIO_API_KEY" "$FORMIO_API_KEY" > /dev/null
Expand Down
3 changes: 3 additions & 0 deletions app/client/src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { CRF2022 } from "@/routes/crf2022";
import { FRF2023 } from "@/routes/frf2023";
import { PRF2023 } from "@/routes/prf2023";
// import { CRF2023 } from "@/routes/crf2023";
import { Change2023 } from "@/routes/change2023";
import { useDialogState, useDialogActions } from "@/contexts/dialog";

/** Custom hook to display a site-wide alert banner */
Expand Down Expand Up @@ -254,6 +255,8 @@ export function App() {
<Route path="prf/2023/:id" element={<PRF2023 />} />
{/* <Route path="crf/2023/:id" element={<CRF2023 />} /> */}

<Route path="/change/2023/:id" element={<Change2023 />} />

<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Route>,
Expand Down
319 changes: 319 additions & 0 deletions app/client/src/components/change2023New.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import { Fragment, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { Form } from "@formio/react";
import clsx from "clsx";
import icons from "uswds/img/sprite.svg";
// ---
import { serverUrl, messages } from "@/config";
import {
type FormType,
type FormioChange2023Submission,
getData,
postData,
useContentData,
useChangeRequestsQuery,
} from "@/utilities";
import { Loading } from "@/components/loading";
import { Message } from "@/components/message";
import { MarkdownContent } from "@/components/markdownContent";
import { useNotificationsActions } from "@/contexts/notifications";

type ChangeRequestData = {
formType: FormType;
comboKey: string;
mongoId: string;
rebateId: string | null;
email: string;
title: string;
name: string;
};

type ServerResponse = { url: string; json: object };

/** Custom hook to fetch Formio schema */
function useFormioSchemaQuery() {
const url = `${serverUrl}/api/formio/2023/change`;

const query = useQuery({
queryKey: ["formio/2023/change"],
queryFn: () => getData<ServerResponse>(url),
refetchOnWindowFocus: false,
});

return { query };
}

export function ChangeRequest2023Button(props: {
disabled: boolean;
data: ChangeRequestData;
}) {
const { disabled, data } = props;

const [dialogShown, setDialogShown] = useState(false);

function closeDialog() {
setDialogShown(false);
}

return (
<>
<button
className={clsx(
"tw-border-0 tw-border-b-[1.5px] tw-border-transparent tw-p-0 tw-text-sm tw-leading-tight",
"enabled:tw-cursor-pointer",
"hover:enabled:tw-border-b-slate-800",
"focus:enabled:tw-border-b-slate-800",
)}
type="button"
disabled={disabled}
onClick={(_ev) => {
if (disabled) return;
setDialogShown(true);
}}
>
<span className={clsx("tw-flex tw-items-center")}>
<span className={clsx("tw-mr-1")}>Change</span>
<svg
className="usa-icon"
aria-hidden="true"
focusable="false"
role="img"
>
<use href={`${icons}#launch`} />
</svg>
</span>
</button>

<ChangeRequest2023Dialog
dialogShown={dialogShown}
closeDialog={closeDialog}
data={data}
/>
</>
);
}

function ChangeRequest2023Dialog(props: {
dialogShown: boolean;
closeDialog: () => void;
data: ChangeRequestData;
}) {
const { dialogShown, closeDialog, data } = props;

/*
* NOTE: For some reason select inputs from the Formio form won't receive
* click events if the Dialog.Panel component is used (strangely, they still
* receive keyboard events), so a div is used instead. The downside is we lose
* the triggering of the Dialog component's `onClose` event when a user clicks
* outside the panel.
*/

return (
<Transition.Root show={dialogShown} as={Fragment}>
<Dialog
as="div"
className={clsx("tw-relative tw-z-10")}
open={dialogShown}
onClose={(_value) => closeDialog()}
>
<Transition.Child
as={Fragment}
enter={clsx("tw-duration-300 tw-ease-out")}
enterFrom={clsx("tw-opacity-0")}
enterTo={clsx("tw-opacity-100")}
leave={clsx("tw-duration-200 tw-ease-in")}
leaveFrom={clsx("tw-opacity-100")}
leaveTo={clsx("tw-opacity-0")}
>
<div
className={clsx(
"tw-fixed tw-inset-0 tw-bg-black/70 tw-transition-colors",
)}
/>
</Transition.Child>

<div className={clsx("tw-fixed tw-inset-0 tw-z-10 tw-overflow-y-auto")}>
<div
className={clsx(
"tw-flex tw-min-h-full tw-items-center tw-justify-center tw-p-4",
)}
>
<Transition.Child
as={Fragment}
enter={clsx("tw-duration-300 tw-ease-out")}
enterFrom={clsx("tw-translate-y-0 tw-opacity-0")}
enterTo={clsx("tw-translate-y-0 tw-opacity-100")}
leave={clsx("tw-duration-200 tw-ease-in")}
leaveFrom={clsx("tw-translate-y-0 tw-opacity-100")}
leaveTo={clsx("tw-translate-y-0 tw-opacity-0")}
>
{/* <Dialog.Panel */}
<div
className={clsx(
"tw-relative tw-transform tw-overflow-hidden tw-rounded-lg tw-bg-white tw-p-4 tw-shadow-xl tw-transition-all",
"sm:tw-w-full sm:tw-max-w-7xl sm:tw-p-6",
)}
>
<div className="twpf">
<div
className={clsx(
"tw-absolute tw-right-0 tw-top-0 tw-pr-4 tw-pt-4",
)}
>
<button
className={clsx(
"tw-rounded-md tw-bg-white tw-text-gray-400 tw-transition-none",
"hover:tw-text-gray-700",
"focus:tw-text-gray-700",
)}
type="button"
onClick={(_ev) => closeDialog()}
>
<span className={clsx("tw-sr-only")}>Close</span>
<XMarkIcon
className={clsx("tw-h-6 tw-w-6 tw-transition-none")}
aria-hidden="true"
/>
</button>
</div>
</div>

<div className={clsx("tw-m-auto tw-max-w-6xl tw-p-4")}>
<ChangeRequest2023Form
data={data}
closeDialog={closeDialog}
/>
</div>
</div>
{/* </Dialog.Panel> */}
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}

function ChangeRequest2023Form(props: {
data: ChangeRequestData;
closeDialog: () => void;
}) {
const { data, closeDialog } = props;
const { formType, comboKey, rebateId, mongoId, email, title, name } = data;

const content = useContentData();
const {
displaySuccessNotification,
displayErrorNotification,
dismissNotification,
} = useNotificationsActions();

const changeRequestsQuery = useChangeRequestsQuery("2023");

const { query } = useFormioSchemaQuery();
const formSchema = query.data;

/**
* Stores when the form is being submitted, so it can be referenced in the
* Form component's `onSubmit` event prop to prevent double submits.
*/
const formIsBeingSubmitted = useRef(false);

if (query.isInitialLoading) {
return <Loading />;
}

if (query.isError || !formSchema) {
return <Message type="error" text={messages.formSchemaError} />;
}

return (
<>
{content && <MarkdownContent children={content.newChangeIntro} />}

<div className="csb-form">
<Form
form={formSchema.json}
url={formSchema.url} // NOTE: used for file uploads
submission={{
data: {
_request_form: formType,
_bap_entity_combo_key: comboKey,
_bap_rebate_id: rebateId,
_mongo_id: mongoId,
_user_email: email,
_user_title: title,
_user_name: name,
},
}}
options={{
noAlerts: true,
}}
onSubmit={(onSubmitSubmission: {
data: { [field: string]: unknown };
metadata: { [field: string]: unknown };
state: "submitted";
}) => {
// account for when form is being submitted to prevent double submits
if (formIsBeingSubmitted.current) return;
formIsBeingSubmitted.current = true;

dismissNotification({ id: 0 });

postData<FormioChange2023Submission>(
`${serverUrl}/api/formio/2023/change/`,
onSubmitSubmission,
)
.then((res) => {
displaySuccessNotification({
id: Date.now(),
body: (
<p
className={clsx(
"tw-text-sm tw-font-medium tw-text-gray-900",
)}
>
Change Request <em>{res._id}</em> submitted successfully.
</p>
),
});

closeDialog();
changeRequestsQuery.refetch();
})
.catch((_err) => {
displayErrorNotification({
id: Date.now(),
body: (
<>
<p
className={clsx(
"tw-text-sm tw-font-medium tw-text-gray-900",
)}
>
Error creating Change Request for{" "}
<em>
{formType.toUpperCase()} {rebateId}
</em>
.
</p>
<p
className={clsx("tw-mt-1 tw-text-sm tw-text-gray-500")}
>
Please try again.
</p>
</>
),
});
})
.finally(() => {
formIsBeingSubmitted.current = false;
});
}}
/>
</div>
</>
);
}
10 changes: 8 additions & 2 deletions app/client/src/components/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import clsx from "clsx";
// ---
// NOTE: React JSX doesn't support namespaces, so `uswds/img/loader.svg` copied
// into app's `images/loader.svg` with namespace tags removed
import loader from "@/images/loader.svg";
Expand All @@ -12,12 +14,16 @@ export function Loading() {
);
}

export function LoadingButtonIcon() {
export function LoadingButtonIcon(props: { position: "start" | "end" }) {
const { position } = props;
return (
<img
src={loaderWhite}
alt="Loading..."
className="margin-left-105 margin-y-neg-05 height-2 is-loading"
className={clsx(
"height-2 is-loading margin-y-neg-05",
position === "start" ? "margin-right-105" : "margin-left-105",
)}
/>
);
}
Loading

0 comments on commit 65e8ffe

Please sign in to comment.