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

Filter by conditions in eCR Library (frontend) #2981

Merged
merged 43 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
971dbad
First pass, filter conditions functionality
angelathe Nov 26, 2024
8f84c72
add select/deselect all functionality
angelathe Nov 26, 2024
1495841
update some styling, maintain checkbox state when toggling filter button
angelathe Nov 27, 2024
757a9a2
styling updates, wip
angelathe Dec 2, 2024
8126024
checkbox color, add icon, add uswds sprite.svg to assets
angelathe Dec 2, 2024
504c78a
adjust padding to fix checkbox focus ring cut off
angelathe Dec 2, 2024
1412872
Merge branch 'main' into angela/2751-condition-frontend
angelathe Dec 2, 2024
276a521
fix icon not displaying by adding static file route
angelathe Dec 3, 2024
7da8e1a
fix unintentional scrolling bug
angelathe Dec 3, 2024
138c46d
update filter row top border
angelathe Dec 3, 2024
fd750f7
wip, add comments, decompose conditions filter to separate const
angelathe Dec 3, 2024
de1625c
fix scrolling bug by adding position-relative
angelathe Dec 3, 2024
7b4703a
add snapshot and unit tests
angelathe Dec 3, 2024
8e32d2e
Merge branch 'main' into angela/2751-condition-frontend
angelathe Dec 3, 2024
ff72d75
add JSDocs
angelathe Dec 3, 2024
14c5330
remove css classes and use utilities instead
angelathe Dec 3, 2024
98f6cfd
Merge branch 'main' into angela/2751-condition-frontend
angelathe Dec 4, 2024
88afe13
Merge branch 'angela/2751-condition-frontend' of https://github.com/C…
angelathe Dec 4, 2024
48a403a
update snapshot test
angelathe Dec 4, 2024
d11e9f9
update select all/deselect all functionality s.t. default is all cond…
angelathe Dec 4, 2024
21fe38b
update so that filters reset if clicking off filter before clicking t…
angelathe Dec 6, 2024
661a138
update basepath so it works in prod
angelathe Dec 6, 2024
7f3a51d
update tests
angelathe Dec 6, 2024
d0172cb
update styles in diff button states, update icon size, make capitaliz…
angelathe Dec 6, 2024
ec2c4e3
Remove log
angelathe Dec 6, 2024
c207e4e
use as form/fieldset, update sync state bug, update tests
angelathe Dec 9, 2024
5f72fc8
remove manual checkboxing for select all, lets react handle the render
angelathe Dec 9, 2024
37522e2
rework state management, update tests
angelathe Dec 9, 2024
c0cd22f
Merge branch 'angela/2751-condition-frontend' of https://github.com/C…
angelathe Dec 9, 2024
75432a1
code review changes, minor
angelathe Dec 10, 2024
aa41879
query should persist over a reload
angelathe Dec 10, 2024
5b7f370
update backend so default (all conditions) would leave out condition …
angelathe Dec 11, 2024
071294d
use import for icon
angelathe Dec 11, 2024
08541c1
Merge branch 'main' into angela/2751-condition-frontend
angelathe Dec 11, 2024
f0ab109
Update base_path env var name
angelathe Dec 11, 2024
75ef9d1
update snapshot test
angelathe Dec 11, 2024
9f10fa4
Merge branch 'angela/2751-condition-frontend' of https://github.com/C…
angelathe Dec 11, 2024
b27ad09
Merge branch 'main' into angela/2751-condition-frontend
angelathe Dec 11, 2024
07ef590
re-use resetFilterConditions
angelathe Dec 11, 2024
82b41e0
one more nit
angelathe Dec 11, 2024
9551a75
update ecr library height to accommodate fiter bar
angelathe Dec 11, 2024
3312b57
Merge branch 'main' into angela/2751-condition-frontend
angelathe Dec 12, 2024
3f9637a
update env var name for base path
angelathe Dec 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions containers/ecr-viewer/public/assets/img/sprite.svg
angelathe marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
253 changes: 253 additions & 0 deletions containers/ecr-viewer/src/app/components/Filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
"use client";

import React, { useCallback, useEffect, useState } from "react";
import { Button } from "@trussworks/react-uswds";
import { useRouter, usePathname, useSearchParams } from "next/navigation";

/**
* Functional component that renders Filters section in eCR Library.
* Includes Filter component for reportable conditions.
* @returns The rendered Filters component.
*/
export const Filters = () => {
return (
<div>
<div className="border-top border-base-lighter"></div>
<div className="margin-x-3 margin-y-105 display-flex flex-align-center gap-105">
<span className="line-height-sans-6">FILTERS:</span>
<FilterReportableConditions />
</div>
</div>
);
};

/**
* Functional component for filtering eCRs in the Library based on reportable conditions.
* @returns The rendered FilterReportableConditions component.
* - Fetches conditions from the `/api/conditions` endpoint.
* - Users can select specific conditions or select all conditions.
* - Updates the browser's query string when the filter is applied.
*/
const FilterReportableConditions = () => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const [isFilterBoxOpen, setIsFilterBoxOpen] = useState(false);
const [conditions, setConditions] = useState<string[]>([]);
const [filterConditions, setFilterConditions] = useState<{
[key: string]: boolean;
}>({});

// Fetch list of conditions
useEffect(() => {
const fetchConditions = async () => {
try {
// Fetch condition list and set default state to filtering on all conditions
const response = await fetch("/api/conditions");
if (!response.ok) {
throw new Error("Failed to fetch conditions");
}
const allConditions = await response.json();
setConditions(allConditions);

const initialFilterConditions = allConditions.reduce(
(dict: { [key: string]: boolean }, condition: string) => {
dict[condition] = true;
return dict;
},
{} as { [key: string]: boolean },
angelathe marked this conversation as resolved.
Show resolved Hide resolved
);
setFilterConditions(initialFilterConditions);
updateFilterConditionsQuery(initialFilterConditions);
angelathe marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
console.error("Error fetching conditions:", error);
angelathe marked this conversation as resolved.
Show resolved Hide resolved
}
};

fetchConditions();
}, []);

// Toggle filter button opening selection box
const toggleFilterBox = () => {
setIsFilterBoxOpen(!isFilterBoxOpen);
};

// Build list of conditions to filter on
const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value, checked } = event.target;
setFilterConditions((prev) => {
if (checked) {
return { ...prev, [value]: true };
} else {
const { [value]: _, ...rest } = prev; // destructuring
return rest;
}
});
};

// Check/Uncheck all boxes based on Select all checkbox
const handleSelectAll = (event: React.ChangeEvent<HTMLInputElement>) => {
const { checked } = event.target;

// Loop through each condition checkbox and set the checked value
const checkboxes = document.querySelectorAll(
'input[id^="condition-"]',
) as NodeListOf<HTMLInputElement>;
checkboxes.forEach((checkbox) => {
(checkbox as HTMLInputElement).checked = checked;
});
angelathe marked this conversation as resolved.
Show resolved Hide resolved

if (checked) {
const updatedConditions = conditions.reduce(
(dict, condition) => {
dict[condition] = true;
return dict;
},
{} as { [key: string]: boolean },
);
setFilterConditions(updatedConditions);
} else {
setFilterConditions({});
}
};

const isAllSelected =
Object.keys(filterConditions).length === conditions.length;

const createQueryString = useCallback(
(name: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
params.set("page", "1");
params.set(name, value);
return params.toString();
},
[searchParams],
);

const updateFilterConditionsQuery = (filterConditions: {
[key: string]: boolean;
}) => {
const filterConditionsSearch = Object.keys(filterConditions).join("|");
if (searchParams.get("condition") !== filterConditionsSearch) {
angelathe marked this conversation as resolved.
Show resolved Hide resolved
router.push(
pathname + "?" + createQueryString("condition", filterConditionsSearch),
);
}
};

return (
<div>
<div className="position-relative display-flex flex-column">
<Button
className="padding-1 usa-button--outline"
id="button-filter-conditions"
aria-label="Filter by reportable condition"
aria-haspopup="listbox"
aria-expanded={isFilterBoxOpen}
onClick={toggleFilterBox}
type="button"
>
<span className="width-205 usa-icon">
<svg
className="usa-icon"
aria-hidden="true"
focusable="false"
role="img"
>
<use xlinkHref={`/assets/img/sprite.svg#coronavirus`}></use>
</svg>
</span>
Reportable condition
angelathe marked this conversation as resolved.
Show resolved Hide resolved
<span
className="usa-tag padding-05 bg-base-darker radius-md"
data-testid="filter-conditions-tag"
>
{Object.keys(filterConditions).length}
</span>
</Button>

{isFilterBoxOpen && (
<div className="usa-combo-box top-full left-0">
<div className="usa-combo-box margin-top-1 bg-white position-absolute radius-md shadow-2 z-top maxh-6205 width-4305">
angelathe marked this conversation as resolved.
Show resolved Hide resolved
{/* Title */}
<legend className="line-height-sans-6 text-bold font-sans-xs padding-y-1 padding-x-105">
Filter by Reportable Condition
</legend>

{/* Select All checkbox */}
<div className="display-flex flex-column">
<div
className="checkbox-color usa-checkbox padding-bottom-1 padding-x-105"
key={"all"}
>
<input
id={"condition-all"}
className="usa-checkbox__input"
type="checkbox"
value={"all"}
onChange={handleSelectAll}
checked={
Object.keys(filterConditions).length === conditions.length
angelathe marked this conversation as resolved.
Show resolved Hide resolved
}
/>
<label
className="line-height-sans-6 font-sans-xs margin-y-0 usa-checkbox__label"
htmlFor={"condition-all"}
>
{isAllSelected ? "Deselect all" : "Select all"}
</label>
</div>
<div className="border-top-1px border-base-lighter margin-x-105"></div>

{/* (Scroll) Filter Conditions checkboxes */}
<div className="position-relative bg-white overflow-y-auto maxh-38 display-flex flex-column gap-1 padding-y-1 padding-x-105">
{conditions.map((condition) => (
<div
className="checkbox-color usa-checkbox"
key={condition}
>
<input
id={`condition-${condition}`}
className="usa-checkbox__input"
type="checkbox"
value={condition}
onChange={handleCheckboxChange}
checked={!!filterConditions[condition]}
angelathe marked this conversation as resolved.
Show resolved Hide resolved
/>
<label
className="line-height-sans-6 font-sans-xs margin-y-0 usa-checkbox__label"
htmlFor={`condition-${condition}`}
>
{condition}
</label>
</div>
))}
</div>
</div>

{/* Apply Filter Button */}
<div className="display-flex flex-column flex-stretch padding-x-105">
<div className="border-top-1px border-base-lighter margin-x-neg-105"></div>
<Button
id="button-filter-conditions-apply"
className="margin-y-1 margin-x-0 padding-y-1 padding-x-205 flex-fill"
aria-label="Apply filter"
onClick={(e) => {
e.preventDefault();
updateFilterConditionsQuery(filterConditions);
}}
type="button"
>
Apply filter
</Button>
</div>
</div>
</div>
)}
</div>
</div>
);
};

export default Filters;
10 changes: 6 additions & 4 deletions containers/ecr-viewer/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import EcrPaginationWrapper from "@/app/components/EcrPaginationWrapper";
import EcrTable from "@/app/components/EcrTable";
import LibrarySearch from "./components/LibrarySearch";
import NotFound from "./not-found";
import Filters from "@/app/components/Filters";

/**
* Functional component for rendering the home page that lists all eCRs.
Expand All @@ -22,14 +23,14 @@ const HomePage = async ({
const sortColumn = (searchParams?.columnId as string) || "date_created";
const sortDirection = (searchParams?.direction as string) || "DESC";
const searchTerm = searchParams?.search as string | undefined;
// Placeholder for given array of conditions to filter on, remove in #2751
const filterConditions = undefined; // Ex. ["Anthrax (disorder)", "Measles (disorder)"];
const filterConditions = searchParams?.condition as string | undefined;
const filterConditionsArr = filterConditions?.split("|");

const isNonIntegratedViewer =
process.env.NEXT_PUBLIC_NON_INTEGRATED_VIEWER === "true";
let totalCount: number = 0;
if (isNonIntegratedViewer) {
totalCount = await getTotalEcrCount(searchTerm, filterConditions);
totalCount = await getTotalEcrCount(searchTerm, filterConditionsArr);
}

return isNonIntegratedViewer ? (
Expand All @@ -43,14 +44,15 @@ const HomePage = async ({
textBoxClassName="width-21-9375"
/>
</div>
<Filters />
<EcrPaginationWrapper totalCount={totalCount}>
<EcrTable
currentPage={currentPage}
itemsPerPage={itemsPerPage}
sortColumn={sortColumn}
sortDirection={sortDirection}
searchTerm={searchTerm}
filterConditions={filterConditions}
filterConditions={filterConditionsArr}
/>
</EcrPaginationWrapper>
</main>
Expand Down
18 changes: 14 additions & 4 deletions containers/ecr-viewer/src/app/services/listEcrDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,13 @@ export const generateFilterConditionsStatement = (
) => ({
rawType: true,
toPostgres: () => {
if (!filterConditions) {
return pgPromise.as.format("NULL IS NULL");
if (
!filterConditions ||
filterConditions.length === 0 ||
angelathe marked this conversation as resolved.
Show resolved Hide resolved
filterConditions.every((item) => item === "")
) {
const subQuery = `SELECT DISTINCT erc_sub.eICR_ID FROM ecr_rr_conditions erc_sub WHERE erc_sub.condition IS NOT NULL`;
return `ed.eICR_ID NOT IN (${subQuery})`;
}

const whereStatement = filterConditions
Expand All @@ -371,8 +376,13 @@ export const generateFilterConditionsStatement = (
const generateFilterConditionsStatementSqlServer = (
filterConditions?: string[],
) => {
if (!filterConditions) {
return "NULL IS NULL";
if (
!filterConditions ||
filterConditions.length === 0 ||
filterConditions.every((item) => item === "")
) {
const subQuery = `SELECT DISTINCT erc_sub.eICR_ID FROM ecr_rr_conditions erc_sub WHERE erc_sub.condition IS NOT NULL`;
return `ed.eICR_ID NOT IN (${subQuery})`;
}

const whereStatement = filterConditions
Expand Down
Loading
Loading