From 029f3cb1ada1f498ba2c08c4361b8d6650a9f3d4 Mon Sep 17 00:00:00 2001 From: fzhao99 Date: Wed, 25 Sep 2024 12:30:30 -0400 Subject: [PATCH] align search form and results page with designs (#2609) * seed some styles * pull out alert * refactor search form * refactor and make results view component dumb * make component more readable * new files * componentize sidenav * new folder * [pre-commit.ci] auto fixes from pre-commit hooks * move some files around * imports * [pre-commit.ci] auto fixes from pre-commit hooks * copy * [pre-commit.ci] auto fixes from pre-commit hooks * add a more reasonable sidenav default * [pre-commit.ci] auto fixes from pre-commit hooks * remove log * docs * rename * lint more * [pre-commit.ci] auto fixes from pre-commit hooks * more file moving * new files * third times the charm * try this? * add missing prop * [pre-commit.ci] auto fixes from pre-commit hooks * one more * change padding * [pre-commit.ci] auto fixes from pre-commit hooks * fix one test * [pre-commit.ci] auto fixes from pre-commit hooks * debug in ci * [pre-commit.ci] auto fixes from pre-commit hooks * change to 10 * remove debug log * [pre-commit.ci] auto fixes from pre-commit hooks * change selector * [pre-commit.ci] auto fixes from pre-commit hooks * change role --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../tefca-viewer/e2e/query_workflow.spec.ts | 30 ++-- .../app/query/components/CustomizeQuery.tsx | 13 +- .../MultiplePatientSearchResults.tsx | 7 +- .../src/app/query/components/ResultsView.tsx | 168 +++++++++++++----- .../app/query/components/ResultsViewTable.tsx | 108 ----------- .../src/app/query/components/SideNav.tsx | 164 ----------------- .../query/components/backLink/Backlink.tsx | 23 +++ .../ResultsViewAccordionBody.tsx | 8 +- .../resultsView/ResultsViewSideNav.tsx | 65 +++++++ .../resultsView/ResultsViewTable.tsx | 60 +++++++ .../{ => resultsView}/resultsTable.module.css | 4 - .../components/searchForm/SearchForm.tsx | 117 ++++++------ .../searchForm/searchForm.module.css | 21 +++ .../Accordion.tsx | 0 .../src/app/query/designSystem/SiteAlert.tsx | 49 +++++ .../redirectToast/RedirectToast.tsx} | 8 +- .../redirectToast}/redirectToast.module.css | 0 .../query/designSystem/sideNav/SideNav.tsx | 76 ++++++++ .../designSystem/sideNav/sidenav.module.css | 34 ++++ .../src/app/query/page.module.css | 6 + .../tefca-viewer/src/app/query/page.tsx | 121 +++++++------ .../tefca-viewer/src/app/query/test/page.tsx | 1 + .../tefca-viewer/src/styles/_variables.scss | 3 + .../src/styles/custom-styles.scss | 50 +----- .../tefca-viewer/src/styles/layout.scss | 4 + .../src/styles/uswds-settings.scss | 36 ++-- 26 files changed, 642 insertions(+), 534 deletions(-) delete mode 100644 containers/tefca-viewer/src/app/query/components/ResultsViewTable.tsx delete mode 100644 containers/tefca-viewer/src/app/query/components/SideNav.tsx create mode 100644 containers/tefca-viewer/src/app/query/components/backLink/Backlink.tsx rename containers/tefca-viewer/src/app/query/components/{ => resultsView}/ResultsViewAccordionBody.tsx (86%) create mode 100644 containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewSideNav.tsx create mode 100644 containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewTable.tsx rename containers/tefca-viewer/src/app/query/components/{ => resultsView}/resultsTable.module.css (91%) rename containers/tefca-viewer/src/app/query/{components => designSystem}/Accordion.tsx (100%) create mode 100644 containers/tefca-viewer/src/app/query/designSystem/SiteAlert.tsx rename containers/tefca-viewer/src/app/query/{components/RedirectionToast.tsx => designSystem/redirectToast/RedirectToast.tsx} (93%) rename containers/tefca-viewer/src/app/query/{components => designSystem/redirectToast}/redirectToast.module.css (100%) create mode 100644 containers/tefca-viewer/src/app/query/designSystem/sideNav/SideNav.tsx create mode 100644 containers/tefca-viewer/src/app/query/designSystem/sideNav/sidenav.module.css create mode 100644 containers/tefca-viewer/src/app/query/page.module.css diff --git a/containers/tefca-viewer/e2e/query_workflow.spec.ts b/containers/tefca-viewer/e2e/query_workflow.spec.ts index bb831973dc..3dc1abe6eb 100644 --- a/containers/tefca-viewer/e2e/query_workflow.spec.ts +++ b/containers/tefca-viewer/e2e/query_workflow.spec.ts @@ -62,7 +62,7 @@ test.describe("querying with the TryTEFCA viewer", () => { // Make sure we have a results page with a single patient // Non-interactive 'div' elements in the table should be located by text await expect( - page.getByRole("heading", { name: "Query Results" }), + page.getByRole("heading", { name: "Patient Record" }), ).toBeVisible(); await expect(page.getByText("Patient Name")).toBeVisible(); await expect(page.getByText("WATERMELON SPROUT MCGEE")).toBeVisible(); @@ -76,20 +76,18 @@ test.describe("querying with the TryTEFCA viewer", () => { "Interested in learning more about using the TEFCA Query Connector for your jurisdiction? Send us an email at dibbs@cdc.gov", ); - // Let's get a little schwifty: there are multiple possible resolutions for 'Observations', - // so we can chain things to get the table header to make sure the accordion is open + // Check to see if the accordion button is open await expect( - page - .getByTestId("accordionItem_observations") - .getByRole("heading", { name: "Observations" }), + page.getByRole("button", { name: "Observations", expanded: true }), ).toBeVisible(); + // We can also just directly ask the page to find us filtered table rows await expect(page.locator("tbody").locator("tr")).toHaveCount(5); // Now let's use the return to search to go back to a blank form - await page.getByRole("link", { name: "New patient search" }).click(); + await page.getByRole("button", { name: "New patient search" }).click(); await expect( - page.getByRole("heading", { name: "Search for a Patient" }), + page.getByRole("heading", { name: "Search for a Patient", exact: true }), ).toBeVisible(); }); @@ -116,7 +114,7 @@ test.describe("querying with the TryTEFCA viewer", () => { await expect(page.getByText("There are no patient records")).toBeVisible(); await page.getByRole("link", { name: "Search for a new patient" }).click(); await expect( - page.getByRole("heading", { name: "Search for a Patient" }), + page.getByRole("heading", { name: "Search for a Patient", exact: true }), ).toBeVisible(); }); @@ -138,7 +136,7 @@ test.describe("querying with the TryTEFCA viewer", () => { // Among verification, make sure phone number is right await page.getByRole("button", { name: "Search for patient" }).click(); await expect( - page.getByRole("heading", { name: "Query Results" }), + page.getByRole("heading", { name: "Patient Record" }), ).toBeVisible(); await expect(page.getByText("Patient Name")).toBeVisible(); await expect(page.getByText("Veronica Anne Blackstone")).toBeVisible(); @@ -158,7 +156,7 @@ test.describe("querying with the TryTEFCA viewer", () => { await page.getByRole("button", { name: "Fill fields" }).click(); await page.getByRole("button", { name: "Search for patient" }).click(); await expect( - page.getByRole("heading", { name: "Query Results" }), + page.getByRole("heading", { name: "Patient Record" }), ).toBeVisible(); }); @@ -171,7 +169,7 @@ test.describe("querying with the TryTEFCA viewer", () => { await page.getByLabel("Phone Number").fill(""); await page.getByRole("button", { name: "Search for patient" }).click(); await expect( - page.getByRole("heading", { name: "Query Results" }), + page.getByRole("heading", { name: "Patient Record" }), ).toBeVisible(); }); }); @@ -214,7 +212,7 @@ test.describe("Test the user journey of a 'tester'", () => { // Make sure we have a results page with a single patient await expect( - page.getByRole("heading", { name: "Query Results" }), + page.getByRole("heading", { name: "Patient Record" }), ).toBeVisible(); await expect(page.getByText("Patient Name")).toBeVisible(); await expect(page.getByText("WATERMELON SPROUT MCGEE")).toBeVisible(); @@ -240,7 +238,7 @@ test.describe("Test the user journey of a 'tester'", () => { // Make sure we have a results page with a single patient await expect( - page.getByRole("heading", { name: "Query Results" }), + page.getByRole("heading", { name: "Patient Record" }), ).toBeVisible(); await expect(page.getByText("Patient Name")).toBeVisible(); await expect(page.getByText("WATERMELON SPROUT MCGEE")).toBeVisible(); @@ -277,10 +275,10 @@ test.describe("Test the user journey of a 'tester'", () => { // Make sure we have a results page with a single patient & appropriate back buttons await expect( - page.getByRole("heading", { name: "Query Results" }), + page.getByRole("heading", { name: "Patient Record" }), ).toBeVisible(); await expect( - page.getByRole("link", { name: "New patient search" }), + page.getByRole("button", { name: "New patient search" }), ).toBeVisible(); await page.getByRole("link", { name: "Return to search results" }).click(); diff --git a/containers/tefca-viewer/src/app/query/components/CustomizeQuery.tsx b/containers/tefca-viewer/src/app/query/components/CustomizeQuery.tsx index 4ffdf4f240..c137c5c96f 100644 --- a/containers/tefca-viewer/src/app/query/components/CustomizeQuery.tsx +++ b/containers/tefca-viewer/src/app/query/components/CustomizeQuery.tsx @@ -1,17 +1,18 @@ "use client"; import React, { useState, useEffect } from "react"; -import { Button, Icon } from "@trussworks/react-uswds"; +import { Button } from "@trussworks/react-uswds"; import { ValueSetType, ValueSetItem } from "../../constants"; import { UseCaseQueryResponse } from "@/app/query-service"; import LoadingView from "./LoadingView"; -import { showRedirectConfirmation } from "./RedirectionToast"; +import { showRedirectConfirmation } from "../designSystem/redirectToast/RedirectToast"; import styles from "./customizeQuery/customizeQuery.module.css"; import CustomizeQueryAccordionHeader from "./customizeQuery/CustomizeQueryAccordionHeader"; import CustomizeQueryAccordionBody from "./customizeQuery/CustomizeQueryAccordionBody"; -import Accordion from "./Accordion"; +import Accordion from "../designSystem/Accordion"; import CustomizeQueryNav from "./customizeQuery/CustomizeQueryNav"; import { mapValueSetItemsToValueSetTypes } from "./customizeQuery/customizeQueryUtils"; +import Backlink from "./backLink/Backlink"; interface CustomizeQueryProps { useCaseQueryResponse: UseCaseQueryResponse; @@ -160,11 +161,9 @@ const CustomizeQuery: React.FC = ({ }, [valueSetOptions, activeTab]); return ( -
+
- goBack()} className="back-link"> - Return to patient search - +

diff --git a/containers/tefca-viewer/src/app/query/components/MultiplePatientSearchResults.tsx b/containers/tefca-viewer/src/app/query/components/MultiplePatientSearchResults.tsx index d393853ca8..f3060b1902 100644 --- a/containers/tefca-viewer/src/app/query/components/MultiplePatientSearchResults.tsx +++ b/containers/tefca-viewer/src/app/query/components/MultiplePatientSearchResults.tsx @@ -14,6 +14,7 @@ import { } from "../../query-service"; import ResultsView from "./ResultsView"; import { ValueSetItem } from "@/app/constants"; +import Backlink from "./backLink/Backlink"; /** * The props for the MultiplePatientSearchResults component. @@ -58,10 +59,10 @@ const MultiplePatientSearchResults: React.FC< goBackToMultiplePatients={() => setSingleUseCaseQueryResponse(undefined) } + queryName={originalRequest.use_case} /> ); } - return ( <>
@@ -108,9 +109,7 @@ const MultiplePatientSearchResults: React.FC<

Not seeing what you are looking for?

- goBack()}> - Return to patient search - +
); diff --git a/containers/tefca-viewer/src/app/query/components/ResultsView.tsx b/containers/tefca-viewer/src/app/query/components/ResultsView.tsx index 26f6b5f8f4..1d32401cbd 100644 --- a/containers/tefca-viewer/src/app/query/components/ResultsView.tsx +++ b/containers/tefca-viewer/src/app/query/components/ResultsView.tsx @@ -1,13 +1,29 @@ import { UseCaseQueryResponse } from "../../query-service"; -import SideNav from "./SideNav"; +import ResultsViewSideNav, { + NavSection, +} from "./resultsView/ResultsViewSideNav"; import React, { useEffect } from "react"; -import { Alert, Icon } from "@trussworks/react-uswds"; -import ResultsViewTable from "./ResultsViewTable"; +import ResultsViewTable from "./resultsView/ResultsViewTable"; +import Backlink from "./backLink/Backlink"; +import styles from "../page.module.css"; +import ConditionsTable from "./ConditionsTable"; +import Demographics from "./Demographics"; +import DiagnosticReportTable from "./DiagnosticReportTable"; +import EncounterTable from "./EncounterTable"; +import MedicationRequestTable from "./MedicationRequestTable"; +import ObservationTable from "./ObservationTable"; type ResultsViewProps = { useCaseQueryResponse: UseCaseQueryResponse; goBack: () => void; goBackToMultiplePatients?: () => void; + queryName: string; +}; + +export type ResultsViewAccordionItem = { + title: string; + subtitle?: string; + content?: React.ReactNode; }; /** @@ -15,73 +31,127 @@ type ResultsViewProps = { * @param props - The props for the QueryView component. * @param props.useCaseQueryResponse - The response from the query service. * @param props.goBack - The function to go back to the previous page. - * @param props.goBackToMultiplePatients - The function to go back to the multiple patients selection page. + * @param props.goBackToMultiplePatients - The function to go back to the + * multiple patients selection page. + * @param props.queryName - The name of the saved query to display to the user * @returns The QueryView component. */ const ResultsView: React.FC = ({ useCaseQueryResponse, goBack, goBackToMultiplePatients, + queryName, }) => { useEffect(() => { window.scrollTo(0, 0); }, []); + + const accordionItems = + mapQueryResponseToAccordionDataStructure(useCaseQueryResponse); + + const sideNavContent = accordionItems + .map((item) => { + if (item.content) { + return { title: item.title, subtitle: item?.subtitle }; + } + }) + .filter((i) => Boolean(i)) as NavSection[]; + return ( <> - - Interested in learning more about using the TEFCA Query Connector for - your jurisdiction? Send us an email at{" "} - - dibbs@cdc.gov - - -
- -
-
- +
+

+ Patient Record +

+

+ Query:{" "} + {queryName} +

+
+ +
+
+
-
-
-

- Query Results -

-
- -
-
+
+
); }; export default ResultsView; + +function mapQueryResponseToAccordionDataStructure( + useCaseQueryResponse: UseCaseQueryResponse, +) { + const patient = + useCaseQueryResponse.Patient && useCaseQueryResponse.Patient.length === 1 + ? useCaseQueryResponse.Patient[0] + : null; + const observations = useCaseQueryResponse.Observation + ? useCaseQueryResponse.Observation + : null; + const encounters = useCaseQueryResponse.Encounter + ? useCaseQueryResponse.Encounter + : null; + const conditions = useCaseQueryResponse.Condition + ? useCaseQueryResponse.Condition + : null; + const diagnosticReports = useCaseQueryResponse.DiagnosticReport + ? useCaseQueryResponse.DiagnosticReport + : null; + const medicationRequests = useCaseQueryResponse.MedicationRequest + ? useCaseQueryResponse.MedicationRequest + : null; + + const accordionItems: ResultsViewAccordionItem[] = [ + { + title: "Patient Info", + subtitle: "Demographics", + content: patient ? : null, + }, + { + title: "Observations", + content: observations ? ( + + ) : null, + }, + { + title: "Encounters", + content: encounters ? : null, + }, + { + title: "Conditions", + content: conditions ? : null, + }, + { + title: "Diagnostic Reports", + content: diagnosticReports ? ( + + ) : null, + }, + { + title: "Medication Requests", + content: medicationRequests ? ( + + ) : null, + }, + ]; + return accordionItems; +} diff --git a/containers/tefca-viewer/src/app/query/components/ResultsViewTable.tsx b/containers/tefca-viewer/src/app/query/components/ResultsViewTable.tsx deleted file mode 100644 index 05d23e14d6..0000000000 --- a/containers/tefca-viewer/src/app/query/components/ResultsViewTable.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import Demographics from "./Demographics"; -import ObservationTable from "./ObservationTable"; -import EncounterTable from "./EncounterTable"; -import DiagnosticReportTable from "./DiagnosticReportTable"; -import React from "react"; -import Accordion from "./Accordion"; -import { UseCaseQueryResponse } from "@/app/query-service"; -import ConditionsTable from "./ConditionsTable"; -import MedicationRequestTable from "./MedicationRequestTable"; -import styles from "./resultsTable.module.css"; -import ResultsViewAccordionBody from "./ResultsViewAccordionBody"; - -type ResultsViewTable = { - queryResponse: UseCaseQueryResponse; -}; - -/** - * Returns the Accordion component to render all components of the query response. - * @param props - The props for the AccordionContainer component. - * @param props.queryResponse - The response from the query service. - * @returns The AccordionContainer component. - */ -const ResultsViewTable: React.FC = ({ queryResponse }) => { - const patient = - queryResponse.Patient && queryResponse.Patient.length === 1 - ? queryResponse.Patient[0] - : null; - const observations = queryResponse.Observation - ? queryResponse.Observation - : null; - const encounters = queryResponse.Encounter ? queryResponse.Encounter : null; - const conditions = queryResponse.Condition ? queryResponse.Condition : null; - const diagnosticReports = queryResponse.DiagnosticReport - ? queryResponse.DiagnosticReport - : null; - const medicationRequests = queryResponse.MedicationRequest - ? queryResponse.MedicationRequest - : null; - - const accordionItems = [ - { - title: "Patient Info", - subtitle: "Demographics", - content: patient ? : null, - }, - { - title: "Observations", - content: observations ? ( - - ) : null, - }, - { - title: "Encounters", - content: encounters ? : null, - }, - { - title: "Conditions", - content: conditions ? : null, - }, - { - title: "Diagnostic Reports", - content: diagnosticReports ? ( - - ) : null, - }, - { - title: "Medication Requests", - content: medicationRequests ? ( - - ) : null, - }, - ]; - - return ( -
- {accordionItems.map((item) => { - const titleId = formatIdForAnchorTag(item.title); - return ( - item.content && ( - <> - - } - expanded={true} - id={titleId} - headingLevel={"h3"} - accordionClassName={styles.accordionWrapper} - containerClassName={styles.accordionContainer} - /> - - ) - ); - })} -
- ); -}; - -export default ResultsViewTable; - -function formatIdForAnchorTag(id: string) { - return id.toLocaleLowerCase().replace(" ", "-"); -} diff --git a/containers/tefca-viewer/src/app/query/components/SideNav.tsx b/containers/tefca-viewer/src/app/query/components/SideNav.tsx deleted file mode 100644 index ba2dfa85fd..0000000000 --- a/containers/tefca-viewer/src/app/query/components/SideNav.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { SideNav as UswdsSideNav } from "@trussworks/react-uswds"; - -export class SectionConfig { - title: string; - id: string; - subNavItems?: SectionConfig[]; - - constructor(title: string, subNavItems?: string[] | SectionConfig[]) { - this.title = title; - this.id = title.toLowerCase().replace(/\s+/g, "-"); - - if (subNavItems) { - this.subNavItems = subNavItems.map((item) => { - if (typeof item === "string") { - return new SectionConfig(item); - } else { - return item; - } - }); - } - } -} - -interface HeadingObject { - text: string; - level: string; - priority: number; -} - -const headingLevels = ["h1", "h2", "h3", "h4", "h5", "h6"]; -const headingSelector = - "h2:not(.unavailable-info):not(.side-nav-ignore), h3:not(.unavailable-info):not(.side-nav-ignore), h4:not(.unavailable-info):not(.side-nav-ignore)"; - -function countObjects(array: SectionConfig[]): number { - let count = 0; - - function countRecursively(item: SectionConfig): void { - count++; - if (item.subNavItems) { - item.subNavItems.forEach((subHead) => countRecursively(subHead)); - } - } - - array.forEach((item) => countRecursively(item)); - - return count; -} - -/** - * Creates a SectionConfig list. Nests headings in each section config - * @param headings - The list of headings to sort - * @returns - The sorted list of SectionConfigs - */ -export const sortHeadings = (headings: HeadingObject[]): SectionConfig[] => { - const result: SectionConfig[] = []; - let headingIndex = 0; - while (headingIndex < headings.length) { - const currentHeading = headings[headingIndex]; - const nextHeadings = headings.slice(headingIndex + 1); - if ( - nextHeadings.length > 0 && - nextHeadings[0].priority > currentHeading.priority - ) { - const nestedResult = sortHeadings(nextHeadings); - result.push(new SectionConfig(currentHeading.text, nestedResult)); - const nestedLength = countObjects(nestedResult); - headingIndex += nestedLength + 1; - } else if ( - nextHeadings.length > 0 && - nextHeadings[0].priority == currentHeading.priority - ) { - result.push(new SectionConfig(currentHeading.text)); - headingIndex++; - } else if ( - nextHeadings.length > 0 && - nextHeadings[0].priority < currentHeading.priority - ) { - result.push(new SectionConfig(currentHeading.text)); - headingIndex += headings.length + 1; - } else { - result.push(new SectionConfig(currentHeading.text)); - headingIndex++; - } - } - return result; -}; - -/** - * SideNav component - * @returns - The SideNav component - */ -const SideNav: React.FC = () => { - const [sectionConfigs, setSectionConfigs] = useState([]); - const [activeSection, setActiveSection] = useState(""); - - useEffect(() => { - // Select all heading tags on the page - const headingElements = document.querySelectorAll(headingSelector); - - // Extract the text content from each heading and store it in the state - const headings: HeadingObject[] = Array.from(headingElements).map( - (heading) => { - return { - text: heading.textContent || "", - level: heading.tagName.toLowerCase(), - priority: headingLevels.findIndex( - (level) => heading.tagName.toLowerCase() == level, - ), - }; - }, - ); - let sortedHeadings: SectionConfig[] = sortHeadings(headings); - setSectionConfigs(sortedHeadings); - - let options = { - root: null, - rootMargin: "0px 0px -80% 0px", - threshold: 0.8, - }; - - let observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - let id = - entry.target.id || entry.target.querySelectorAll("span")[0]?.id; - setActiveSection(id); - } - }); - }, options); - headingElements.forEach((element) => observer.observe(element)); - }, []); - - function buildSideNav(sectionConfigs: SectionConfig[]) { - let sideNavItems: React.ReactNode[] = []; - for (let section of sectionConfigs) { - let sideNavItem = ( - - {section.title} - - ); - sideNavItems.push(sideNavItem); - - if (section.subNavItems) { - let subSideNavItems = buildSideNav(section.subNavItems); - sideNavItems.push( - , - ); - } - } - - return sideNavItems; - } - - let sideNavItems = buildSideNav(sectionConfigs); - - return ; -}; - -export default SideNav; diff --git a/containers/tefca-viewer/src/app/query/components/backLink/Backlink.tsx b/containers/tefca-viewer/src/app/query/components/backLink/Backlink.tsx new file mode 100644 index 0000000000..9420c032ff --- /dev/null +++ b/containers/tefca-viewer/src/app/query/components/backLink/Backlink.tsx @@ -0,0 +1,23 @@ +import { Icon } from "@trussworks/react-uswds"; + +type BacklinkProps = { + onClick: () => void; + label: string; +}; + +/** + * + * @param root0 - params + * @param root0.onClick - function to handle a click (likely a goBack function) + * @param root0.label - Link label to display + * @returns A backlink component styled according to Figma + */ +const Backlink: React.FC = ({ onClick, label }) => { + return ( + + {label} + + ); +}; + +export default Backlink; diff --git a/containers/tefca-viewer/src/app/query/components/ResultsViewAccordionBody.tsx b/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewAccordionBody.tsx similarity index 86% rename from containers/tefca-viewer/src/app/query/components/ResultsViewAccordionBody.tsx rename to containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewAccordionBody.tsx index 7b016f109a..639015c558 100644 --- a/containers/tefca-viewer/src/app/query/components/ResultsViewAccordionBody.tsx +++ b/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewAccordionBody.tsx @@ -22,9 +22,11 @@ const ResultsViewAccordionBody: React.FC = ({ }) => { return ( <> -

- {title} -

+ {title && ( +

+ {title} +

+ )}
{content}
); diff --git a/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewSideNav.tsx b/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewSideNav.tsx new file mode 100644 index 0000000000..33956f216a --- /dev/null +++ b/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewSideNav.tsx @@ -0,0 +1,65 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { formatIdForAnchorTag } from "./ResultsViewTable"; +import SideNav, { NavItem } from "../../designSystem/sideNav/SideNav"; + +export type NavSection = { + title: string; + subtitle?: string; +}; + +type ResultsViewSideNavProps = { + items: NavSection[]; +}; +/** + * ResultsViewSideNav component + * @param root0 - params + * @param root0.items - a list of nav items to display in the sidenav + * @returns - The ResultsViewSideNav component + */ +const ResultsViewSideNav: React.FC = ({ items }) => { + const [activeItem, setActiveItem] = useState( + window.location.hash || formatIdForAnchorTag(items[0]?.title), + ); + const hashChangeHandler = useCallback(() => { + setActiveItem(window.location.hash); + }, [window.location.hash]); + useEffect(() => { + window.addEventListener("hashchange", hashChangeHandler); + return () => { + window.removeEventListener("hashchange", hashChangeHandler); + }; + }, []); + + const sideNavItems: NavItem[] = items.flatMap((item) => { + const sectionId = formatIdForAnchorTag(item.title); + if (item.subtitle) { + const subSectionId = formatIdForAnchorTag(item.subtitle); + return [ + { + title: item.title, + activeItem: activeItem.includes(sectionId), + }, + { + title: item.subtitle, + activeItem: activeItem.includes(subSectionId), + isSubNav: true, + }, + ]; + } else { + return { + ...item, + activeItem: activeItem.includes(sectionId), + }; + } + }); + + return ( + + ); +}; + +export default ResultsViewSideNav; diff --git a/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewTable.tsx b/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewTable.tsx new file mode 100644 index 0000000000..5f2943f777 --- /dev/null +++ b/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewTable.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import Accordion from "../../designSystem/Accordion"; +import styles from "./resultsTable.module.css"; +import ResultsViewAccordionBody from "./ResultsViewAccordionBody"; +import { ResultsViewAccordionItem } from "../ResultsView"; + +type ResultsViewTable = { + accordionItems: ResultsViewAccordionItem[]; +}; + +/** + * Returns the ResultsViewTable component to render all components of the query response. + * @param root0 - The props for the AccordionContainer component. + * @param root0.accordionItems - an array of items to render as an accordion + * group of type ResultsViewAccordionItem + * @returns The ResultsViewTable component. + */ +const ResultsViewTable: React.FC = ({ accordionItems }) => { + return ( +
+ {accordionItems.map((item) => { + const titleId = formatIdForAnchorTag(item.title); + return ( + item.content && ( +
+ + } + expanded={true} + id={titleId} + headingLevel={"h3"} + accordionClassName={styles.accordionWrapper} + containerClassName={styles.accordionContainer} + /> +
+ ) + ); + })} +
+ ); +}; + +export default ResultsViewTable; + +/** + * Helper function to format titles (probably title cased with spaces) into + * anchor tag format + * @param title A string that we want to turn + * into anchor tag format + * @returns - A hyphenated id that can be linked as an anchor tag + */ +export function formatIdForAnchorTag(title: string) { + return title.toLocaleLowerCase().replace(" ", "-"); +} diff --git a/containers/tefca-viewer/src/app/query/components/resultsTable.module.css b/containers/tefca-viewer/src/app/query/components/resultsView/resultsTable.module.css similarity index 91% rename from containers/tefca-viewer/src/app/query/components/resultsTable.module.css rename to containers/tefca-viewer/src/app/query/components/resultsView/resultsTable.module.css index dbf7eeb29b..37f2310ab4 100644 --- a/containers/tefca-viewer/src/app/query/components/resultsTable.module.css +++ b/containers/tefca-viewer/src/app/query/components/resultsView/resultsTable.module.css @@ -13,10 +13,6 @@ padding: 1rem 1.5rem; } -.accordionContainer { - padding: 0.5rem; -} - .accordionHeading { font-family: "Source Sans Pro Web", "Helvetica Neue", "Helvetica", "Roboto", "Arial", "sans-serif" !important; diff --git a/containers/tefca-viewer/src/app/query/components/searchForm/SearchForm.tsx b/containers/tefca-viewer/src/app/query/components/searchForm/SearchForm.tsx index a9865e34be..aa1287a5f8 100644 --- a/containers/tefca-viewer/src/app/query/components/searchForm/SearchForm.tsx +++ b/containers/tefca-viewer/src/app/query/components/searchForm/SearchForm.tsx @@ -4,7 +4,6 @@ import { Label, TextInput, Select, - Alert, Button, } from "@trussworks/react-uswds"; import { @@ -142,59 +141,73 @@ const SearchForm: React.FC = ({ return ( <> - - This site is for demo purposes only. Please do not enter PII on this - website. - -
-

Search for a Patient

+ +

+ Search for a Patient +

+

+ Enter patient information below to search for a patient. We will query + the connected network to find matching records.{" "} +

{ -
-