Skip to content

Commit

Permalink
Filter by Date (Part 1) (#3072)
Browse files Browse the repository at this point in the history
* 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
3 people authored Dec 23, 2024
1 parent 0fa0ec6 commit a9d82ef
Show file tree
Hide file tree
Showing 13 changed files with 1,186 additions and 210 deletions.
186 changes: 186 additions & 0 deletions containers/ecr-viewer/src/app/components/BaseFilter.tsx
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>
);
};
6 changes: 5 additions & 1 deletion containers/ecr-viewer/src/app/components/EcrTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import { listEcrData } from "@/app/services/listEcrDataService";

import { EcrTableClient } from "@/app/components/EcrTableClient";
import { DateRangePeriod } from "@/app/view-data/utils/date-utils";

/**
* eCR Table
Expand All @@ -10,6 +10,7 @@ import { EcrTableClient } from "@/app/components/EcrTableClient";
* @param props.itemsPerPage - The number of items to be displayed in the table
* @param props.sortColumn - The column to sort by
* @param props.sortDirection - The direction to sort by
* @param props.filterDate - The date range used to filter data
* @param props.searchTerm - The search term used to list data
* @param props.filterConditions - (Optional) The reportable condition(s) used to filter the data
* @returns - eCR Table element
Expand All @@ -21,11 +22,13 @@ const EcrTable = async ({
sortDirection,
searchTerm,
filterConditions,
filterDate,
}: {
currentPage: number;
itemsPerPage: number;
sortColumn: string;
sortDirection: string;
filterDate: DateRangePeriod;
searchTerm?: string;
filterConditions?: string[];
}) => {
Expand All @@ -36,6 +39,7 @@ const EcrTable = async ({
itemsPerPage,
sortColumn,
sortDirection,
filterDate,
searchTerm,
filterConditions,
);
Expand Down
Loading

0 comments on commit a9d82ef

Please sign in to comment.