Skip to content

Commit

Permalink
[Issue #1489] Complete search filter accordion logic (#1490)
Browse files Browse the repository at this point in the history
Fixes #1489 

- Finish debounced update for accordion filter (agency and funding
instrument)
  • Loading branch information
rylew1 authored Mar 18, 2024
1 parent 606c89f commit fb33b83
Show file tree
Hide file tree
Showing 24 changed files with 420 additions and 128 deletions.
51 changes: 27 additions & 24 deletions frontend/src/app/search/SearchForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"use client";

import React, { useRef } from "react";

import { ConvertedSearchParams } from "../../types/requestURLTypes";
import { SearchAPIResponse } from "../../types/searchTypes";
import SearchBar from "../../components/search/SearchBar";
Expand All @@ -11,8 +9,7 @@ import SearchOpportunityStatus from "../../components/search/SearchOpportunitySt
import SearchPagination from "../../components/search/SearchPagination";
import SearchResultsHeader from "../../components/search/SearchResultsHeader";
import SearchResultsList from "../../components/search/SearchResultsList";
import { updateResults } from "./actions";
import { useFormState } from "react-dom";
import { useSearchFormState } from "../../hooks/useSearchFormState";

interface SearchFormProps {
initialSearchResults: SearchAPIResponse;
Expand All @@ -23,34 +20,40 @@ export function SearchForm({
initialSearchResults,
requestURLQueryParams,
}: SearchFormProps) {
const [searchResults, updateSearchResultsAction] = useFormState(
updateResults,
initialSearchResults,
);

const formRef = useRef(null); // allows us to submit form from child components

const { status, query, sortby, page } = requestURLQueryParams;

// TODO: move this to server-side calculation?
const maxPaginationError =
searchResults.pagination_info.page_offset >
searchResults.pagination_info.total_pages;
// Capture top level logic, including useFormState in useSearhcFormState hook
const {
searchResults, // result of calling server action
updateSearchResultsAction, // server action function alias
formRef, // used in children to submit the form
maxPaginationError,
statusQueryParams,
queryQueryParams,
sortbyQueryParams,
pageQueryParams,
agencyQueryParams,
fundingInstrumentQueryParams,
} = useSearchFormState(initialSearchResults, requestURLQueryParams);

return (
<form ref={formRef} action={updateSearchResultsAction}>
<div className="grid-container">
<div className="search-bar">
<SearchBar initialQuery={query} />
<SearchBar initialQueryParams={queryQueryParams} />
</div>
<div className="grid-row grid-gap">
<div className="tablet:grid-col-4">
<SearchOpportunityStatus
formRef={formRef}
initialStatuses={status}
initialQueryParams={statusQueryParams}
/>
<SearchFilterFundingInstrument
formRef={formRef}
initialQueryParams={fundingInstrumentQueryParams}
/>
<SearchFilterAgency
formRef={formRef}
initialQueryParams={agencyQueryParams}
/>
<SearchFilterFundingInstrument />
<SearchFilterAgency />
</div>
<div className="tablet:grid-col-8">
<div className="usa-prose">
Expand All @@ -59,10 +62,10 @@ export function SearchForm({
searchResultsLength={
searchResults.pagination_info.total_records
}
initialSortBy={sortby}
initialSortBy={sortbyQueryParams}
/>
<SearchPagination
page={page}
initialQueryParams={pageQueryParams}
formRef={formRef}
showHiddenInput={true}
totalPages={searchResults.pagination_info.total_pages}
Expand All @@ -72,7 +75,7 @@ export function SearchForm({
maxPaginationError={maxPaginationError}
/>
<SearchPagination
page={page}
initialQueryParams={pageQueryParams}
formRef={formRef}
totalPages={searchResults.pagination_info.total_pages}
/>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/app/search/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const searchFetcher = getSearchFetcher();
export async function updateResults(
prevState: SearchAPIResponse,
formData: FormData,
) {
): Promise<SearchAPIResponse> {
console.log("formData => ", formData);
const pageValue = formData.get("currentPage");
const page = pageValue ? parseInt(pageValue as string, 10) : 1;
const safePage = !isNaN(page) && page > 0 ? page : 1;
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/FilterCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { Checkbox } from "@trussworks/react-uswds";
import React from "react";

Expand All @@ -8,6 +10,7 @@ interface FilterCheckboxProps {
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
checked?: boolean;
value?: string;
}

const FilterCheckbox: React.FC<FilterCheckboxProps> = ({
Expand All @@ -17,6 +20,7 @@ const FilterCheckbox: React.FC<FilterCheckboxProps> = ({
onChange,
disabled = false, // Default enabled. Pass in a mounted from parent if necessary.
checked = false,
value,
}) => (
<Checkbox
id={id}
Expand All @@ -25,6 +29,7 @@ const FilterCheckbox: React.FC<FilterCheckboxProps> = ({
onChange={onChange}
disabled={disabled}
checked={checked}
value={value || ""}
/>
);

Expand Down
9 changes: 5 additions & 4 deletions frontend/src/components/search/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { useState } from "react";
"use client";

import { useSearchParamUpdater } from "../../hooks/useSearchParamUpdater";
import { useState } from "react";

interface SearchBarProps {
initialQuery: string;
initialQueryParams: string;
}

export default function SearchBar({ initialQuery }: SearchBarProps) {
const [inputValue, setInputValue] = useState<string>(initialQuery);
export default function SearchBar({ initialQueryParams }: SearchBarProps) {
const [inputValue, setInputValue] = useState<string>(initialQueryParams);
const { updateQueryParams } = useSearchParamUpdater();

const handleSubmit = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Accordion } from "@trussworks/react-uswds";
import React from "react";
import { QueryParamKey } from "../../../types/searchTypes";
import SearchFilterCheckbox from "./SearchFilterCheckbox";
import SearchFilterSection from "./SearchFilterSection/SearchFilterSection";
import SearchFilterToggleAll from "./SearchFilterToggleAll";
Expand All @@ -25,12 +25,18 @@ export interface FilterOption {

interface SearchFilterAccordionProps {
initialFilterOptions: FilterOption[];
title: string;
title: string; // Title in header of accordion
initialQueryParams: string; // comma-separated string list of query params from the request URL
queryParamKey: QueryParamKey; // Ex - In query params, search?{key}=first,second,third
formRef: React.RefObject<HTMLFormElement>;
}

export function SearchFilterAccordion({
initialFilterOptions,
title,
queryParamKey,
initialQueryParams,
formRef,
}: SearchFilterAccordionProps) {
// manage most of state in custom hook
const {
Expand All @@ -41,7 +47,12 @@ export function SearchFilterAccordion({
toggleSelectAll,
incrementTotal,
decrementTotal,
} = useSearchFilter(initialFilterOptions);
} = useSearchFilter(
initialFilterOptions,
initialQueryParams,
queryParamKey,
formRef,
);

const getAccordionTitle = () => (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import FilterCheckbox from "../../FilterCheckbox";
import { FilterOption } from "./SearchFilterAccordion";

Expand Down Expand Up @@ -26,9 +28,11 @@ const SearchFilterCheckbox: React.FC<SearchFilterCheckboxProps> = ({
<FilterCheckbox
id={option.id}
label={option.label}
name={option.id} // value passed to server action {name: "{option.label}", value: "on" } (if no value provided)
onChange={handleChange}
disabled={!mounted}
checked={option.isChecked === true}
// value={option.id} // TODO: consider poassing explicit value
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { useEffect, useState } from "react";

import { FilterOption } from "../SearchFilterAccordion";
Expand Down Expand Up @@ -67,7 +69,7 @@ const SearchFilterSection: React.FC<SearchFilterSectionProps> = ({
<SectionLinkCount sectionCount={sectionCount} />
</span>
</button>
{childrenVisible && (
{childrenVisible ? (
<div className="padding-y-1">
<SearchFilterToggleAll
onSelectAll={handleSelectAll}
Expand All @@ -82,11 +84,20 @@ const SearchFilterSection: React.FC<SearchFilterSectionProps> = ({
decrement={decrement}
mounted={mounted}
updateCheckedOption={updateCheckedOption}
// value={child.id} // TODO: consider passing the actual value to the server action
/>
</li>
))}
</ul>
</div>
) : (
// Collapsed sections won't send checked values to the server action.
// So we need hidden inputs.
option.children?.map((child) =>
child.isChecked ? (
<input key={child.id} type="hidden" name={child.value} value="on" />
) : null,
)
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

interface SearchFilterToggleAllProps {
onSelectAll?: () => void;
onClearAll?: () => void;
Expand All @@ -11,15 +13,23 @@ const SearchFilterToggleAll: React.FC<SearchFilterToggleAllProps> = ({
<div className="grid-col-fill">
<button
className="usa-button usa-button--unstyled font-sans-xs"
onClick={onSelectAll}
onClick={(event) => {
// form submission is done in useSearchFilter, so
// prevent the onClick from submitting here.
event.preventDefault();
onSelectAll?.();
}}
>
Select All
</button>
</div>
<div className="grid-col-fill text-right">
<button
className="usa-button usa-button--unstyled font-sans-xs"
onClick={onClearAll}
onClick={(event) => {
event.preventDefault();
onClearAll?.();
}}
>
Clear All
</button>
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/components/search/SearchFilterAgency.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
"use client";

import { SearchFilterAccordion } from "src/components/search/SearchFilterAccordion/SearchFilterAccordion";
import { agencyFilterList } from "./SearchFilterAccordion/filterJSONLists/agencyFilterList";

export default function SearchFilterAgency() {
export interface SearchFilterAgencyProps {
initialQueryParams: string;
formRef: React.RefObject<HTMLFormElement>;
}

export default function SearchFilterAgency({
initialQueryParams,
formRef,
}: SearchFilterAgencyProps) {
return (
<SearchFilterAccordion
initialFilterOptions={agencyFilterList}
title="Agency"
queryParamKey="agency"
initialQueryParams={initialQueryParams}
formRef={formRef}
/>
);
}
15 changes: 14 additions & 1 deletion frontend/src/components/search/SearchFilterFundingInstrument.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
"use client";

import {
FilterOption,
SearchFilterAccordion,
} from "src/components/search/SearchFilterAccordion/SearchFilterAccordion";

export default function SearchFilterFundingInstrument() {
export interface SearchFilterFundingInstrumentProps {
initialQueryParams: string;
formRef: React.RefObject<HTMLFormElement>;
}

export default function SearchFilterFundingInstrument({
formRef,
initialQueryParams,
}: SearchFilterFundingInstrumentProps) {
const initialFilterOptions: FilterOption[] = [
{
id: "funding-opportunity-cooperative_agreement",
Expand Down Expand Up @@ -31,6 +41,9 @@ export default function SearchFilterFundingInstrument() {
<SearchFilterAccordion
initialFilterOptions={initialFilterOptions}
title="Funding instrument"
queryParamKey="fundingInstrument"
formRef={formRef}
initialQueryParams={initialQueryParams}
/>
);
}
10 changes: 6 additions & 4 deletions frontend/src/components/search/SearchOpportunityStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { useEffect, useState } from "react";
"use client";

import { useEffect, useState } from "react";

import { Checkbox } from "@trussworks/react-uswds";
import { useDebouncedCallback } from "use-debounce";
Expand All @@ -12,7 +14,7 @@ interface StatusOption {

interface SearchOpportunityStatusProps {
formRef: React.RefObject<HTMLFormElement>;
initialStatuses: string;
initialQueryParams: string;
}

const statusOptions: StatusOption[] = [
Expand All @@ -28,13 +30,13 @@ const SEARCH_OPPORTUNITY_STATUS_DEBOUNCE_TIME = 500;

const SearchOpportunityStatus: React.FC<SearchOpportunityStatusProps> = ({
formRef,
initialStatuses,
initialQueryParams,
}) => {
const [mounted, setMounted] = useState(false);
const { updateQueryParams } = useSearchParamUpdater();

const initialStatusesSet = new Set(
initialStatuses ? initialStatuses.split(",") : [],
initialQueryParams ? initialQueryParams.split(",") : [],
);

const [selectedStatuses, setSelectedStatuses] =
Expand Down
Loading

0 comments on commit fb33b83

Please sign in to comment.