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

[Issue #1489] Complete search filter accordion logic #1490

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 || ""}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Allow a value to be passed in case we want to explicitly have a value set for checkbox submissions on search

Without this it's just { name: "status-forecased", value: "on"}

/>
);

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,
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

More logic was added to useSearchFilter since the SearchFilterAccordion is getting sizeable


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?.();
}}
Copy link
Contributor Author

@rylew1 rylew1 Mar 18, 2024

Choose a reason for hiding this comment

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

select all/clear all options need to prevent form submit. The actual query param update and form submit happens together in useSearchFilter

>
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}
/>
);
}
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
Loading