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

Add disease specific AOE tab for bulk upload guide #8165

Merged
merged 15 commits into from
Oct 16, 2024
17 changes: 17 additions & 0 deletions frontend/src/app/testResults/uploads/CsvSchemaDocumentation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,20 @@
font-style: normal;
}
}

.csv-section-tablist {
li:first-child {
padding-left: 0;
}

.tab-button {
display: inline-block;
}

.tab-selected,
.tab-button:hover {
border-bottom: 0.25rem solid #005ea2;
padding-bottom: 0.5rem;
mpbrown marked this conversation as resolved.
Show resolved Hide resolved
cursor: pointer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import CsvSchemaDocumentation, {
CsvSchemaItem,
getPageTitle,
} from "./CsvSchemaDocumentation";
import { specificSchemaBuilder } from "./specificSchemaBuilder";
import {
RequiredStatusTag,
specificSchemaBuilder,
} from "./specificSchemaBuilder";

jest.mock("../../TelemetryService", () => ({
...jest.requireActual("../../TelemetryService"),
Expand All @@ -21,8 +24,7 @@ window.scrollTo = jest.fn();
const baseItem: CsvSchemaItem = {
name: "Sample Item",
colHeader: "sample_item",
required: false,
requested: false,
requiredStatusTag: RequiredStatusTag.OPTIONAL,
acceptedValues: [],
description: [],
subHeader: [],
Expand Down Expand Up @@ -50,7 +52,7 @@ describe("CsvSchemaDocumentation tests", () => {
it("renders a required schema item", () => {
const item = {
...baseItem,
required: true,
requiredStatusTag: RequiredStatusTag.REQUIRED,
};
render(<CsvSchemaDocumentationItem item={item} />);
expect(screen.getByText("Required")).toBeInTheDocument();
Expand All @@ -59,22 +61,22 @@ describe("CsvSchemaDocumentation tests", () => {
it("renders a optional schema item", () => {
const item = {
...baseItem,
required: false,
};
render(<CsvSchemaDocumentationItem item={item} />);
expect(screen.getByText("Optional")).toBeInTheDocument();
});

it("renders a requested schema item", () => {
it("renders a required when positive schema item", () => {
const item = {
...baseItem,
requested: true,
requiredStatusTag: RequiredStatusTag.REQUIRED_FOR_POSITIVES,
};
render(<CsvSchemaDocumentationItem item={item} />);
expect(screen.queryByText("Required")).not.toBeInTheDocument();
expect(screen.queryByText("Optional")).not.toBeInTheDocument();
const header = screen.getByTestId("header");
expect(within(header).getByText("Requested")).toBeInTheDocument();
expect(
within(header).getByText("Required for Positives")
).toBeInTheDocument();
});

it("renders a schema item with description", () => {
Expand Down Expand Up @@ -161,6 +163,34 @@ describe("CsvSchemaDocumentation tests", () => {
);
expect(container).toMatchSnapshot();
});
it("has working tabs for navigation", async () => {
render(
<MemoryRouter initialEntries={["/results/upload/submit/guide"]}>
<Routes>
<Route
path={"/results/upload/submit/guide"}
element={
<CsvSchemaDocumentation
schemaBuilder={specificSchemaBuilder}
returnUrl={"/results/upload/submit"}
/>
}
/>
</Routes>
</MemoryRouter>
);
const user = userEvent.setup();

expect(screen.getAllByText("Required for Positives")).toHaveLength(1);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ expecting it to have length 1 because we use the Required for Positives tag in the explanation of how the tags work at the start of the guide


const hivAoeTab = screen.getByRole("tab", {
name: "HIV",
});

await user.click(hivAoeTab);

expect(screen.getAllByText("Required for Positives")).toHaveLength(2);
});
it("logs to App Insights on template download", async () => {
const mockTrackEvent = jest.fn();
(getAppInsights as jest.Mock).mockImplementation(() => {
Expand Down
176 changes: 150 additions & 26 deletions frontend/src/app/testResults/uploads/CsvSchemaDocumentation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";

import { LinkWithQuery } from "../../commonComponents/LinkWithQuery";
Expand All @@ -9,18 +9,17 @@ import ScrollToTopOnMount from "../../commonComponents/ScrollToTopOnMount";
import { getFacilityIdFromUrl } from "../../utils/url";
import { BULK_UPLOAD_SUPPORTED_DISEASES_COPY_TEXT } from "../../../config/constants";

import { CsvSchema } from "./specificSchemaBuilder";
import { CsvSchema, RequiredStatusTag, Section } from "./specificSchemaBuilder";

export type CsvSchemaItem = {
name: string;
colHeader: string;
required: boolean;
requested: boolean;
acceptedValues?: string[];
description?: string[];
subHeader?: string[];
format?: string;
examples?: string[];
requiredStatusTag?: RequiredStatusTag;
};

type CsvSchemaItemProps = {
Expand All @@ -40,19 +39,20 @@ export const CsvSchemaDocumentationItem: React.FC<CsvSchemaItemProps> = ({
data-testid="header"
>
{item.name}
{item.required && (
{item.requiredStatusTag === RequiredStatusTag.REQUIRED && (
<span className="text-normal bg-white border-1px border-secondary font-body-3xs padding-x-1 padding-y-05 text-secondary margin-left-2 text-bottom">
Required
</span>
)}

{!item.required && item.requested && (
<span className="text-normal bg-white border-1px border-base font-body-3xs padding-x-1 padding-y-05 text-base margin-left-2 text-bottom">
Requested
{item.requiredStatusTag ===
RequiredStatusTag.REQUIRED_FOR_POSITIVES && (
<span className="text-normal bg-white border-1px border-secondary font-body-3xs padding-x-1 padding-y-05 text-secondary margin-left-2 text-bottom">
Required for Positives
</span>
)}

{!item.required && !item.requested && (
{item.requiredStatusTag === RequiredStatusTag.OPTIONAL && (
<span className="text-normal bg-white border-1px border-base font-body-3xs padding-x-1 padding-y-05 text-base margin-left-2 text-bottom">
Optional
</span>
Expand Down Expand Up @@ -158,6 +158,65 @@ interface CsvSchemaDocumentationProps {
returnUrl: string;
}

const buildDefaultTabSectionState = (schema: CsvSchema) => {
let tabSectionState: Record<string, string> = {};
schema.fields.forEach((field) =>
field.sections.forEach((section) => {
// only set a default if there isn't a value for this key
if (section.tabs && !tabSectionState[section.slug]) {
tabSectionState[section.slug] = section.tabs[0].title;
}
})
);
return tabSectionState;
};

interface SectionTabButtonProps {
selected: boolean;
parentSection: Section;
tabSection: Section;
setTabSectionState: React.Dispatch<
React.SetStateAction<Record<string, string>>
>;
index: number;
}

const SectionTabButton = ({
selected,
parentSection,
tabSection,
setTabSectionState,
index,
}: SectionTabButtonProps) => {
return (
<div
className={`usa-nav__secondary-item padding-right-1 ${
selected ? "usa-current" : ""
} ${index === 0 ? "padding-left-0" : ""}`}
key={`${parentSection.slug}-${tabSection.slug}-tabpanel-item`}
>
<button
id={`${parentSection.slug}-${tabSection.slug}-tab`}
role="tab"
className={`usa-button--unstyled text-ink text-no-underline tab-button ${
selected ? "tab-selected" : ""
}`}
onClick={() =>
setTabSectionState((prevState) => {
return {
...prevState,
[parentSection.slug]: tabSection.title,
};
})
}
aria-selected={selected}
>
{tabSection.title}
</button>
</div>
);
};

const CsvSchemaDocumentation: React.FC<CsvSchemaDocumentationProps> = ({
schemaBuilder,
returnUrl,
Expand All @@ -173,6 +232,9 @@ const CsvSchemaDocumentation: React.FC<CsvSchemaDocumentationProps> = ({
const appInsights = getAppInsights();
const activeFacilityId = getFacilityIdFromUrl(location);
const schema = schemaBuilder(activeFacilityId);
const [tabSectionState, setTabSectionState] = useState(
buildDefaultTabSectionState(schema)
);

return (
<div className="prime-container card-container csv-guide-container">
Expand Down Expand Up @@ -313,23 +375,44 @@ const CsvSchemaDocumentation: React.FC<CsvSchemaDocumentationProps> = ({
</p>
<h4>Include data for all required fields</h4>
<p>
The data template has three field types: required, requested, and
optional. SimpleReport won’t accept files with missing or incorrect
headers and values in required fields. Requested fields are not
required by HHS, but the data is helpful to jurisdictions. The tags
next to data element names listed below show field type:
</p>
<p>
<span className="text-normal bg-white border-1px border-secondary font-body-1xs padding-x-1 padding-y-05 text-secondary margin-right-1 text-middle">
Required
</span>
<span className="text-normal bg-white border-1px border-base font-body-1xs padding-x-1 padding-y-05 text-base margin-right-1 text-middle">
Requested
</span>
<span className="text-normal bg-white border-1px border-base font-body-1xs padding-x-1 padding-y-05 text-base margin-right-1 text-middle">
Optional
</span>
The data template has three field types: Required, Required for
Positives, and Optional. SimpleReport won’t accept files with
missing or incorrect headers and values in required fields. You'll
find these tags throughout the template to guide you through data
requirements:
</p>
<div className="grid-row margin-bottom-1">
<div className="grid-col-4 text-base">
<span className="text-normal bg-white border-1px border-secondary font-body-1xs padding-x-1 padding-y-05 text-secondary margin-right-1 text-middle">
Required
</span>
</div>
<div className="grid-col-auto">
"Required" fields are legally required by HHS.
</div>
</div>
<div className="grid-row margin-bottom-05 border-base-lighter border-top-1px padding-top-1">
<div className="grid-col-4 text-base">
<span className="text-normal bg-white border-1px border-secondary font-body-1xs padding-x-1 padding-y-05 text-secondary margin-right-1 text-middle">
Required for Positives
</span>
</div>
<div className="grid-col-8">
"Required for Positives" fields are legally required by HHS for
positive test results.
</div>
</div>
<div className="grid-row margin-bottom-05 border-base-lighter border-top-1px padding-top-1">
<div className="grid-col-4 text-base">
<span className="text-normal bg-white border-1px border-base font-body-1xs padding-x-1 padding-y-05 text-base margin-right-1 text-middle">
Optional
</span>
</div>
<div className="grid-col-8">
"Optional" fields are not legally required by HHS, but the data is
helpful to jurisdictions.
</div>
</div>
</section>

<section className="margin-top-5">
Expand Down Expand Up @@ -365,14 +448,55 @@ const CsvSchemaDocumentation: React.FC<CsvSchemaDocumentationProps> = ({
className="margin-bottom-5"
>
{field.sections?.map((section) => {
let sectionToRender = section;
let tabHeading;
if (section.tabs && section.tabs.length > 0) {
let tabIds = "";
section.tabs.forEach((tabSection) => {
tabIds = tabIds + `${section.slug}-${tabSection.slug}-tab `;
});
tabHeading = (
<nav
className="prime-secondary-nav"
aria-label={`Tab navigation for ${section.title}`}
>
<div
role="tablist"
aria-owns={tabIds}
className="usa-nav__secondary-links prime-nav csv-section-tablist"
>
{section.tabs.map((tabSection, index) => {
return (
<SectionTabButton
selected={
tabSectionState[section.slug] ===
tabSection.title
}
parentSection={section}
tabSection={tabSection}
setTabSectionState={setTabSectionState}
index={index}
/>
);
})}
</div>
</nav>
);
sectionToRender = section.tabs.filter(
(tabSection) =>
tabSectionState[section.slug] === tabSection.title
)[0];
}
return (
<div
key={`sectionTiles-section-${section.title}`}
className="margin-top-9"
>
<h4 id={`${section.slug}`}>{section.title}</h4>

{section.items?.map((item) => {
{tabHeading}

{sectionToRender.items?.map((item) => {
return (
<CsvSchemaDocumentationItem
item={item}
Expand Down
Loading
Loading