-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* first pass, back-end * first pass, front-end * simplify date range query, dont need subquery * update and add tests * make filtersService file, abstract components * add test for convertDateOptionToDateRange * update date helpers, modify tests * wip, Filters * update local dev default to last year * udpate tests to reflect local dev default * else if nit * refactor: moar abstraction (#3078) * refactor: moar abstraction * tiny nits, update tests * fix test --------- Co-authored-by: angelathe <[email protected]> * refactor Filters file to add BaseFilter file * add BaseFilter * fix todays date for test * move FiltersService to utils as date-utils * add test to check reading query will result in correct date range * Update dateRangelabels Co-authored-by: Mary McGrath <[email protected]> * [pre-commit.ci] auto fixes from pre-commit hooks * reverting change temporarily, causing test failures * update default nits * update default nits * hardcode expected start dates in tests * Flexible widths Co-authored-by: [email protected] * fix small nit, tag disappeared when no conditions were selected * update snapshot test for tag, turn btnClass to functional prop isActive * refactor dateRangeLabels * move default date range const to date-utils because forgot to import in page * update docstrings * refactor updateQueryParam * update today test fixture to add time * add sec/ms to date fixture for extra certainty --------- Co-authored-by: Mary McGrath <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
- Loading branch information
1 parent
0fa0ec6
commit a9d82ef
Showing
13 changed files
with
1,186 additions
and
210 deletions.
There are no files selected for viewing
186 changes: 186 additions & 0 deletions
186
containers/ecr-viewer/src/app/components/BaseFilter.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
import React, { ComponentType, ReactNode, useCallback, useState } from "react"; | ||
import { Button } from "@trussworks/react-uswds"; | ||
import { useRouter, usePathname, useSearchParams } from "next/navigation"; | ||
|
||
/** | ||
* Custom hook to manage query parameters in the URL (set, delete, and update). Hook always resets page back to 1. | ||
* @returns - An object containing | ||
* - searchParams: Current search params from the URL | ||
* - updateQueryParam: Function to update a specific query parameter. | ||
* If an object is passed, its keys that are set to true are concatenated with a |. Otherwise, the value is set directly. | ||
*/ | ||
export const useQueryParam = () => { | ||
const router = useRouter(); | ||
const pathname = usePathname(); | ||
const searchParams = useSearchParams(); | ||
|
||
// Set a query param with a specific value | ||
const setQueryParam = useCallback( | ||
(key: string, value: string) => { | ||
const params = new URLSearchParams(searchParams.toString()); | ||
params.set("page", "1"); | ||
params.set(key, value); | ||
return params; | ||
}, | ||
[searchParams], | ||
); | ||
|
||
// Delete a query param or a specific query param value | ||
const deleteQueryParam = useCallback( | ||
(name: string, value?: string) => { | ||
const params = new URLSearchParams(searchParams.toString()); | ||
params.set("page", "1"); | ||
params.delete(name, value); | ||
return params; | ||
}, | ||
[searchParams], | ||
); | ||
|
||
// Update a specific query param (set or delete if default) | ||
const updateQueryParam = ( | ||
key: string, | ||
value: string | { [key: string]: boolean }, | ||
isDefault: boolean, | ||
) => { | ||
if (typeof value === "object") { | ||
value = Object.keys(value) | ||
.filter((k) => (value as { [key: string]: boolean })[k] === true) | ||
.join("|"); | ||
} | ||
const updatedParams = isDefault | ||
? deleteQueryParam(key) | ||
: setQueryParam(key, value); | ||
|
||
// Update query params | ||
if (searchParams.get(key) !== updatedParams.get(key)) { | ||
router.push(pathname + "?" + updatedParams.toString()); | ||
} | ||
}; | ||
|
||
return { searchParams, updateQueryParam }; | ||
}; | ||
|
||
/** | ||
* A reusable Filter component for eCR Library. It displays a button | ||
* that toggles a dropdown form for filtering data. The form includes functionality | ||
* for resetting and submitting the filter. | ||
* @param props - The props for the Filter component. | ||
* @param props.isActive - Boolean to indicate if a filter is actively filtering eCRs. | ||
* @param props.type - Type of the filter (e.g., "Recieved Date", "Reportable Condition"). | ||
* @param props.title - Title text displayed on the button; defaults to `type`. | ||
* @param props.icon - Icon component rendered inside the filter button. | ||
* @param props.tag - Optional tag element displayed next to the title. | ||
* @param props.resetHandler - Callback for resetting the filter. | ||
* @param props.submitHandler - Callback for applying the filter on form submission. | ||
* @param props.children - The filter form fields and content displayed in the dropdown. | ||
* @returns A JSX element for the filter with a dropdown form. | ||
*/ | ||
export const Filter = ({ | ||
isActive, | ||
type, | ||
title = "", | ||
icon: IconTag, | ||
tag = "", | ||
resetHandler, | ||
submitHandler, | ||
children, | ||
}: { | ||
isActive: boolean; | ||
type: string; | ||
title?: string; | ||
icon: ComponentType<{ className?: string }>; | ||
tag?: ReactNode; | ||
resetHandler: () => void; | ||
submitHandler: () => void; | ||
children: ReactNode; | ||
}) => { | ||
const [isFilterBoxOpen, setIsFilterBoxOpen] = useState(false); | ||
const openBtnRef = React.useRef<HTMLElement | null>(null); | ||
|
||
return ( | ||
<div> | ||
<div className="position-relative display-flex flex-column"> | ||
<Button | ||
className={`margin-right-0 ${isActive ? "filters-applied" : "filter-button"}`} | ||
aria-label={`Filter by ${type}`} | ||
aria-haspopup="listbox" | ||
aria-expanded={isFilterBoxOpen} | ||
onClick={() => { | ||
if (isFilterBoxOpen) { | ||
resetHandler(); | ||
} | ||
setIsFilterBoxOpen(!isFilterBoxOpen); | ||
}} | ||
type="button" | ||
> | ||
<span ref={openBtnRef} className="square-205 usa-icon"> | ||
<IconTag aria-hidden className="square-205" /> | ||
</span> | ||
<span className="text-ink">{title || type}</span> | ||
{tag && ( | ||
<span | ||
className="usa-tag padding-05 bg-base-darker radius-md" | ||
data-testid="filter-tag" | ||
> | ||
{tag} | ||
</span> | ||
)} | ||
</Button> | ||
|
||
{isFilterBoxOpen && ( | ||
<div className="usa-combo-box top-full left-0"> | ||
<form | ||
onSubmit={(e) => { | ||
e.preventDefault(); | ||
submitHandler(); | ||
setIsFilterBoxOpen(false); | ||
openBtnRef?.current?.parentElement?.focus(); | ||
}} | ||
> | ||
<fieldset className="usa-combo-box border-0 padding-0 margin-top-1 bg-white position-absolute radius-md shadow-2 z-top maxh-6205 width-full"> | ||
<FilterLegend type={type} /> | ||
{children} | ||
<ApplyFilterButton type={type} /> | ||
</fieldset> | ||
</form> | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
/** | ||
* A component to render a filter legend (title). | ||
* @param props - React props | ||
* @param props.type - The type of filter | ||
* @returns - The rendered legend element | ||
*/ | ||
const FilterLegend = ({ type }: { type: string }) => { | ||
return ( | ||
<legend className="line-height-sans-6 text-bold font-sans-xs bg-white width-full padding-y-1 padding-x-105 text-no-wrap"> | ||
Filter by {type} | ||
</legend> | ||
); | ||
}; | ||
|
||
/** | ||
* A button component for applying a filter. | ||
* @param props - React props | ||
* @param props.type - The type of filter | ||
* @returns - The rendered button element | ||
*/ | ||
const ApplyFilterButton = ({ type }: { type: string }) => { | ||
return ( | ||
<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 | ||
type="submit" | ||
className="margin-y-1 margin-x-0 padding-y-1 padding-x-205 flex-fill text-no-wrap" | ||
aria-label={`Apply Filter for ${type}`} | ||
> | ||
Apply Filter | ||
</Button> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.