Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experience/16070/message bank page #16792

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions frontend-react/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SenderType } from "./utils/DataDashboardUtils";
import { lazyRouteMarkdown } from "./utils/LazyRouteMarkdown";
import { PERMISSIONS } from "./utils/UsefulTypes";
jpandersen87 marked this conversation as resolved.
Show resolved Hide resolved

const ReportTestingPage = lazy(() => import("./components/Admin/MessageTesting/MessageTesting"));
/* Content Pages */
const Home = lazy(lazyRouteMarkdown(() => import("./content/home/index.mdx")));
const About = lazy(lazyRouteMarkdown(() => import("./content/about/index.mdx")));
Expand Down Expand Up @@ -437,6 +438,10 @@ export const appRoutes: RouteObject[] = [
path: "orgreceiversettings/org/:orgname/receiver/:receivername/action/:action",
element: <EditReceiverSettingsPage />,
},
{
path: "orgreceiversettings/org/:orgname/receiver/:receivername/action/:action/message-testing",
element: <ReportTestingPage />,
},
{
path: "orgsendersettings/org/:orgname/sender/:sendername/action/:action",
element: <EditSenderSettingsPage />,
Expand Down
178 changes: 42 additions & 136 deletions frontend-react/src/components/Admin/EditReceiverSettings.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Button, Textarea } from "@trussworks/react-uswds";
import { ChangeEvent, Dispatch, SetStateAction, useState } from "react";
import { RSMessage } from "../../../config/endpoints/settings";

export const CustomMessage = ({
customMessageNumber,
currentTestMessages,
setCustomMessageNumber,
setCurrentTestMessages,
setOpenCustomMessage,
}: {
customMessageNumber: number;
currentTestMessages: { fileName: string; reportBody: string }[];
setCustomMessageNumber: (value: number) => void;
setCurrentTestMessages: Dispatch<SetStateAction<RSMessage[] | null>>;
setOpenCustomMessage: (value: boolean) => void;
}) => {
const [text, setText] = useState("");
const handleTextareaChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setText(event.target.value);
};
const handleAddCustomMessage = () => {
const dateCreated = new Date();
setCurrentTestMessages([
...currentTestMessages,
{
dateCreated: dateCreated.toString(),
fileName: `Custom message ${customMessageNumber}`,
reportBody: text,
},
]);
setCustomMessageNumber(customMessageNumber + 1);
setText("");
setOpenCustomMessage(false);
};

return (
<div className="width-full">
<p className="text-bold">Enter custom message</p>
<p>Custom messages do not save to the bank after you log out.</p>
<Textarea
value={text}
onChange={handleTextareaChange}
id="custom-message-text"
name="custom-message-text"
className="width-full maxw-full margin-bottom-205"
/>
<div className="width-full text-right">
<Button
type="button"
outline
onClick={() => {
setOpenCustomMessage(false);
}}
>
Cancel
</Button>
<Button type="button" onClick={handleAddCustomMessage} disabled={text.length === 0}>
Add
</Button>
</div>
</div>
);
};
114 changes: 114 additions & 0 deletions frontend-react/src/components/Admin/MessageTesting/MessageTesting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Button, GridContainer } from "@trussworks/react-uswds";
import { ChangeEvent, useState } from "react";
import { Helmet } from "react-helmet-async";
import { useParams } from "react-router";
import { CustomMessage } from "./CustomMessage";
import { RadioField } from "./RadioField";
import useTestMessages from "../../../hooks/api/messages/UseTestMessages";
import { FeatureName } from "../../../utils/FeatureName";
import AdminFetchAlert from "../../alerts/AdminFetchAlert";
import Crumbs, { CrumbsProps } from "../../Crumbs";
import Spinner from "../../Spinner";
import Title from "../../Title";
import { AdminFormWrapper } from "../AdminFormWrapper";
import { EditReceiverSettingsParams } from "../EditReceiverSettings";

function ReportTesting() {
const { orgname, receivername } = useParams<EditReceiverSettingsParams>();
const { testMessages, isDisabled, isLoading } = useTestMessages();
const [selectedOption, setSelectedOption] = useState<string | null>(null);
const [currentTestMessages, setCurrentTestMessages] = useState(testMessages);
const [openCustomMessage, setOpenCustomMessage] = useState(false);
const [customMessageNumber, setCustomMessageNumber] = useState(1);
const crumbProps: CrumbsProps = {
crumbList: [
{
label: FeatureName.RECEIVER_SETTINGS,
path: `/admin/orgreceiversettings/org/${orgname}/receiver/${receivername}/action/edit`,
},
{ label: FeatureName.MESSAGE_TESTING },
],
};

if (isDisabled) {
return <AdminFetchAlert />;
}
if (isLoading || !currentTestMessages) return <Spinner />;

const handleSelect = (event: ChangeEvent<HTMLInputElement>) => {
setSelectedOption(event.target.value);
};

const handleAddCustomMessage = () => {
setSelectedOption(null);
setOpenCustomMessage(true);
};

return (
<>
<Helmet>
<title>Message testing - ReportStream</title>
</Helmet>
<GridContainer>
<Crumbs {...crumbProps}></Crumbs>
</GridContainer>
<AdminFormWrapper
header={
<>
<Title title={"Message testing"} />
<h2 className="margin-bottom-0">
<span className="text-normal font-body-md text-base margin-bottom-0">
Org name: {orgname}
<br />
Receiver name: {receivername}
</span>
</h2>
</>
}
>
<GridContainer>
<p>
Test a message from the message bank or by entering a custom message. You can view test results
in this window while you are logged in. To save for later reference, you can open messages, test
results and output messages in separate tabs.
</p>
<hr />
<p className="font-sans-xl text-bold">Test message bank</p>
<form>
<fieldset className="usa-fieldset bg-base-lightest padding-3">
{currentTestMessages?.map((item, index) => (
<RadioField
key={index}
index={index}
title={item.fileName}
body={item.reportBody}
handleSelect={handleSelect}
selectedOption={selectedOption}
/>
))}
{openCustomMessage && (
<CustomMessage
customMessageNumber={customMessageNumber}
currentTestMessages={currentTestMessages}
setCustomMessageNumber={setCustomMessageNumber}
setCurrentTestMessages={setCurrentTestMessages}
setOpenCustomMessage={setOpenCustomMessage}
/>
)}
</fieldset>
<div className="padding-top-4">
<Button type="button" outline onClick={handleAddCustomMessage}>
Test custom message
</Button>
<Button disabled={!selectedOption} type="button">
Run test
</Button>
</div>
</form>
</GridContainer>
</AdminFormWrapper>
</>
);
}

export default ReportTesting;
57 changes: 57 additions & 0 deletions frontend-react/src/components/Admin/MessageTesting/RadioField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Button, Icon, Radio } from "@trussworks/react-uswds";
import { ChangeEvent } from "react";

export const RadioField = ({
title,
body,
index,
handleSelect,
selectedOption,
}: {
title: string;
body: string;
index: number;
handleSelect: (event: ChangeEvent<HTMLInputElement>) => void;
selectedOption: string | null;
}) => {
const openTextInNewTab = () => {
let formattedContent = body;

// Check if the content is JSON and format it
try {
formattedContent = JSON.stringify(JSON.parse(body), null, 2);
} catch {
formattedContent = body;
}

const blob = new Blob([formattedContent], { type: "text/plain" });

const url = URL.createObjectURL(blob);

window.open(url, "_blank");

// Revoke the URL to free up memory
URL.revokeObjectURL(url);
};

return (
<Radio
id={`message-${index}`}
name="message-test-form"
value={body}
onChange={handleSelect}
checked={selectedOption === body}
className="usa-radio bg-base-lightest padding-2 border-bottom-1px border-gray-30"
label={
<>
{" "}
{title}{" "}
<Button type="button" unstyled onClick={openTextInNewTab}>
View message
<Icon.Visibility className="text-tbottom margin-left-05" aria-label="View message" />
</Button>
</>
}
/>
);
};
18 changes: 18 additions & 0 deletions frontend-react/src/config/endpoints/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export enum ServicesUrls {
PUBLIC_KEYS = "/settings/organizations/:orgName/public-keys",
}

export enum ReportsUrls {
TESTING = "/reports/testing",
}

export interface RSSettings {
version: number;
createdAt: string;
Expand All @@ -22,6 +26,12 @@ export interface RSService extends RSSettings {
customerStatus?: string;
}

export interface RSMessage {
dateCreated?: string;
fileName: string;
reportBody: string;
}

export interface RSOrganizationSettings extends RSSettings {
description: string;
filters: string[];
Expand Down Expand Up @@ -106,3 +116,11 @@ export const servicesEndpoints: RSApiEndpoints = {
queryKey: "createPublicKey",
}),
};

export const reportsEndpoints: RSApiEndpoints = {
testing: new RSEndpoint({
path: ReportsUrls.TESTING,
method: HTTPMethods.GET,
queryKey: "reportsTesting",
}),
};
48 changes: 48 additions & 0 deletions frontend-react/src/hooks/api/messages/UseTestMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { useCallback } from "react";
import { reportsEndpoints, RSMessage } from "../../../config/endpoints/settings";
import useSessionContext from "../../../contexts/Session/useSessionContext";
import { Organizations } from "../../UseAdminSafeOrganizationName/UseAdminSafeOrganizationName";

const { testing } = reportsEndpoints;

/**
* Custom hook to fetch and manage "Test Messages" data for the current session.
*
* @description
* This hook fetches "Test Messages" from the backend. While the UI and design refer to this feature as
* "Test Messages," the corresponding API endpoint is `/api/reports/testing`, which uses the "Reports" nomenclature.
* This discrepancy exists between the backend naming convention ("Reports") and the frontend display ("Messages").
*
* @returns {object} The hook returns the following:
* - `testMessages` (`RSMessage[] | undefined`): The fetched array of test messages.
* - `isDisabled` (`boolean`): Indicates whether the feature is disabled for the current user.
* - Other properties from `useSuspenseQuery` (e.g., `isLoading`, `isError`, `error`).
*/

const useTestMessages = () => {
const { activeMembership, authorizedFetch } = useSessionContext();
const parsedName = activeMembership?.parsedName;
const isAdmin = Boolean(parsedName) && parsedName === Organizations.PRIMEADMINS;

const memoizedDataFetch = useCallback(() => {
if (isAdmin) {
return authorizedFetch<RSMessage[]>({}, testing);
}
return null;
}, [isAdmin, authorizedFetch]);
const useSuspenseQueryResult = useSuspenseQuery({
queryKey: [testing.queryKey, activeMembership],
queryFn: memoizedDataFetch,
});

const { data } = useSuspenseQueryResult;

return {
...useSuspenseQueryResult,
testMessages: data,
isDisabled: !isAdmin,
};
};

export default useTestMessages;
12 changes: 7 additions & 5 deletions frontend-react/src/utils/FeatureName.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export enum FeatureName {
ADMIN = "Admin",
DAILY_DATA = "Daily Data",
DATA_DASHBOARD = "Data Dashboard",
FACILITIES_PROVIDERS = "All facilities & providers",
MESSAGE_TESTING = "Message testing",
PUBLIC_KEY = "Public Key",
RECEIVER_SETTINGS = "Receiver settings",
REPORT_DETAILS = "Report Details",
SUBMISSIONS = "Submissions",
SUPPORT = "Support",
ADMIN = "Admin",
UPLOAD = "Upload",
FACILITIES_PROVIDERS = "All facilities & providers",
DATA_DASHBOARD = "Data Dashboard",
REPORT_DETAILS = "Report Details",
PUBLIC_KEY = "Public Key",
}
Loading