Skip to content

Commit

Permalink
fix(ui): enable super user ID editing (#663)
Browse files Browse the repository at this point in the history
* feat: update `useGetItemCache` to params

* feat: change details to :authority/:id route; update dashboard links

* feat: rework breadcrumbs and navigating to work with `authority` param

* feat: create new `Authority` unions and maps

* revert: revert `routes` change to `/details` route

* feat: preference to use union over enum changes

* fix: update `useOriginPath` to work with new `details/:authority/:id` route

* fix: remove optional parameter type from `getDashboardTabForAuthority`

* fix: update unit test for new `AuthorityAPI` type

* fix: update test `.toEqual` value

* fix: add `encodeURIComponent` for authority table links

* fix: redirect wasn't working due to wrong authority type

* chore: remove unused `locationState` arg

* feat: add `hasSubmissionDate` check to `statusChecks`

* fix: add `recordExists` and `hasSubmissionDate`

* chore: prefer react-router `useParams`

* feat: update `origin` to `details` and `dashboard`

* feat: add typ guard function: `isStringAuthority`

* chore: remove `origin` query from `OptionCard`

* feat: rework origin tracking function

* feat: lift banner/alert functions into `ActionForm`; use simplified origin function

* feat: rework origin logic in `SubmitAndCancelBtnSection`

* chore: remove `originPath` variable/prop

* chore: remove `navigationLocation` variable and prop

* chore: update origin handling in forms

* chore: some function renaming

* fix: mark `getFormOrigin` args optional

* chore: delete unused `useQuery`

* chore: delete `stripQueryString`

* revert: correct document polling checks for waiver forms

* chore: remove `/details` route from tests, no longer static route

* chore: remove `/details` route from tests, no longer static route

* chore: move `getDashboardTabForAuthority` to `switch` logic

* chore: simplify render logic for cell details link

* feat: opt to use `Authority`

* chore: fill in missing types

* chore: update types to `Authority`

* add a log to see record info when it fails

* give mako topic gets new id

---------

Co-authored-by: 13bfrancis <[email protected]>
  • Loading branch information
asharonbaltazar and 13bfrancis authored Jul 26, 2024
1 parent 33a0310 commit 24fe0c8
Show file tree
Hide file tree
Showing 26 changed files with 283 additions and 439 deletions.
6 changes: 4 additions & 2 deletions src/packages/shared-utils/package-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export const PackageCheck = ({
appkParentId,
appkParent,
initialIntakeNeeded,
leadAnalystName
submissionDate,
leadAnalystName,
}: opensearch.main.Document) => {
const planChecks = {
isSpa: checkAuthority(authority, [Authority.MED_SPA, Authority.CHIP_SPA]),
Expand Down Expand Up @@ -72,6 +72,8 @@ export const PackageCheck = ({
* object attributes! **/
hasStatus: (authorizedStatuses: string | string[]) =>
checkStatus(seatoolStatus, authorizedStatuses),
/** If submission date exists */
hasSubmissionDate: submissionDate !== undefined,
};
const subStatusChecks = {
/** Is in any of our pending statuses, sans Pending-RAI **/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,6 @@ export class MakoWriteService {
export type UpdateIdDto = {
topicName: string;
id: string;
newId: string;
action: Action;
} & Record<string, unknown>;
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ export class PackageActionWriteService implements PackageWriteClass {
id,
timestamp,
topicName,
newId,
...data,
});
}
Expand Down
1 change: 1 addition & 0 deletions src/services/data/handlers/sinkMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ const onemac = async (kafkaRecords: KafkaRecord[], topicPartition: string) => {
if (!record.newId) {
console.log(
"Malformed update id record. We're going to skip.",
JSON.stringify(record),
);
break; // we need to add a safeparse so malformed receords fail in a nominal way.
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/ui/src/components/Cards/OptionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const OptionCard = ({
}: MACFieldsetOption) => {
return (
<label>
<Link to={`${linkTo}?origin=options`} relative={"path"}>
<Link to={linkTo} relative={"path"}>
<div
data-testid={"card-inner-wrapper"}
className={`flex items-center justify-between gap-6 px-6 py-4 ${
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
import {
Action,
Authority,
CognitoUserAttributes,
opensearch,
} from "shared-types";
import { Authority, CognitoUserAttributes, opensearch } from "shared-types";
import { getAvailableActions, formatSeatoolDate } from "shared-utils";
import { Link as TypedLink } from "@/components";
import { Link } from "react-router-dom";
import * as POP from "@/components";
import { cn, mapActionLabel } from "@/utils";
import { cn, DASHBOARD_ORIGIN, mapActionLabel } from "@/utils";

export const renderCellDate = (key: keyof opensearch.main.Document) =>
function Cell(data: opensearch.main.Document) {
Expand Down Expand Up @@ -54,7 +49,7 @@ export const renderCellActions = (user: CognitoUserAttributes | null) => {
</POP.PopoverTrigger>
<POP.PopoverContent>
<div className="flex flex-col">
{actions.map((action: Action, idx: any) => (
{actions.map((action, idx) => (
<TypedLink
state={{
from: `${location.pathname}${location.search}`,
Expand All @@ -71,7 +66,7 @@ export const renderCellActions = (user: CognitoUserAttributes | null) => {
"relative flex select-none items-center rounded-sm px-2 py-2 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
)}
query={{
origin: "actionsDashboard",
origin: DASHBOARD_ORIGIN,
}}
>
{mapActionLabel(action)}
Expand Down
203 changes: 114 additions & 89 deletions src/services/ui/src/features/package-actions/ActionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,116 +3,141 @@ import {
Form,
LoadingSpinner,
PreSubmitNotice,
Route,
useAlertContext,
useModalContext,
useNavigate,
} from "@/components";
import { useGetUser } from "@/api/useGetUser";
import { useOriginPath } from "@/utils";
import { getFormOrigin } from "@/utils";
import { FormSetup } from "./lib";
import { submitActionForm } from "@/features/package-actions/lib";
import { useGetItem } from "@/api";
import { useCallback, useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useParams, ActionFormHeaderCard } from "@/components";
import { successCheckSwitch } from "./lib/successCheckSwitch";
import { useLocation } from "react-router-dom";
import { ActionFormHeaderCard } from "@/components";
import { SubmitAndCancelBtnSection } from "../submission/waiver/shared-components";
import { useNavigate } from "react-router-dom";
import { Action, Authority } from "shared-types";

export const ActionForm = ({ setup }: { setup: FormSetup }) => {
const { id, type, authority } = useParams("/action/:authority/:id/:type");
type ActionFormProps = {
setup: FormSetup;
id: string;
authority: Authority;
actionType: Action;
};

export const ActionForm = ({
setup,
id,
authority,
actionType,
}: ActionFormProps) => {
const navigate = useNavigate();
const origin = useOriginPath();
const location = useLocation();
const alert = useAlertContext();
const modal = useModalContext();
const { data: user } = useGetUser();
const { data: item } = useGetItem(id!);
const content = useMemo(
() => (item?._source ? setup.content(item._source) : null),
[item],
);
const form = useForm({
const { data: item } = useGetItem(id);

const form = useForm<Record<string, string>>({
resolver: setup.schema ? zodResolver(setup.schema) : undefined,
mode: "onChange",
defaultValues: { id },
});

if (type === "temporary-extension") {
if (!item || !user) {
return <LoadingSpinner />;
}

if (actionType === "temporary-extension") {
form.setValue("seaActionType", "Extend");
}

// Submission Handler
const handler = form.handleSubmit(async (data) => {
await submitActionForm({
data,
id: data?.id ?? id,
type: type!,
authority: authority!,
user: user!,
alert,
navigate,
originRoute: origin,
statusToCheck: successCheckSwitch(type),
locationState: location.state,
});
const content = setup.content(item._source);

const onSubmit = form.handleSubmit(async (data) => {
try {
await submitActionForm({
data,
id,
actionType,
authority,
user,
});

alert.setBannerStyle("success");
alert.setBannerShow(true);
alert.setContent(content.successBanner);

const originPath = getFormOrigin({
id: data?.newId ?? id,
authority,
});

navigate(originPath);

alert.setBannerDisplayOn(originPath.pathname as Route);
} catch (error) {
alert.setContent({
header: "An unexpected error has occurred:",
body: error instanceof Error ? error.message : String(error),
});
alert.setBannerStyle("destructive");
alert.setBannerDisplayOn(window.location.pathname as Route);
alert.setBannerShow(true);
window.scrollTo(0, 0);
}
});
useEffect(() => {
content?.successBanner && alert.setContent(content.successBanner);
}, [content]);
// Adapted handler for destructive confirmation modal use
const confirmSubmitCallback = useCallback(() => {
modal.setModalOpen(false);
handler();
}, [content]);

if (content === null) {
// Still loading item and no content hydrated
return <LoadingSpinner />;
} else {
return (
<Form {...form}>
{form.formState.isSubmitting && <LoadingSpinner />}
<form onSubmit={handler}>
{setup?.fields &&
setup.fields.map((field, index) => {
// only some forms will need to have the title in a card along with the description
if (index === 0) {
return (
<ActionFormHeaderCard
title={content.title}
hasRequiredField={setup.schema !== null}
isTE={field.key === "te-content-description"}
key="content-description"
>
{field}
</ActionFormHeaderCard>
);
} else {
return field;
}
})}
<ErrorBanner />
{content?.preSubmitNotice && (
<PreSubmitNotice
message={content.preSubmitNotice}
hasProgressLossReminder={setup.schema !== null}
/>
)}
{content?.confirmationModal ? (
<SubmitAndCancelBtnSection
confirmWithdraw={() => {
modal.setContent(content.confirmationModal!);
modal.setOnAccept(() => confirmSubmitCallback);
modal.setModalOpen(true);
}}
enableSubmit={content.enableSubmit}
/>
) : (
<SubmitAndCancelBtnSection enableSubmit={content.enableSubmit} />
)}
</form>
</Form>
);
}

const onConfirmWithdraw = () => {
if (content.confirmationModal) {
modal.setContent(content.confirmationModal);
}

modal.setModalOpen(true);

modal.setOnAccept(() => () => {
modal.setModalOpen(false);
onSubmit();
});
};

return (
<Form {...form}>
{form.formState.isSubmitting && <LoadingSpinner />}
<form onSubmit={onSubmit}>
{setup.fields.map((field, index) => {
// only some forms will need to have the title in a card along with the description
if (index === 0) {
return (
<ActionFormHeaderCard
title={content.title}
hasRequiredField={setup.schema !== null}
isTE={field.key === "te-content-description"}
key="content-description"
>
{field}
</ActionFormHeaderCard>
);
}

return field;
})}
<ErrorBanner />
{content.preSubmitNotice && (
<PreSubmitNotice
message={content.preSubmitNotice}
hasProgressLossReminder={setup.schema !== null}
/>
)}
{content.confirmationModal ? (
<SubmitAndCancelBtnSection
confirmWithdraw={onConfirmWithdraw}
enableSubmit={content.enableSubmit}
/>
) : (
<SubmitAndCancelBtnSection enableSubmit={content.enableSubmit} />
)}
</form>
</Form>
);
};
25 changes: 21 additions & 4 deletions src/services/ui/src/features/package-actions/ActionPage.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
import { BreadCrumbs, FAQFooter, SimplePageContainer } from "@/components";
import { useParams } from "@/components/Routing";
import { getSetupFor } from "@/features/package-actions/lib";
import { ActionForm } from "@/features/package-actions/ActionForm";
import { detailsAndActionsCrumbs } from "@/utils";
import { Navigate, useParams } from "react-router-dom";
import { Action, Authority, AuthorityUnion } from "shared-types";

export const ActionPage = () => {
const { id, type, authority } = useParams("/action/:authority/:id/:type");
const {
id,
type: actionType,
authority,
} = useParams<{ id: string; type: Action; authority: Authority }>();

// TODO: use zod
if (!id || !actionType || !authority) {
return <Navigate to="/dashboard" />;
}

const setup = getSetupFor(actionType, authority as AuthorityUnion);

return (
<SimplePageContainer>
<BreadCrumbs
options={detailsAndActionsCrumbs({
id,
authority,
actionType: type,
actionType,
})}
/>
<ActionForm setup={getSetupFor(type, authority)} />
<ActionForm
setup={setup}
actionType={actionType}
authority={authority}
id={id}
/>
<FAQFooter />
</SimplePageContainer>
);
Expand Down
Loading

0 comments on commit 24fe0c8

Please sign in to comment.